Relationships & Genealogy

Relationships & Genealogy

mbr tracks two kinds of connections between 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:

FieldTypePurpose
borndate/stringBirth date; shown on its own line in the person infobox with a 📅 icon, humanized when it’s an ISO date (e.g. 1855-10-30October 30, 1855). Other formats display as written.
dieddate/stringDeath date; shown on its own line beneath born, humanized the same way.
born_placestringBirthplace; displayed on the person’s page beneath the dates.
imagepath or URLPortrait; displayed on the person’s page.
genderstringStyles the genealogy charts (e.g. male, female) — card tinting and parent-line colors; any value is accepted.
aliasesarray of stringsAlternate 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:

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:

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:

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:

So if George’s note declares type: child, to: [[John Doe]] (“John is my child”), then:

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_types replaces 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

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:

Breaking change: the old mermaid-based <mbr-relationships> element has been removed. A custom .mbr/_display_enhancements.html override that still references it silently renders nothing (unknown elements are inert), and its depth / max-nodes attributes are superseded by the graph_depth config 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):

Timeline tree — a custom time-aware layout:

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 %}

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.

The default depth comes from the graph_depth config option (default 2, valid 15, 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:

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.

OptionTypeDefaultDescription
relationship_trackingbooltrueEnable typed relationship tracking
relationship_typesarraygenealogy defaultsRelation types and their semantics/labels
graph_depthnumber2Default neighbourhood depth (15) 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.