Relationships & Genealogy
mbr tracks two kinds of connections between notes:
- Content links — ordinary markdown links (
[text](url)), tracked as inbound/outbound “backlinks”. These say that two notes are connected. - Typed relationships — named, directed edges declared in YAML frontmatter.
These say what connects two notes (a
parent, aspouse, asibling…), and can carry their own attributes (marriage dates, places, notes).
The driving example is genealogy: each note is a person, edges are
parent / child / spouse / sibling, birth/death dates live on the person
notes, and marriage/divorce dates live on the relationship.
Typed relationships are on by default and cost nothing when unused. Disable them
with relationship_tracking = false (or --no-relationship-tracking).
Why
I use markdown to track everything. Lately my wife has been into genealogy, but uses various sites and one offline program to track things. I just don’t believe these will all be around in ten years and I certainly don’t want to pay subscription fees for that time.
To me, a family tree is more about stories than names and dates, which makes markdown notes perfect. We can track structure between them (something we already do anyway) and visualize that structure, but otherwise, they’re just notes. This also gives a chance to double own on alignment with OKF allowing notes to be used for more than just prose. It’s a test case for prose with optional structures.
We’ll see where this goes, but really this note type could be used for other things, too, like tracking contacts. Which would allow linking to referenced people and having their contact info at my fingertips. This seems so much better than my current system that intermingles people from different places and contexts.
The type field
Notes may declare a first-class, self-descriptive type (like slides):
---
type: person
title: John Doe
---
Unknown types are tolerated — type is a plain string that flows through
frontmatter unchanged and is exposed to templates and site.json.
Person fields
A type: person note may carry optional, self-descriptive attributes. All are
ordinary frontmatter — they flow through to templates and site.json unchanged —
but mbr gives a few of them first-class treatment:
| Field | Type | Purpose |
|---|---|---|
born | date/string | Birth date; shown on its own line in the person infobox with a 📅 icon, humanized when it’s an ISO date (e.g. 1855-10-30 → October 30, 1855). Other formats display as written. |
died | date/string | Death date; shown on its own line beneath born, humanized the same way. |
born_place | string | Birthplace; displayed on the person’s page beneath the dates. |
image | path or URL | Portrait; displayed on the person’s page. |
gender | string | Styles the genealogy charts (e.g. male, female) — card tinting and parent-line colors; any value is accepted. |
aliases | array of strings | Alternate and maiden names (see below). |
---
type: person
title: Mary Doe
born: 1927-03-19
died: 2010-08-05
born_place: "Boulder, CO"
gender: female
image: /people/mary.jpg
aliases:
- Mary Smith # maiden name
- "Mrs. John Doe"
relationships:
- type: spouse
to: "[[John Doe]]"
---
Aliases
aliases lists alternate names for a person — most usefully maiden names and
married names. They do two things:
- Endpoint resolution. A relationship endpoint that names an alias — written
as a
[[Wikilink]]or a path — resolves to this note, exactly like itstitleor filename stem. So a parent can point achildedge atto: "[[Mary Smith]]"(her maiden name) even though her note is titled Mary Doe, and it still links up. - Search. Aliases are searchable, so looking up a maiden name finds the person.
Resolution order is title → alias → filename stem (all case-insensitive). As with titles, an ambiguous name shared by multiple notes resolves deterministically to the note with the lexicographically smallest URL.
All of these fields are optional; a person note with none of them behaves exactly as before.
The relationships field
Relationships are a single generic array of edge objects, so the same mechanism works for any domain (genealogy is just one):
---
type: person
title: John Doe
born: 1925-06-02 # node attribute on the person — ordinary frontmatter
died: 1999-11-20
relationships:
- type: spouse # required predicate
to: "[[Mary Smith]]" # endpoint
married: 1948-06-01 # arbitrary edge attributes, preserved verbatim
place: "Denver, CO"
- type: child
to: "[[Alice Doe]]" # "Alice is my child"
- type: parent
to: "[[George Doe]]" # "George is my parent"
---
Endpoints and direction
Each edge connects a subject and an object:
to:names the object. Whenfromis omitted the subject defaults to the current note, sotype: X, to: [[Y]]reads “Y is my X”.from:names the subject. Whentois omitted the object defaults to the current note, sotype: X, from: [[Y]]reads “I am Y’s X”.- If both are present, the edge connects two other notes (useful for authoring a graph from a single index note).
- If neither is present, the edge is skipped with a warning.
For genealogy the most natural form is to: from each person’s own note:
- type: parent
to: "[[Sam Doe]]" # Sam is my parent → I show up under Sam's Children
- type: child
to: "[[Alice Doe]]" # Alice is my child → I show up under Alice's Parents
Endpoint values
Endpoints may be:
- A
[[Wikilink]]— resolved by note title first, then by filename stem (case-insensitive).[[Target|Alias]]is supported (the target is used for resolution). - A path — absolute (
/people/john.md) or relative (../john.md), resolved the same way markdown links are.
If an endpoint can’t be resolved, the raw string is kept for display and a warning is emitted — a broken relationship never fails a render or build. Ambiguous names (two notes with the same title) resolve deterministically to the note with the lexicographically smallest URL, and mbr now tells you when it had to make that choice — see Data problems mbr reports.
Body [[Wikilinks]] in page text resolve the same global way as relationship
endpoints: the current folder is preferred, otherwise the first matching note in
any folder (by title, alias, then filename stem) is used.
Reserved keys and attributes
On each edge object, type, to, from, and label are reserved. Every
other key is a free-form edge attribute, preserved verbatim (dates, places,
notes, numbers, booleans…) and shown in the info panel and exposed as JSON.
Automatic reverse edges
You declare each edge once; the reverse is derived automatically, exactly
like backlink derivation. This is driven by the relationship_types registry,
which classifies each type as:
- symmetric — the reverse reads the same (
spouse,sibling). - inverse pair — the reverse reads as the paired type (
parent↔child). - directed (anything not listed) — still tracked and visible from both sides, but not relabelled.
So if George’s note declares type: child, to: [[John Doe]] (“John is my
child”), then:
- George’s note shows John under Children.
- John’s note shows George under Parents (derived, no editing needed).
Reciprocal declarations are de-duplicated: if John also declares
type: parent, to: [[George Doe]], both sides collapse to the same single edge.
The built-in genealogy defaults are:
relationship_types = [
{ name = "parent", inverse = "child", label = "Parent", label_plural = "Parents" },
{ name = "child", inverse = "parent", label = "Child", label_plural = "Children" },
{ name = "spouse", symmetric = true, label = "Spouse", label_plural = "Spouses" },
{ name = "sibling", symmetric = true, label = "Sibling", label_plural = "Siblings" },
]
Add your own domain types (e.g. { name = "manager", inverse = "report" }) in
.mbr/config.toml.
Note: setting
relationship_typesreplaces the defaults wholesale — the genealogy types above are not merged in. Copy the ones you still want.
Declaring one half of an inverse pair
You only need to declare one half of an inverse pair. Given
relationship_types = [
{ name = "manager", inverse = "report" },
]
mbr auto-registers the missing report type with inverse = "manager" and an
auto-derived label (“Report” / “Reports”), so both sides relabel correctly and
reciprocal declarations still collapse to a single edge. Declare the other half
explicitly only when you want custom labels or an irregular plural:
relationship_types = [
{ name = "manager", inverse = "report", label_plural = "Managers" },
{ name = "report", inverse = "manager", label = "Direct report" },
]
Explicit declarations always win over auto-registration. If the two halves
disagree (manager names report as its inverse, but report names something
else), both are kept exactly as written and a warning is logged naming both —
one direction will not be relabelled as you expect. A type declared as both
symmetric and inverse keeps its symmetric meaning; the inverse is ignored
with a warning.
Genealogy walkthrough
A three-generation family, one note per person, each edge declared once:
# people/george.md
---
type: person
title: George Doe
born: 1898-02-11
relationships:
- type: spouse
to: "[[Martha Doe]]"
married: 1920-04-10
- type: child
to: "[[John Doe]]"
- type: child
to: "[[Robert Doe]]"
---
# people/john.md
---
type: person
title: John Doe
born: 1925-06-02
relationships:
- type: spouse
to: "[[Mary Smith]]"
married: 1948-06-01
place: "Denver, CO"
- type: sibling
to: "[[Robert Doe]]"
- type: child
to: "[[Alice Doe]]"
- type: child
to: "[[Sam Doe]]"
---
Even though Sam’s note declares no relationships, opening it shows his
parents (John, Mary) and his sibling (Alice) — all derived from edges declared
elsewhere. Open the info panel (Ctrl+g) to see relationships grouped by
predicate, with edge attributes like married 1948-06-01 · place Denver, CO.
How it’s exposed
links.json(per page, loaded by the info panel): arelationshipsarray of resolved edges alongsideinbound/outboundlinks.site.json: a top-levelrelationship_typesregistry, plus arelationshipsarray on eachmarkdown_filesentry (resolved URLs included), so downstream tooling can consume the whole graph.
Each resolved relationship includes: rel_type (as declared), predicate
(viewpoint label used for grouping), neighbor (resolved URL, empty if
unresolved), neighbor_title, neighbor_raw, resolved, direction
(outgoing/incoming), optional label, attributes, and derived (true
when produced by the reverse-edge rule rather than declared on this note).
Visualizing relationships
mbr ships two complementary visualizations. Both are lazy-loaded (they cost nothing until used) and both work identically in server, GUI, and static-build modes:
- Genealogy charts — interactive family charts rendered inline on
type: personpages. - Link graph in the sidebar — a force-directed mini graph of the current note’s neighbourhood (content links and typed relationships), shown at the top of the info panel on every note.
Breaking change: the old mermaid-based
<mbr-relationships>element has been removed. A custom.mbr/_display_enhancements.htmloverride that still references it silently renders nothing (unknown elements are inert), and itsdepth/max-nodesattributes are superseded by thegraph_depthconfig option and the new elements. Mermaid diagrams in ordinary code blocks are unaffected.
Genealogy charts on person pages
Notes with type: person render an interactive family chart — the
<mbr-genealogy> web component, emitted by _display_enhancements.html for
person pages only. It draws from the resolved relationships in site.json and
renders nothing when the person has no resolved relationships, so a person
note without edges never produces an empty box.
A selector in the top-left corner of the chart switches between two views; the
choice persists in localStorage (mbr_genealogy_chart):
Family chart (default) — built on the family-chart library (ISC license):
- SVG person cards with portraits from the
imagefrontmatter field. - Shows ancestors and descendants two generations each around the current person.
- Expand/collapse of non-blood branches via per-card mini-tree and link-break toggles.
- Native pan/zoom; clicking a card navigates to that person;
⤢recenters.
Timeline tree — a custom time-aware layout:
- Ancestors above, descendants below the current person (two generations each).
- A year axis on the left positions each person by birth year. People
without a
borndate fall back to their generation’s median year, then to estimated 28-year generations; if no one has dates the axis is hidden. - Lines are colored by parent — blue father-lines, pink mother-lines
(gray when
genderis unknown) — and couples are joined by marriage bars. - Pan, zoom, and click-to-navigate as above.
The chart JS is a lazy chunk (/.mbr/components/mbr-genealogy.min.js, ~204 kB
min / ~61 kB gz). Person pages prefetch it, but it loads and renders only when
the chart scrolls near the viewport, so it never blocks page render.
Roadmap: the chart selector is designed for additional chart types — a bubble map of birth places, hierarchical edge bundling across all notes, and an ancestors/descendants sunburst are planned.
When the charts appear
The inline chart appears only on type: person notes that have at least one
resolved relationship. Notes with other type values (type: character,
type: service, …) get no inline chart — their typed relationships appear in
the sidebar link graph (below) and in the info panel’s textual
Relationships section instead.
To gate the chart differently or place it elsewhere, override
.mbr/_display_enhancements.html in your repository:
<!-- .mbr/_display_enhancements.html -->
{% if type and type == "person" %}<mbr-genealogy></mbr-genealogy>{% endif %}
Link graph in the sidebar
The info panel (Ctrl+g / Cmd+g) shows a force-directed mini graph at
the top: the current note plus its neighbourhood of inbound links, outbound
internal links, and typed relationships. It appears on any note that has a
links.json — not just typed notes — so it doubles as a visual backlink map.
- Nodes are colored by degree: the focus note uses the theme’s primary color, and 1st-, 2nd-, … degree neighbours step down a ramp of the same hue.
- Hovering a node shows a card with the note’s title and description (desktop only); clicking navigates; dragging repositions.
- The
⤢button opens a full-screen view with pan/zoom, node labels, and a depth stepper (1–5) to widen or narrow the neighbourhood on the fly.
The default depth comes from the graph_depth config option (default 2,
valid 1–5, env MBR_GRAPH_DEPTH).
The graph is assembled client-side by a breadth-first walk over each
neighbour’s per-page links.json (about 4 fetches in flight at a time, capped
at 80 nodes, cached for the session). Because it needs no dedicated server
endpoint, it works identically in static builds. In server mode, mbr
additionally bounds concurrent inbound-link repository scans so a burst of
links.json requests can’t stampede the server.
The graph JS is a lazy chunk (/.mbr/components/mbr-graph.min.js, d3-force,
~57 kB min / ~19 kB gz) loaded only when the info panel first opens. If link
tracking is disabled (--no-link-tracking / link_tracking = false), there is
no links.json and the graph section simply doesn’t appear.
Data problems mbr reports
Relationship data goes wrong quietly: nothing 404s, nothing fails the build, and
the note just renders with the wrong family. mbr detects the following and
reports each one twice — once as a WARN line when the index is built (both
mbr -s and mbr -b), and once on the affected page itself in the
page-problems panel, which is server/GUI only. Static builds are never
changed by any of this.
Relationship cycles
A parent/child chain that loops back on itself — “A is B’s parent, B is A’s
parent”, or any longer ring. This cannot be true of a real family tree, and it
is actively harmful: the genealogy chart walks the hierarchy and a cycle makes it
unrenderable. mbr reports the loop with every note in it:
WARN relationship cycle over `child`: /people/ada/ -> /people/bob/ -> /people/ada/
— each note is the previous note's `child`, which cannot be true in a
hierarchy (nobody is their own ancestor). ...
Open any note named in the chain and remove or correct the offending edge. Only
hierarchical types are checked — the halves of an inverse pair like
parent/child or employer/employee. Symmetric types (spouse, sibling)
and untyped edges cycle legitimately and are never reported.
The most common way to create one by accident is an ambiguous name (below) attaching an edge to the wrong namesake.
Ambiguous names
Two notes answering to one name — a John Doe Sr and a John Doe Jr, a maiden name
that is also someone else’s title. mbr resolves these first-wins (smallest URL)
and always has; what is new is that it says so, for both relationship endpoints
and body [[Wikilinks]].
Both appear in the page-problems panel. Relationship endpoints additionally warn at startup, because a relationship is declared in a note’s frontmatter and mbr reads every one of them while indexing:
WARN ambiguous relationship endpoint name `[[john doe]]`: resolved to
/people/john-jr/; also matched by /people/john-sr/. mbr always picks the
first — rename a note, give it a distinguishing `aliases:` entry, or name
the file explicitly to choose
Wikilinks are reported only on the page containing the link, never in the
log. Two notes sharing a name is not itself a problem — a link written through
that name is — and the index has no way to know which shared names anything
links to. It also cannot know the answer: wikilinks resolve current-folder
first, so [[Sam]] next to a Sam.md is unambiguous however many other Sams
the repository holds, and elsewhere it resolves to a different note entirely.
Which one you got depends on the page, so that is where it is reported.
To disambiguate, pick any of:
- Give the notes distinct titles (
title: John Doe Sr). - Add an
aliases:entry that is unique to one of them and reference that. - Name the file explicitly instead of using a name:
to: people/john-sr.md. A path endpoint is never ambiguous.
A relationship-endpoint problem is reported on the note that declared the endpoint; a wikilink problem on the page that contains the link. Namesakes that merely share a name and declare nothing are not flagged.
Genealogy repositories can have dozens of shared names, so the startup log shows at most 20 individual endpoint warnings followed by a one-line summary of the rest. The page-problems panel is never truncated — each page always lists all of its own.
Frontmatter parse errors
A single YAML mistake discards the entire frontmatter block, not just the bad
line: type: person, born, aliases and every relationships entry vanish at
once. A duplicated key is the easy one to write by hand:
relationships:
- type: parent
to: "[[Mary Doe]]"
to: "[[Sam Doe]]" # second `to:` — the whole block is now discarded
(Use two separate list entries.) The warning names the file:
WARN Failed to parse YAML frontmatter: String("to"): duplicated key in mapping
at byte 117 line 8 column 9; the whole frontmatter block (including any
`aliases` and `relationships`) is ignored for this note
path=/notes/people/john.md
Watch for this when other notes suddenly report unresolved endpoints: if a note
loses its aliases, every note that referred to it by an alias stops resolving,
so one parse error can look like a dozen unrelated broken relationships.
Configuration
No configuration is needed — reporting is always on and adds no options. It
follows the feature it describes: --no-relationship-tracking turns off
relationship tracking and its reports along with it, and the page-problems panel
as a whole is gated on link tracking.
| Option | Type | Default | Description |
|---|---|---|---|
relationship_tracking | bool | true | Enable typed relationship tracking |
relationship_types | array | genealogy defaults | Relation types and their semantics/labels |
graph_depth | number | 2 | Default neighbourhood depth (1–5) for the sidebar link graph (env MBR_GRAPH_DEPTH) |
CLI: --no-relationship-tracking disables the feature for a single run. See the
Configuration Reference and
CLI Reference.