The Docs-as-Code Principle#
Documentation as code means treating documentation the same way you treat source code: stored in version control, reviewed in pull requests, tested in CI, and deployed automatically. The alternative – documentation in a wiki, a Google Doc, or someone’s head – drifts out of sync with the codebase within weeks.
The core workflow: docs live alongside the code they describe (usually in a docs/ directory or inline), changes go through the same PR process, CI builds and validates the docs, and a pipeline deploys them automatically on merge.
Static Site Generator Comparison#
Four tools dominate the docs-as-code space. Each has a different sweet spot.
MkDocs (with Material theme)#
MkDocs is a Python-based static site generator purpose-built for project documentation. The Material for MkDocs theme adds search, versioning, admonitions, and code annotations out of the box.
# mkdocs.yml
site_name: My Project Docs
theme:
name: material
features:
- navigation.tabs
- navigation.sections
- search.suggest
- content.code.copy
plugins:
- search
- mkdocstrings:
handlers:
python:
paths: [src]
markdown_extensions:
- admonition
- pymdownx.highlight
- pymdownx.superfences
nav:
- Home: index.md
- Getting Started: getting-started.md
- API Reference: api/Strengths: Minimal configuration to get a professional result. The Material theme is the best documentation theme available for any SSG. mkdocstrings generates API reference from Python docstrings. Built-in versioning with mike. Strong ecosystem of plugins.
Weaknesses: Python-only for auto-generated API docs (mkdocstrings). Markdown only – no reStructuredText. The Material theme’s advanced features (insiders edition) require a paid sponsorship.
Best for: Python projects, internal platform documentation, any team that wants good docs fast without extensive customization.
Docusaurus#
Docusaurus is a React-based documentation framework from Meta. It produces a full website with documentation, a blog, and custom pages.
// docusaurus.config.js
module.exports = {
title: 'My Project',
url: 'https://docs.myproject.com',
baseUrl: '/',
presets: [
['@docusaurus/preset-classic', {
docs: {
sidebarPath: require.resolve('./sidebars.js'),
editUrl: 'https://github.com/org/project/edit/main/',
},
blog: { showReadingTime: true },
}],
],
plugins: [
['docusaurus-plugin-openapi-docs', {
id: 'api',
docsPluginId: 'classic',
config: {
petstore: {
specPath: 'openapi.yaml',
outputDir: 'docs/api',
},
},
}],
],
};Strengths: Full website capabilities beyond documentation (blog, landing pages). MDX support means you can embed React components in docs. Strong versioning. Good search with Algolia integration. OpenAPI plugin for interactive API docs.
Weaknesses: Heavier build toolchain (Node.js, React, webpack). Slower builds than MkDocs or Hugo. Requires JavaScript knowledge for customization. Overkill for internal docs that just need to convey information.
Best for: Open source projects that need a public-facing docs site with a blog and landing page. Projects with a JavaScript/TypeScript team comfortable with React.
Hugo#
Hugo is a Go-based static site generator known for build speed. It is not documentation-specific but has documentation themes (Docsy, Book).
# hugo.yaml
baseURL: https://docs.myproject.com
title: My Project Docs
theme: book
params:
BookSection: docs
BookToC: true
BookSearch: true
markup:
goldmark:
renderer:
unsafe: true
highlight:
style: githubStrengths: Fastest builds of any SSG (milliseconds for hundreds of pages). Single binary, no runtime dependencies. Excellent for large documentation sites. Flexible content organization with sections, taxonomies, and custom output formats (HTML, JSON, RSS simultaneously).
Weaknesses: Go template syntax has a steep learning curve. Fewer documentation-specific features out of the box compared to MkDocs Material. Themes vary widely in quality. No built-in API doc generation.
Best for: Large documentation sites where build speed matters. Teams already using Go. Sites that need custom output formats (like JSON for API consumption alongside HTML for humans).
Sphinx#
Sphinx is the documentation standard in the Python and C/C++ world. It uses reStructuredText by default but supports Markdown via MyST.
# conf.py
project = 'My Project'
extensions = [
'sphinx.ext.autodoc',
'sphinx.ext.napoleon',
'sphinx.ext.intersphinx',
'myst_parser',
'sphinxcontrib.openapi',
]
intersphinx_mapping = {
'python': ('https://docs.python.org/3', None),
}
html_theme = 'furo'Strengths: Best-in-class cross-referencing between documents and API elements. autodoc extracts documentation from Python docstrings with full type information. intersphinx links to other Sphinx-documented projects. reStructuredText is more powerful than Markdown for complex documentation (directives, roles, domain-specific markup).
Weaknesses: reStructuredText is harder to learn than Markdown. Configuration is a Python file, which is flexible but complex. Slower builds than Hugo. Default themes look dated (use Furo or PyData theme).
Best for: Python libraries that need comprehensive API documentation with cross-references. Scientific or academic projects. Any project where intersphinx linking to other documented libraries is valuable.
Decision Matrix#
| Criterion | MkDocs Material | Docusaurus | Hugo | Sphinx |
|---|---|---|---|---|
| Setup time | 10 minutes | 30 minutes | 20 minutes | 30 minutes |
| Build speed | Fast | Slow | Fastest | Moderate |
| Auto API docs (Python) | Good | None | None | Best |
| Auto API docs (OpenAPI) | Plugin | Plugin | None | Plugin |
| Versioning | mike plugin | Built-in | Manual | readthedocs |
| Custom pages | Limited | Full (React) | Full (templates) | Limited |
| Learning curve | Low | Medium | High (templates) | High (reST) |
| Best audience | Internal teams | OSS projects | Large sites | Python libraries |
Doc Testing in CI#
Documentation rots. The only way to prevent it is to test it.
Link checking: Dead links are the most common documentation failure. Run a link checker in CI:
# GitHub Actions
- name: Check links
uses: lycheeverse/lychee-action@v1
with:
args: --verbose --no-progress './docs/**/*.md'
fail: trueCode example testing: Code blocks in documentation should be executable. For Python, doctest and pytest --doctest-modules verify inline examples. For other languages, extract code blocks and compile/run them:
# Extract fenced code blocks and test them
mdsh --frozen docs/getting-started.mdBuild validation: The docs site should build without warnings in CI. Treat warnings as errors:
mkdocs build --strict
# or
sphinx-build -W docs/ docs/_build/Prose linting: Enforce consistent terminology and style with Vale:
# .vale.ini
StylesPath = .vale/styles
MinAlertLevel = warning
[*.md]
BasedOnStyles = Vale, write-goodA CI step runs vale docs/ and fails on terminology violations, passive voice, or jargon.
API Documentation Generation#
API docs should be generated from the source of truth, not maintained separately.
OpenAPI/Swagger: For REST APIs, maintain an openapi.yaml spec and generate documentation from it:
# Generate HTML docs
npx @redocly/cli build-docs openapi.yaml -o docs/api/index.html
# Validate spec
npx @redocly/cli lint openapi.yamlEmbed the generated docs into your documentation site, or use an interactive viewer like Swagger UI or Redoc.
protoc-gen-doc: For gRPC APIs, generate documentation from .proto files:
protoc --doc_out=docs/api --doc_opt=markdown,api.md proto/*.protoLanguage-specific generators: mkdocstrings for Python, TypeDoc for TypeScript, Javadoc for Java, godoc for Go. Each extracts documentation from source code annotations and produces browsable reference docs.
The key principle: the spec or the source code is the single source of truth. Documentation is generated from it, not duplicated alongside it.
Architecture Decision Records (ADRs)#
ADRs document the “why” behind architectural decisions. They capture context that is invisible in code: what alternatives you considered, what constraints drove the decision, and what tradeoffs you accepted.
A minimal ADR format:
# ADR-0001: Use PostgreSQL for primary data store
## Status
Accepted
## Context
We need a primary data store for the user service.
Expected load is 500 requests/second with 80% reads.
Team has PostgreSQL operational experience.
## Decision
Use PostgreSQL 16 on AWS RDS.
## Alternatives Considered
- **DynamoDB**: Lower operational burden but limited query flexibility.
We expect ad-hoc queries during debugging and reporting.
- **CockroachDB**: Better horizontal scaling but higher complexity.
Current load does not justify the operational overhead.
## Consequences
- We accept the operational burden of managing RDS instances.
- We gain full SQL query capability for debugging and reporting.
- Horizontal scaling beyond a single primary will require read replicas
or a future migration to a distributed database.Store ADRs in docs/decisions/ or docs/adr/. Number them sequentially. Never delete or modify an accepted ADR – instead, create a new ADR that supersedes it.
Use adr-tools to manage the lifecycle:
adr new "Use PostgreSQL for primary data store"
adr new -s 1 "Migrate to CockroachDB for multi-region"
adr listThe -s 1 flag marks the new ADR as superseding ADR-0001.
Docs in CI: The Complete Pipeline#
A mature docs-as-code pipeline has four stages:
# .github/workflows/docs.yml
name: Documentation
on:
pull_request:
paths: ['docs/**', 'openapi.yaml', 'proto/**']
push:
branches: [main]
paths: ['docs/**', 'openapi.yaml', 'proto/**']
jobs:
validate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build docs (strict mode)
run: mkdocs build --strict
- name: Check links
uses: lycheeverse/lychee-action@v1
with:
args: './docs/**/*.md'
- name: Lint prose
run: vale docs/
- name: Validate OpenAPI spec
run: npx @redocly/cli lint openapi.yaml
generate-api-docs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Generate API reference
run: npx @redocly/cli build-docs openapi.yaml -o docs/api/index.html
deploy:
if: github.ref == 'refs/heads/main'
needs: [validate, generate-api-docs]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build and deploy
run: |
mkdocs build
# Deploy to your hosting (GitHub Pages, Cloudflare Pages, S3, etc.)The pipeline validates on every PR. On merge to main, it builds and deploys. Docs stay in sync with code because they go through the same review and CI process.