Docs-as-Code Patterns¶
FCC's docs pipeline is worth studying. It generates hundreds of pages from a mix of hand-authored Markdown, structured YAML/JSON data, and Jinja2 templates. The result is consistent, cross-linked, and cheap to update. This page walks through the adaptable parts and gives you a minimum-viable starter pipeline.
The pattern at a glance¶
FCC's docs pipeline has three kinds of inputs:
- Hand-authored Markdown under
docs/for narrative content. - Structured YAML/JSON data files for personas, scenarios, workflows, compliance requirements.
- Jinja2 templates under
src/fcc/templates/docs/that render the structured data into Markdown.
The templates emit 173 model cards, 20 category overviews, evolution guides, and cross-reference matrices — all auto-generated and regenerable.
The insight: treat docs like code, but divide them into narrative and generated. Narrative docs are edited by humans. Generated docs are edited only by regeneration. Never blur the line.
Why this matters for OSS¶
Three pains it solves:
- Docs drift. A module's API changes, and the docs reference stale method names for months. If the docs are generated from the code, they drift less.
- Cross-reference rot. Someone renames a module; fifty doc pages still say the old name. Auto-generation fixes them in one pass.
- New-contributor onboarding. Contributors struggle to keep docs in sync with code. When docs are generated, contributors touch only templates and sources, not every doc page.
Minimum-viable pipeline¶
A starter pipeline has four pieces:
- One Markdown convention. Pick a static-site generator (MkDocs, Docusaurus, Hugo, Zola). Commit to it.
- One data format. YAML for most things. Use JSONSchema to validate.
- One template engine. Jinja2 if you are in Python-adjacent territory; otherwise whichever matches your language.
- One make target.
make docsregenerates everything.make docs-checkverifies no drift from the last regeneration.
A worked example¶
Suppose your OSS is a library with 15 modules. You want one page per module documenting its public API, its role, and its collaborators.
Step 1 — Data. For each module, src/mymodule/ROLE.yaml:
id: mymodule
role: "Handles validation of user input."
constraints:
- Never mutate input arguments.
responsibilities:
- Validate against schema.
- Raise descriptive errors.
role_collaborators:
- core
- errors
public_api:
- "validate(obj, schema) -> ValidationResult"
- "ValidationResult.is_valid -> bool"
Step 2 — Template at templates/module-doc.md.j2:
# Module: {{ role.id }}
{{ role.role }}
## Public API
{% for fn in role.public_api %}
- `{{ fn }}`
{% endfor %}
## Constraints
{% for c in role.constraints %}
- {{ c }}
{% endfor %}
## Collaborators
{% for col in role.role_collaborators %}
- [{{ col }}](./{{ col }}.md)
{% endfor %}
Step 3 — Generator (30-line Python):
import yaml, jinja2, pathlib
tpl = jinja2.Template(pathlib.Path("templates/module-doc.md.j2").read_text())
for role_file in pathlib.Path("src").glob("*/ROLE.yaml"):
role = yaml.safe_load(role_file.read_text())
out = pathlib.Path(f"docs/modules/{role['id']}.md")
out.parent.mkdir(exist_ok=True, parents=True)
out.write_text(tpl.render(role=role))
Step 4 — Make target:
docs:
python scripts/gen_docs.py
mkdocs build
docs-check:
python scripts/gen_docs.py
git diff --exit-code docs/modules/
The pipeline is complete. From this 30-line foundation you can grow indefinitely.
Cross-reference matrices¶
FCC ships a cross_reference.yaml that records persona-to-persona upstream/downstream/peer relationships. The same idea applied to modules yields a matrix that answers questions like "which modules depend on X?" and "which tests exercise the boundary between X and Y?"
Implementation sketch:
Generate a matrix page that renders this for the whole project. New contributors love it.
Separating narrative from generated¶
Two directories:
docs/
narrative/ # hand-authored, never regenerated
getting-started.md
design-philosophy.md
generated/ # auto-regenerated, never edited by hand
modules/
api/
cross-reference/
Put a comment at the top of each generated file:
Enforce in PR review: "if your PR edits a file with AUTO-GENERATED, it will be rejected."
Mermaid diagrams as docs-as-code¶
FCC ships ~280 Mermaid diagrams, rendered to SVG+PNG by a Lua pandoc filter. Mermaid source is cheap to write, diffable in PRs, and renders in most site generators directly.
Convention:
- Diagram source lives alongside the narrative page as a fenced
mermaidblock. - CI validates that each block parses.
- For publication-quality PDFs, a render step produces PNGs.
Even a small OSS benefits from Mermaid: architecture overviews, state machines, sequence diagrams. Two dozen well-placed diagrams can replace a hundred paragraphs of prose.
Quarto and multi-format publishing (advanced)¶
FCC uses Quarto + Pandoc to publish books, articles, and slides from the same Markdown sources. If your project eventually wants a PDF book, a web book, and a paper, Quarto repays its learning cost.
Start simple: one _quarto.yml, one chapter list, one quarto render command. FCC's publications/ directory has a worked example.
CI integration¶
Three checks worth running on every PR:
- Docs build. If the site doesn't build, fail.
- Link check. If internal links are broken, fail.
- Generated-drift check. If templates produce different output than what's committed, fail.
All three run in under a minute for a modest project.
Anti-patterns¶
- Mixed generation and hand-editing. The fastest way to lose trust in the pipeline.
- Over-generalizing templates. Start with one template per artifact type; split only when genuinely needed.
- Skipping validation. JSONSchema validates your YAML before the generator runs. Saves time.
- Bespoke build scripts per contributor. Keep the one-make-target discipline.
Where next¶
With docs discipline in hand, see how FCC orchestrates contributor workflows in Contributor Ceremony, or learn the release rhythms in Release Cadence Lessons.