Atelier / Archive Duchastel · Fiches Duchastel → · Français
Turning a four-page blog into a bilingual, cross-referenced archive of people, places, and trades — one that shows its own uncertainty and invites the family to help resolve it. Eight stages, none discarding the last.
/fr/ and /en/
Symmetric. Neither language is second-class, and a third slots in without restructuring.
This is close to greenfield, which is the most useful thing about it. Nothing below is a migration problem — it is a set of decisions that were never made because a blog theme made them for us.
Built for posts, tags, reading time, and reverse-chronological feeds. The model here is
a cross-referenced encyclopedia. /categories/ and /tags/ are live
right now, serving empty pages.
The library sits in static/js/ but no template loads it, so the
data-lightbox link on the family tree just navigates to a raw PNG. Precisely
the small-image-to-popup requirement, currently broken.
No generated thumbnails, no responsive sizes, no small/full-resolution pairing — the exact machinery the media requirement depends on.
Hardcoded to French. Adding the structure now costs a day; adding it after a few hundred pages exist costs a rewrite.
duchastel_maison.md is a single 44 KB page holding Cormontreuil,
Montflambert, Crogny, and Monthoury — each with its own situation, history, images, and
citations. Four places, four pages, four map points.
A static Hugo site with a hybrid core: structured YAML carries the skeleton — people, dates, coordinates, relationships — while markdown carries the narrative. Hugo joins them at build time. Facts stay consistent and machine-checkable; prose stays pleasant to write.
Hardcoding a fixed set of types would be a mistake, and avoiding it costs almost nothing when done from the start. So types are not code — types are data. A type descriptor declares its fields, which sections it supports, and how it is grouped in directories. A generic template reads the descriptor and renders. Adding a kind of thing means adding a file, not writing a layout.
data/schema/, declared once
Relationships are registered the same way types are: each declares its endpoints, its cardinality, its inverse, and its label in both languages.
data/, what gets typed
Register a new type in the top band and its pages, directory, and search entries appear here — with no template written.
Family is not a safe type. It conflates three different things: a surname lineage, a conjugal unit, and a household at a moment in time. The family records prove the problem — du Chastel, Duchastel, Duchatel, Dûchatel, and Duchastel de Montrouge are one lineage with five spellings, and people marry in and out of it constantly.
A lineage is a facet for grouping and search, not a membership. It still gets a page, and that page still lists key members — but that list is curated by hand, not computed. A family page is a story someone chooses to tell, not a set the machine derives.
The conjugal unit becomes a union record, and descent becomes a parentage record. Both are first-class objects rather than fields on a person.
Moving unions and parentage off the person and into their own records is the single change that makes the difficult cases easy. Divorce, second and third marriages, unknown parents that become known — none of them are special cases in the code. They are field values.
| Case | How it is recorded |
|---|---|
| Marriage ended by death | end.reason: death |
| Divorce | end.reason: divorce |
| Second, third marriage | Another union record, order: 2, order: 3 |
| Children without marriage | kind: liaison |
| Spouse known to exist but unnamed | partners: [x, null] |
| Parents unknown | No parentage record — nothing to contradict later |
| Parents presumed, then proven | One parentage record; raise its conf |
| Two candidate fathers | Two parentage records, each with its own confidence |
| Adoption, step-parentage | kind: adoptive, kind: step |
Every one of these is worked through on real records from the family notes in Fiches Duchastel — including the second marriage to an unnamed wife, the parentage the archivist could only presume, and the construction date two sources disagree about.
Building generic, reusable pieces rather than one timeline at a time is what makes the later stages cheap. Seven components cover every requirement.
Any array of dated entries, rendered vertically inline or compactly in a popup. Written once, driven by data.
any object typeHugo generates responsive thumbnails; clicking opens an overlay with caption, credit, and a download button for the full-resolution original.
every pageFocus draws 2–4 generations around one person. Sparse draws many generations with few nodes, to expose links between lineages.
person · lineageKeyed markers with a legend beneath, and hover tooltips carrying their own text. Scales from a city quarter to a continent.
place · lineage · globalLinks to related objects, resolved from IDs so a renamed page can never leave a dead link behind.
every pageTwo collapsible blocks for what is unresolved and what is worth recording. Optional everywhere, and collected into the research log.
every pageGenerated listing over any registered type, grouped by lineage, period, region, or trade. New objects appear without touching a template.
any typeThe decision most likely to cause pain if deferred. A date written as prose — « vers 1757 » — is frozen in French forever, and invisible to the privacy rule.
| Stored | Renders (fr) | Renders (en) | Why it matters |
|---|---|---|---|
{year: 1757, qual: circa} | vers 1757 | c. 1757 | Translates itself |
{year: 1802, qual: before} | avant 1802 | before 1802 | Sorts correctly on a timeline |
{y: 1813, m: 5, d: 19} | 19 mai 1813 | 19 May 1813 | Formats per locale |
{year: 1955} | — suppressed by privacy rule — | Checked mechanically, everywhere | |
Only birth dates are protected. This matches how the records themselves are governed: birth records are typically restricted for 75–100 years, while death and marriage records are restricted for only 50–75. And the century threshold is the genealogical standard — British genealogists publish under a “GEN100” mark to signal exactly this cut-off, on the working assumption that anyone born within the past hundred years is living unless known otherwise.
showBirthDate(person, today):
# explicit overrides always win
if person.privacy == public -> FULL
if person.privacy == private -> HIDDEN
# 1. the century rule
if today.year - birth.year >= 100 -> FULL
# 2. the decade-after-death rule
if death.year and today.year - death.year >= 10 -> FULL
# otherwise reduce, never erase
if birth.year -> DECADE # « née dans les années 1940 »
else -> HIDDEN
always public: death dates, marriage dates, places, names
always suppressed with the birth date: age at death, age at
marriage, age recorded in any census
A decade is safe on its own. It stops being safe next to a death date and an age. Show “born in the 1940s”, “died 2012”, and “died aged 67” together, and 1945 has been republished exactly — any two of those three recover the third.
So suppression has to cascade to computed values, not just to the stored date. That is the real reason this lives in one function: age-at-death is calculated in a dozen places, and each one has to ask permission.
Michèle — born 1945, died 2012. In 2020: 75 years since birth (under 100) and 8 years since death (under 10), so the birth reduces to années 1940 while the death date shows in full. In 2026: 14 years since death clears the second rule, so 1945 publishes. That is the intended behaviour.
Worth knowing that the dominant software goes further: webtrees hides everything about a living person and prints “Private” as the name. That suits a site open to strangers. For a family archive, showing the name and reducing the date to a decade is the better trade — a tree is unreadable when it is impossible to tell which generation someone belongs to.
The split runs through pages, not between them. A single place page carries all three tiers at once — here is the Montflambert material, sorted:
| Content | Tier | Treatment |
|---|---|---|
| Interface, headings, père/father, dates | 1 | Both, automatically — a fixed table of ~80 strings |
| “Situé à environ 29 km au sud de Reims” | 2 | Both — original prose, and it is short |
| “1753 (14 avril) — vendu à Jean-Baptiste du Chatel” | 1 | Stored as data; renders in either language |
| The 1751 marriage-contract extract | 3 | French verbatim, English gloss above it |
| “Maintenant un Bed and Breakfast” | 2 | Both — practical information for a visiting relative |
| Jean-Baptiste · Cormontreuil · Reims | — | Never translated |
Translating a notarial act would destroy its value as evidence — a reader who wants to verify a claim needs the words actually in the book. The gloss carries the meaning; the quote carries the proof.
Because everything is entered by hand and nothing needs to import, the format can be designed for the person typing it. One principle governs the whole thing:
A birth year is the same number in French and English. Only the writing differs. So facts live once, in YAML shared by both languages, and each language gets a thin markdown file holding only its narrative. Nothing factual is stored twice, so nothing factual can drift.
data/schema/
types/person.yaml # the registry: what a person IS
types/place.yaml
relations/union.yaml # endpoints, cardinality, inverse, labels
relations/parentage.yaml
data/
persons/duchastel-louis-francois-1784.yaml
places/monthoury.yaml
unions/duchastel-legoix-1813.yaml
parentage/legoix-sophie-1790.yaml
sources/ad-aisne-4q1e2856.yaml
content/fr/ personnes/… lieux/… # French narrative only
content/en/ persons/… places/… # optional English
id: union
endpoints: [person, person]
ordered: false # partners are symmetric
multiple: true # a person may have several
label: { fr: "union", en: "union" }
fields: [kind, order, begin, end, conf, sources]
Adding apprenticeship as a person-to-person relation, or artefact as
a new type, means writing one file like the above. Nothing in the templates changes.
surname-given-birthyear for people, a short slug for everything else. They
appear in every cross-reference, so they are chosen once and never renamed. An unknown year gets
-nd; a genuine collision gets -b.
This is standard practice. GEDCOM has carried a
QUAY tag for this since the
1990s — “an assessment of the certainty of the evidence.” Gramps implements it too, and
puts the field on the
citation rather than on the person, which turns out to be the important part.
Three websites copying the same parish register are one source, not three. A single original register entry — written by a participant, at the time — outranks all of them. Evidence Explained splits this into three axes: original or derivative, primary or secondary, direct or indirect or negative.
Three axes is too much to fill in by hand a thousand times. So one field is kept — but it records a judgement of the evidence, not a count of rows.
A hypothesis is not “low confidence” — it is a different kind of claim. Three values form an ordinal ramp; two are categorical flags drawn deliberately unlike the ramp.
| Value | Mark | Means | Typical evidence |
|---|---|---|---|
proven |
●Attesté | Documented and consistent; defensible | Original record, primary information |
probable |
◐Probable | One good source, nothing contradicting it | A single register, a reliable published work |
possible |
○Possible | Weak or indirect; plausible but unconfirmed | Derivative source, family recollection |
hypothesis |
◈Hypothèse | A proposal, not yet evidence at all | Reasoning from names, places, or gaps |
disputed |
▲Contradiction | Sources genuinely conflict, and it is unresolved | Two records giving different years |
The ramp reads by fill (● ◐ ○); the two flags take an outline instead, so they read as a different kind of mark rather than a further step down the same scale. Each carries a word as well as a shape.
This survives greyscale printing, photocopying, every form of colour vision deficiency, and a phone in sunlight — which matters more than usual here, since a genealogy audience skews older and roughly one man in twelve has difficulty with red and green. If colour is ever wanted, it layers on as a redundant second signal without touching any data.
Louis-François’s marriage date can be proven while his occupation is
possible and his parentage a hypothesis — all on one person, each
displayed differently, each collected separately.
WikiTree is the instructive case: it enforces at least one source per profile, flags unsourced profiles openly, runs an annual “Source-a-Thon” where progress is visible, and routes every doubt into a genealogist-to-genealogist forum. FamilySearch recommends the same core move. Distilled for a family site with no accounts:
mailto: carrying a pre-filled subject as the
zero-infrastructure fallback.Three maintenance pages, all generated from records already written, none hand-maintained. They are the same machinery pointed at three different questions — which is why the third one costs almost nothing once the first exists.
Every assertion marked hypothesis, disputed, or
possible, plus every questions: entry. Grouped by lineage, place,
and period; each with its own anchor, so a cousin can be sent one question rather than a
page of them.
Every object whose French page exists without an English counterpart, and every tier-2 summary still missing one. Sorted so the highest-traffic pages surface first, with a word count, so a job can be picked to fit the time available.
what is not yet bilingualBroken references, relations whose endpoints do not match the registry, media files that do not exist, unparseable dates, people with no sources at all. Fails the build rather than publishing the error.
what is malformedA missing translation is structurally identical to an open question: a gap the site can detect by itself. Hugo already knows which language versions of a page exist, so the queue is a list derived from pages, not a to-do list anyone maintains. It cannot go stale, and it cannot forget anything.
It also gives the tiers teeth. A missing tier-2 summary is flagged as blocking — an English reader cannot understand the subject without it. A missing tier-3 narrative is flagged optional, because the French plus an English gloss was always the intended end state.
Ordered by dependency, not by appeal. Each stage leaves the site working and deployed; none requires discarding the previous one. The numbering is real — trees need people, people need the registry, the registry needs the foundations.
Where things stand
Stage 0 was completed on 21 August 2026 — these pages are its output, published at
/atelier/, unlisted and outside the sitemap. Stage 1 followed on 31 August 2026:
PaperMod is gone, the theme is ours, and the site is bilingual on symmetric
/fr/ and /en/ URLs. Stage 2 landed on 1 September 2026 — types
and relations are declared as data, the four houses have their own pages, and the build
refuses to publish a cross-reference that does not resolve. Stage 3, the common sections
and the workbench, is next.
Publish these planning pages on the site itself, under an unlisted path
(/atelier/), linked to each other but from nothing else. Excluded from the
sitemap, marked noindex, and deliberately not named in
robots.txt — which would advertise the very path meant to stay quiet. Ships first because it costs an afternoon and makes every later stage reviewable
by the family.
ResultOne link to send the family, and a habit of writing decisions down where they can be argued with.
Nothing new appears, and everything afterwards depends on it. Retire PaperMod for a
custom theme built around objects rather than posts. Stand up /fr/ and
/en/ routing with the interface string table. Convert images to page bundles
so Hugo can process them. Build the media component — which finally fixes the lightbox.
Write the date model and the privacy function. Upgrade Hugo from 0.147.7 to 0.165.0
in the same pass — PaperMod is the only thing that has ever broken on a version bump,
and it is being deleted here anyway.
ResultThe same four pages, but bilingual, with a working image viewer and dates that know what they are.
The spine. Build the type and relation registry, then declare the first types and
relations in it. Generate a page per object; resolve cross-references from IDs and build
the reverse indexes. The conf field ships here rather than later —
retrofitting confidence onto a thousand facts already entered is the one mistake this
plan cannot absorb. Prove it on real material by splitting
duchastel_maison.md into four places.
ResultOne 44 KB page replaced by four navigable places — and the ability to add a fifth kind of thing without writing a template.
The optional blocks for every page: timeline in both inline and popup modes, cross-references, questions, notes. Then the three generated maintenance pages — research log, translation queue, build report — which are indexes over data that already exists rather than new machinery.
ResultEvery uncertain claim visibly marked, one page collecting every open question, and one page listing everything awaiting translation.
Enter the data. Person records, union records, parentage records — including the awkward ones: remarriages, unnamed spouses, presumed descent. Lineage pages with curated key members. Day and Dion stop being nine-word stubs.
ResultA real archive rather than a demonstration — the point where the site becomes worth showing to relatives.
Both modes, on the same underlying records. Focus draws two to four generations around a chosen person. Sparse spans many generations while omitting most individuals, so the links between the Duchastel, Day, and Dion lineages become visible at a glance. Unnamed and living nodes render without leaking anything.
ResultThe relationships legible without reading a single page of prose.
Leaflet with self-hosted tiles — no API key, no per-view billing, nothing to break when a vendor changes terms. Keyed markers with a legend beneath and hover tooltips carrying their own text, at both scales: the Reims-and-Champagne view, and the broader view showing movement between countries.
ResultEvery place record on a map, and the family's geography as a thing that can be pointed at.
Grouped directories over every registered type. Client-side search. The one-click reply channel on every open question, with a contributor credit line. Then the tier-2 English pass, working down the translation queue — done last, deliberately, because translating content that is still moving wastes the effort twice.
ResultThe destination: a bilingual, searchable, cross-referenced archive that a cousin in either language can navigate unaided.
Nothing here blocks Stage 1 any more. These are choices worth turning over before they arrive rather than after.
The date rule is settled and the name rule follows from it — living people keep their names, which is what makes a family tree readable. Photographs are the one case still undecided, and they behave differently: a portrait is not a fact that ages out, and the person in it may simply not want it published.
The proposal is that images default to visible for the deceased and to hidden for the
living, with a per-image override — the same privacy: field the person record
already carries. That keeps it mechanical rather than a judgement call.
/atelier/ need to be?Unlisted is not private. Anyone with the URL can read it, and URLs leak — through referrer headers, browser sync, and forwarded email. For planning documents that is almost certainly fine; there is nothing sensitive in them.
If it ever needs to be genuinely closed, Cloudflare Access sits in front of a path and sends a one-time code to approved email addresses. It is free for up to 50 users, needs no accounts for family members, and runs on the existing hosting. Worth knowing it exists before the first page containing living relatives goes up.
possible appear in the research log?Hypotheses and contradictions clearly belong there. Weakly-sourced facts are a judgement call: including them makes the log a complete to-do list, but a long one that may read as discouraging. The proposal is to include them, grouped separately under à confirmer rather than à résoudre.
A mark on every single date makes a page look like a spreadsheet. The alternative is to
mark only possible, hypothesis, and disputed —
letting an unmarked fact mean “established” — with a toggle that reveals all of them.
Quieter pages, same information, one click away. A Stage 3 decision.
The choices behind the design, with what each one rules out. Recorded so that anyone joining later can see not only what was decided but what was considered and rejected — and can reopen a decision on the evidence rather than on memory.
A birth date is published if the birth was at least 100 years ago, or the death is known and at least 10 years past. Death dates, marriage dates, places, and names stay public throughout.
Birth records are the restricted class — typically closed 75–100 years, against 50–75 for deaths and marriages. The century threshold is the genealogical norm, published in Britain under the “GEN100” mark.
One consequence is accepted deliberately: because the second rule depends only on the death date, someone who died young becomes public sooner — a child born in 2010 and died in 2015 is fully public in 2025. Genealogically that is correct, and those dates matter.
RejectedHiding everything about a living person, as webtrees does. Correct for a site open to strangers; for a family archive it makes the tree unreadable.
RejectedAn age floor — never publishing a full birth date under roughly 50 years, regardless of death. It would withhold the dates the two rules exist to release, and the case it guards against is both rare and genealogically correct.
Reducing rather than erasing keeps generations legible, which is what a tree needs to be worth looking at.
This forces a second rule: suppression must cascade to computed values — age at death, age at marriage, age in a census. A decade beside a death date and an age recovers the exact year.
Roughly one man in twelve has a colour vision deficiency, a genealogy readership skews older, and red-versus-green is the most commonly confused pair of all. The marks stay readable in greyscale, in print, and on a phone in sunlight.
RejectedA green/amber/red traffic light. Colour can still be layered on later as a redundant second signal without changing any data.
One person can carry a proven marriage date, a possible occupation, and a hypothetical
parentage at once. This follows GEDCOM's QUAY tag and Gramps, both of which
put the field on the citation.
RejectedA single reliability score per person, and counting sources as a proxy for confidence — three sites copying one parish register are one source.
The word conflates a surname lineage, a conjugal unit, and a household at a moment in time. It is replaced by a lineage facet whose member list is curated by hand, plus union and parentage records.
The records force this: du Chastel, Duchastel, Duchatel, Dûchatel, and Duchastel de Montrouge are one lineage under five spellings, with people marrying in and out continually.
Divorce, second and third marriages, spouses known to exist but not named, and parentage that is presumed before it is proven all become ordinary field values instead of special cases in the code.
Both are declared as data in data/schema/, and a generic template renders
from the descriptor. A new kind of thing costs one file, not a layout.
RejectedA fixed set of four hardcoded types, which would have to be unpicked the first time the material needed a fifth.
A native format, designed for the person typing it, entered by hand. A converter from an external genealogy system can be written at any point, because it would only ever need to write into these files.
RejectedImporting GEDCOM, or mirroring an external tool. Round trips through GEDCOM lose data, and a mirror produces two versions of the same person.
Interface, labels, and dates in both languages; family, person, and place summaries in both. The deep archival narrative stays French, with English glosses introducing quoted sources. Primary sources are never translated.
RejectedFull parity, which doubles every paragraph written from now on; and interface-only translation, which leaves an English reader able to navigate but unable to understand any subject.
Hugo on Cloudflare Pages: free, fast, nothing to patch, no database to back up.
Rejectedwebtrees and TNG. Both are richer and support multi-user editing, but need PHP and MySQL on a maintained server — and the editing model here is one person working through git.
These pages sit outside the site's navigation and its sitemap, but anyone holding the address can read them. That is accepted for planning documents, which contain nothing sensitive.
If a page ever holds details of living relatives, Cloudflare Access can close the path properly — a one-time code to approved addresses, free for up to 50 people, on the existing hosting.