CoDNA Request access
Contents

Docs Decisions

0006 · Inline markdown links with relative .md hrefs

Status: Accepted · 2026-09-22

Context

Documents reference each other, and those references form the graph that drives share suggestions and backlinks. The href format determines whether an export is useful, what a rename costs, and whether a person can write a link without looking something up.

Decision

Ordinary inline markdown, with relative or root-relative .md paths:

See the [onboarding guide](../onboarding.md).
The [deploy runbook](/ops/deploy.md) covers rollback.

Edges are stored with both the href as written and the resolved target_id (linking.md). Renames never break the graph; only the written text goes stale, and staleness is surfaced as a decision rather than repaired automatically.

Consequences

  • An export is a working file tree. It opens in Obsidian, browses on GitHub, renders anywhere, with every link resolving. This is what makes "easily exportable" mean something.
  • Pasting in content from an existing docs repository keeps its links working.
  • Links are extracted from the goldmark AST, not a regex — a regex over prose cannot distinguish a link in a paragraph from one inside a fenced code block.
  • A rename leaves stale text in the bodies that point at it. Deliberate: auto-rewriting would manufacture a revision on every linking document, attributed to whoever did the rename.
  • A link to a document that does not exist yet is pending and resolves itself when one appears.

Alternatives rejected

[[wikilinks]]. Familiar, terse, and unambiguous about intent. Rejected because they render as literal text everywhere outside a wiki-aware editor — an export full of [[foo]] is not a working document tree.

codna://doc/<uuid> URIs. Never stale, nothing to reconcile. Rejected because raw markdown becomes unreadable, exports do not work in any external viewer, and writing a link by hand requires looking up a UUID first.

App-absolute paths (/wiki/engineering/onboarding). Reads well and works as a real URL in the UI. Rejected because an export is then not self-contained — every link points at a running instance, which is exactly what export exists to survive.