mdx as interface
rule
treat an MDX file as a human-and-machine interface, not merely serialized data: typed frontmatter, semantic component blocks, tables, code, links, and prose in one canonical artifact. machines parse its AST into content JSON blocks; humans keep the context that explains the structured blocks. this beats inventing a custom data structure just to display a capability — the document stays readable, reviewable, versionable, and its blocks stay extractable.
the cost of treating MDX as mere storage: a parallel JSON schema splits the source of truth. prose updates stop propagating to the machine boundary without a manual JSON rewrite; within weeks prose and JSON disagree — machines follow stale schema, humans follow newer prose, bugs are silent. a debugging agent reads the spec while running code follows a forgotten JSON rule; a tool regenerating the JSON wipes prose-only nuance added later.
one source; two useful views — rendered MDX is the human interface; parsed frontmatter and semantic blocks are the machine interface. neither is a lossy afterthought of the other.
why
storing a fact twice (prose + structure) splits the truth: machines read one copy, humans the other, and they drift the first week. one MDX artifact keeps explanation, typed frontmatter, and semantic blocks together, so every update propagates to both interfaces at once. risk avoided: silent divergence — code enforcing a stale rule the documentation no longer states.
content layers
| layer | human value | machine value |
|---|---|---|
| YAML frontmatter | identity, lifecycle, ownership, and discovery. | typed queryable metadata. |
| headings and prose | intent, rationale, constraints, and tradeoffs. | ordered semantic sections with source positions. |
| Fumadocs components | visual hierarchy for warnings, choices, and detailed evidence. | typed block kinds and props. |
| tables and lists | compact comparison and workflow explanation. | rows, cells, and ordered items. |
| code blocks | exact copyable examples. | language-tagged implementation evidence. |
extraction contract
an extractor parses MDX with an MDX-aware parser, validates frontmatter, and emits typed content blocks carrying the document path and source location. it does not scrape rendered HTML, flatten paragraphs into arbitrary JSON, or derive identity by rewriting headings.
type ContentBlock =| { kind: "heading"; depth: number; text: string; source: SourceLocation }| { kind: "callout"; type: string; title?: string; source: SourceLocation }| {kind: "table";columns: string[];rows: string[][];source: SourceLocation;}| { kind: "code"; language?: string; code: string; source: SourceLocation };
the emitted content JSON is a derived view; the MDX stays canonical unless a separate approved data authority owns the fact. LLM output follows the same boundary: it returns MDX, not a private JSON shape another renderer must reinterpret; a parser and the component-bank allowlist validate it before it becomes content or UI.
when mdx is the right interface
- a slice needs visible design plus machine-auditable implementation evidence.
- a handbook, runbook, spec, architecture decision, or knowledge page combines structured fields with essential explanatory context.
- a UI needs content blocks that retain author intent and component semantics.
- an agent needs a rule, its exact examples, and its source location in one parseable file.
when a database or schema remains right
use a typed API, schema, and durable store for transactional facts, high-volume records, authorization decisions, concurrent mutations, numeric aggregation, and query-heavy product state. MDX describes and presents those facts; it does not replace their source of truth. do not embed raw JSON inside MDX to imitate a database: a fact needing a data model gets one; a fact needing prose plus structured presentation makes the MDX interface canonical.
interface design checklist
give the file a shortest exact title and frontmatter identity. model visible component blocks by semantic purpose. define the extractor's supported block kinds, validation schema, and stable source-location fields before consumers depend on them.change and verification
update the MDX interface before a consumer changes. parse it, validate its frontmatter and supported components, inspect the derived content JSON, and verify both the rendered human view and the consuming machine boundary.good and bad
bad
mdx source (rules/audit-checklist.mdx):
---kind: checklisttitle: Code Audit Checklist---# Security Review- [ ] all user inputs validated with Zod- [ ] no hardcoded secrets in source- [ ] authentication boundary defined
parallel json schema (schemas/checklists.json):
{"title": "Code Audit Checklist","items": [{ "text": "All user inputs validated", "required": true },{ "text": "No hardcoded keys", "required": true }]}
six months later an agent adds a requirement to the MDX:
- [ ] authorization checks enforced at api boundary
but the JSON schema is regenerated from a stale template: the new item never appears in the JSON. machines enforce the old two-item checklist; humans assume the new requirement is enforced. a changed user role bypasses the authorization check documented in prose but never registered in the schema the code follows — production incident.
good
mdx source (rules/audit-checklist.mdx):
---kind: checklisttitle: Code Audit Checklistitems:- text: all user inputs validated with Zodrequired: trueenforced_at: validation.ts#validateUserInput- text: no hardcoded secrets in sourcerequired: trueenforced_at: build.ts#secretsAudit- text: authorization checks enforced at api boundaryrequired: trueenforced_at: auth.ts#checkPermission---# Security Review...detailed prose explaining each requirement...
one MDX parser extracts the items array from frontmatter for machines and renders the prose for humans. when the requirement changes, both views reflect the same canonical source; code reading the derived JSON cannot diverge from documented intent.
do not
- design a bespoke data schema only because a page has a table or a list.
- parse MDX by regex or infer block meaning from decorative class names.
- treat rendered HTML as the canonical machine representation.
- hide transactional product state in prose where it cannot be validated or safely mutated.
- let a derived JSON export become writable without an explicit authority decision.