HomeCurriculumTechnical WritingDocs-as-Code Workflows & Static Site Generators
Docs-as-Code Workflows & Static Site Generators

Docs-as-Code Workflows & Static Site Generators

Implement enterprise Docs-as-Code CI/CD pipelines, configure Vale prose linters, validate Markdown link/anchor integrity, and execute continuous code snippet tests (DocTests).

DT
Authored by DevThesis Systems Editorial Team
Reviewed for Technical Accuracy & Code Determinism

Executive Architectural Overview#

For decades, technical documentation was trapped in proprietary WYSIWYG platforms, centralized corporate wikis, and binary desktop word processors (Word, Confluence, FrameMaker). These legacy knowledge repositories suffered from fatal operational flaws:

  1. Isolated from Code Lifecycles: Documentation lived in an external tool disconnected from the code repositories where engineers worked. When code changed, documentation updates were forgotten.
  2. Zero Automated Quality Assurance: Wiki pages accumulated broken hyperlinks, syntax errors, and outdated API examples with zero automated validation.
  3. No Branching or Coordinated Releases: Updating documentation for an unreleased feature was impossible without leaking confidential roadmaps or corrupting production guides.

The modern software industry solved this through the Docs-as-Code (Docs-like-Code) movement. Docs-as-Code is the philosophy and practice of treating technical documentation with the exact same rigor, workflows, version control systems, automated linters, and CI/CD pipelines as production software code.

In a mature Docs-as-Code ecosystem, technical documentation is written in plain-text lightweight markup (Markdown, MDX, AsciiDoc), tracked in Git version control alongside code, peer-reviewed via Pull Requests, linted for style and broken links by automated CI bots, and compiled into ultra-fast static web portals using Static Site Generators (SSGs) deployed to global Content Delivery Networks (CDNs).

text
       LEGACY CENTRAL WIKI vs. MODERN DOCS-AS-CODE PIPELINE
  ┌────────────────────────────────────────────────────────┐
  │ LEGACY WIKI SYSTEM (Confluence / Google Docs)          │
  │ • Proprietary WYSIWYG editor; unversioned drift        │
  │ • No pull requests, no automated link validation       │
  │ • Stale pages accumulate with zero automated governance│
  └──────────────────────────┬─────────────────────────────┘
                             │ Architectural Shift
                             ▼
  ┌────────────────────────────────────────────────────────┐
  │ MODERN DOCS-AS-CODE PIPELINE                           │
  │ 1. Author in Markdown / MDX ──► Git Feature Branch     │
  │ 2. Pull Request Opened ─────► Ephemeral Preview Deploy │
  │ 3. Automated CI Gates:                                 │
  │    • Vale Prose Linting (Enforces corporate style)     │
  │    • Lychee Dead Link & Fragment Checker               │
  │    • Markdownlint Formatting Validation                │
  │ 4. Git Merge to Main ───────► SSG Build (Docusaurus)   │
  │ 5. Instant CDN Edge Deployment (Cloudflare / Vercel)   │
  └────────────────────────────────────────────────────────┘

1. The Docs-as-Code Core Foundations & Philosophy#

Docs-as-Code operates on five non-negotiable architectural tenets:

  1. Plain-Text Lightweight Markup:

Documents are authored in text formats (predominantly Markdown / CommonMark, MDX, or AsciiDoc). Plain text allows engineers to write documentation directly inside their code editors (VS Code, Neovim, IntelliJ) without context switching.

  1. Version Control Management (Git):

Documentation resides in Git repositories. Every change is tracked in an immutable commit history with author attribution, commit messages, and git blame inspection.

  1. Branching & Synchronized Releases:

When an engineer develops a new feature on a feature branch (feat/oauth2-pkce), the documentation for that feature is authored on the exact same Git branch. When the pull request merges to main, the code and documentation deploy to production simultaneously.

  1. Peer Review Culture (Pull Requests):

Documentation changes undergo formal code review. Technical writers and peer engineers review diffs, test code snippets, and approve changes before merging.

  1. Continuous Integration & Automated Testing (CI/CD):

Every pull request triggers automated linters that test prose quality, terminology compliance, and link integrity before human reviewers ever read the diff.


2. Static Site Generator (SSG) Landscape & Evaluation#

A Static Site Generator (SSG) compiles raw Markdown files and templates into static, pre-rendered HTML, CSS, and client-side JavaScript assets during a build step.

text
+─────────────────────+──────────────────+─────────────────────────────────────────────+
| Static Generator    | Core Engine      | Primary Architectural Fit                   |
+─────────────────────+──────────────────+─────────────────────────────────────────────+
| 1. Docusaurus       | React / Node.js  | Rich React ecosystems; interactive MDX      |
|                     |                  | components, native multi-versioning, i18n.  |
+─────────────────────+──────────────────+─────────────────────────────────────────────+
| 2. MkDocs Material  | Python / Jinja2  | Python ecosystems; blazingly fast build;    |
|                     |                  | clean search; YAML plugin extensions.       |
+─────────────────────+──────────────────+─────────────────────────────────────────────+
| 3. Hugo             | Go (Golang)      | Massive enterprise documentation sites      |
|                     |                  | (100,000+ pages); sub-second compilation.   |
+─────────────────────+──────────────────+─────────────────────────────────────────────+
| 4. Starlight        | Astro            | Zero-JS by default; exceptional Core Web    |
|                     |                  | Vitals; multi-framework component islanding.|
+─────────────────────+──────────────────+─────────────────────────────────────────────+

The AST Compilation Pipeline (Unified / Remark / Rehype)#

Modern JavaScript-based static site generators (Docusaurus, Starlight, Nextra) process Markdown through an Abstract Syntax Tree (AST) pipeline:

text
  Source Markdown (.md / .mdx)
               │
               ▼
   [ unified / remark-parse ] ──► Markdown Abstract Syntax Tree (mdast)
               │                  (AST transformation, custom shortcodes)
               ▼
   [ remark-rehype ] ──────────► HTML Abstract Syntax Tree (hast)
               │                  (Prism/Shiki syntax highlighting, anchor IDs)
               ▼
   [ rehype-stringify ] ───────► Production HTML5 Output

3. Automated Quality Gates: Vale, Lychee & Markdownlint#

A continuous integration pipeline for documentation acts as a compiler for prose. Automated quality gates catch errors before documentation reaches production.

text
                  THE DOCS CI/CD QUALITY MATRIX
  ┌────────────────────────────────────────────────────────┐
  │ 1. Markdownlint: AST Structure & Syntax Validation     │
  │    • Validates heading levels (# ──► ## ──► ###)       │
  │    • Eliminates trailing whitespace & missing newlines │
  ├────────────────────────────────────────────────────────┤
  │ 2. Vale: Prose Linter & Style Governance               │
  │    • Enforces active voice, conciseness, terminology   │
  │    • Enforces Google / Microsoft Style Guides          │
  ├────────────────────────────────────────────────────────┤
  │ 3. Lychee / Link Checker: Hyperlink Integrity          │
  │    • Validates all external URLs (HTTP 200 checks)     │
  │    • Validates internal relative links & anchor tags   │
  ├────────────────────────────────────────────────────────┤
  │ 4. cspell: Automated Technical Spell Checking          │
  │    • Validates spelling against custom tech dictionary │
  └────────────────────────────────────────────────────────┘

Vale: The Linter for Prose#

Vale is an open-source, highly configurable prose linter designed for technical documentation. Unlike generic spellcheckers, Vale evaluates text against semantic rules written in YAML:

  • Capitalization Rules: Enforces proper branding (e.g., flagging "Postgres" or "postgreSQL" and requiring "PostgreSQL").
  • Voice and Tone: Flags passive voice constructions (e.g., "The file is created by the system" -> "The system creates the file").
  • Prohibited Words: Warns against non-inclusive terms (master/slave -> primary/replica, blacklist -> denylist) or ambiguous temporal terms ("recently", "currently").

4. Multi-Version Documentation & Internationalization (i18n)#

Enterprise software releases multiple major versions that must be supported simultaneously (e.g., Version 2.0, Version 3.0, and Next/Beta).

Multi-Version Directory Topology#

Static site generators like Docusaurus implement multi-versioning through snapshotting:

text
  my-docs-repo/
  ├── docs/                          ◄── Next / Unreleased (Cutting Edge)
  │   └── getting-started.md
  ├── versioned_docs/
  │   ├── version-3.0/               ◄── Current Stable Release (/docs/3.0/...)
  │   │   └── getting-started.md
  │   └── version-2.4/               ◄── Long-Term Support LTS (/docs/2.4/...)
  │       └── getting-started.md
  ├── versions.json                  ◄── ["3.0", "2.4"]
  └── docusaurus.config.js

When version 3.1 is tagged, the CLI executes:

bash
npx docusaurus docs:version 3.1

The generator freezes the active docs/ folder into an immutable snapshot under versioned_docs/version-3.1/, generating version dropdown selectors in the portal navigation bar automatically.


5. Enterprise Checkpoint Practice Problems#

Test your mastery with these four real-world production checkpoint problems. Each problem presents a concrete business schema, data samples, edge cases, a failed attempt analysis, and a fully explained optimal solution.


Problem 1: End-to-End Enterprise GitHub Actions Docs-as-Code CI/CD Pipeline#

Context: You are architecting a CI/CD documentation pipeline for an enterprise repository using GitHub Actions and Docusaurus.

  • Every Pull Request touching /docs/** must:

1. Lint Markdown syntax with markdownlint-cli2.

2. Run the Vale prose linter with custom corporate style rules.

3. Validate all internal and external hyperlinks with lychee.

4. Build the Docusaurus static site to catch broken React/MDX imports.

5. If on main, deploy the compiled static assets to Cloudflare Pages.

Optimal Production GitHub Actions Workflow (`.github/workflows/docs-pipeline.yml`):

yaml
name: "Documentation CI/CD Pipeline"

on:
  push:
    branches: ["main"]
    paths: ["docs/**", "src/**", "docusaurus.config.js", "package.json"]
  pull_request:
    branches: ["main"]
    paths: ["docs/**", "src/**", "docusaurus.config.js", "package.json"]

concurrency:
  group: {{ github.workflow }}-{{ github.ref }}
  cancel-in-progress: true

jobs:
  lint-and-validate:
    name: "Lint Prose, Syntax & Hyperlinks"
    runs-on: ubuntu-latest
    steps:
      - name: "Checkout Source Code"
        uses: actions/checkout@v4

      - name: "Setup Node.js"
        uses: actions/setup-node@v4
        with:
          node-version: "20"
          cache: "npm"

      - name: "Install NPM Dependencies"
        run: npm ci

      - name: "Run Markdownlint"
        run: npx markdownlint-cli2 "docs/**/*.md"

      - name: "Run Vale Prose Linter"
        uses: errata-ai/vale-action@v2
        with:
          files: 'docs'
        env:
          GITHUB_TOKEN: {{ secrets.GITHUB_TOKEN }}

      - name: "Verify Hyperlink Integrity with Lychee"
        uses: lycheeverse/lychee-action@v1
        with:
          args: "--verbose --no-progress './docs/**/*.md' --exclude-all-private --max-concurrency 8"
          fail: true
        env:
          GITHUB_TOKEN: {{ secrets.GITHUB_TOKEN }}

      - name: "Compile Docusaurus Production Build"
        run: npm run build

  deploy-production:
    name: "Deploy to Cloudflare Pages"
    needs: lint-and-validate
    if: github.ref == 'refs/heads/main' && github.event_name == 'push'
    runs-on: ubuntu-latest
    steps:
      - name: "Checkout Source Code"
        uses: actions/checkout@v4

      - name: "Setup Node.js"
        uses: actions/setup-node@v4
        with:
          node-version: "20"
          cache: "npm"

      - name: "Install & Build"
        run: |
          npm ci
          npm run build

      - name: "Publish Assets to Cloudflare Pages"
        uses: cloudflare/pages-action@v1
        with:
          apiToken: {{ secrets.CLOUDFLARE_API_TOKEN }}
          accountId: {{ secrets.CLOUDFLARE_ACCOUNT_ID }}
          projectName: "enterprise-docs"
          directory: "build"
          gitHubToken: {{ secrets.GITHUB_TOKEN }}

Architectural Walkthrough:

  1. concurrency.cancel-in-progress: true: Aborts in-flight CI runs if new commits are pushed, saving compute minutes.
  2. Vale runs as an automated GitHub bot, annotating pull request diffs directly with inline review comments.
  3. npm run build verifies that all MDX components, React dependencies, and routing paths compile without syntax errors prior to deployment.

Problem 2: Authoring Custom Enterprise Vale Prose Linter Rules#

Context: Your organization's technical writing team is enforcing three strict rules across all repositories:

  1. Rule 1 (Terminology): Prohibit the term "whitelist" and "blacklist", enforcing "allowlist" and "denylist".
  2. Rule 2 (Brand Capitalization): Enforce strict capitalization for tech terms: "JavaScript" (not "Javascript"), "Kubernetes" (not "kubernetes"), "PostgreSQL" (not "Postgres").
  3. Rule 3 (Conciseness / Filler Words): Flag wordy filler expressions like "in order to" (suggesting "to") and "at this point in time" (suggesting "now").
  4. Author the complete .vale.ini configuration and the custom YAML rules.

Optimal Production `.vale.ini` Configuration:

ini
StylesPath = .github/styles
MinAlertLevel = suggestion

[*.md]
BasedOnStyles = Vale, Corporate

# Enable custom corporate package rules
Corporate.InclusiveLanguage = YES
Corporate.BrandCapitalization = YES
Corporate.Conciseness = YES

Rule 1: `.github/styles/Corporate/InclusiveLanguage.yml`:

yaml
extends: substitution
message: "Use '%s' instead of non-inclusive term '%s'."
level: error
ignorecase: true
swap:
  whitelist: allowlist
  whitelisted: allowlisted
  blacklist: denylist
  blacklisted: denylisted
  master: primary
  slave: replica

Rule 2: `.github/styles/Corporate/BrandCapitalization.yml`:

yaml
extends: substitution
message: "Incorrect brand capitalization. Use '%s' instead of '%s'."
level: error
ignorecase: false
swap:
  Javascript: JavaScript
  javascript: JavaScript
  kubernetes: Kubernetes
  Postgres: PostgreSQL
  postgres: PostgreSQL
  postgresql: PostgreSQL
  Github: GitHub

Rule 3: `.github/styles/Corporate/Conciseness.yml`:

yaml
extends: substitution
message: "Consider using concise alternative '%s' instead of '%s'."
level: warning
ignorecase: true
swap:
  in order to: to
  at this point in time: now
  due to the fact that: because
  for the purpose of: for

Architectural Walkthrough:

  1. Automated enforcement eliminates human review friction during pull requests.
  2. ignorecase: false in BrandCapitalization catches subtle case discrepancies (Postgres -> PostgreSQL).
  3. level: error blocks CI merges on inclusive language violations, while level: warning suggests conciseness improvements.

Problem 3: Configuring Multi-Version Software Documentation in Docusaurus#

Context: You are configuring docusaurus.config.js for an enterprise database product.

  1. The portal must support the current stable version v3.0 and the previous LTS version v2.4.
  2. The unreleased development version in docs/ must be labeled as Canary / Next.
  3. Configure the navigation bar with an interactive Version Dropdown selector.
  4. Add an automated banner to older versions warning users: "This is documentation for an older version. Click here to view the latest stable v3.0 release."

Optimal Production `docusaurus.config.js` Configuration:

javascript
// @ts-check
/** @type {import('@docusaurus/types').Config} */
const config = {
  title: 'Enterprise Distributed DB',
  tagline: 'Ultra-low latency globally distributed storage',
  url: 'https://docs.enterprisedb.com',
  baseUrl: '/',
  favicon: 'img/favicon.ico',

  presets: [
    [
      'classic',
      /** @type {import('@docusaurus/preset-classic').Options} */
      ({
        docs: {
          sidebarPath: require.resolve('./sidebars.js'),
          editUrl: 'https://github.com/enterprise/docs/tree/main/',
          // Multi-Version Configuration
          lastVersion: '3.0',
          versions: {
            current: {
              label: 'Next (Canary)',
              path: 'next',
              banner: 'unreleased',
            },
            '3.0': {
              label: '3.0 (Current Stable)',
              path: '3.0',
              banner: 'none',
            },
            '2.4': {
              label: '2.4 (LTS)',
              path: '2.4',
              banner: 'unmaintained',
            },
          },
        },
        theme: {
          customCss: require.resolve('./src/css/custom.css'),
        },
      }),
    ],
  ],

  themeConfig:
    /** @type {import('@docusaurus/preset-classic').ThemeConfig} */
    ({
      navbar: {
        title: 'EnterpriseDB Docs',
        logo: {
          alt: 'EnterpriseDB Logo',
          src: 'img/logo.svg',
        },
        items: [
          {
            type: 'doc',
            docId: 'intro',
            position: 'left',
            label: 'Documentation',
          },
          // Interactive Version Dropdown Selector
          {
            type: 'docsVersionDropdown',
            position: 'right',
            dropdownActiveClassDisabled: true,
          },
          {
            href: 'https://github.com/enterprise/docs',
            label: 'GitHub',
            position: 'right',
          },
        ],
      },
    }),
};

module.exports = config;

Architectural Walkthrough:

  1. lastVersion: '3.0' designates version 3.0 as the primary canonical release for search engine indexing (SEO).
  2. banner: 'unmaintained' automatically renders a top alert banner on version 2.4 pages warning readers that they are viewing an LTS release.
  3. docsVersionDropdown renders an interactive navigation selector syncing readers across versions.

Context: In a documentation site with 5,000 Markdown pages, developers frequently rename headings (e.g., ## Quick Start -> ## Getting Started).

  • When a heading changes, its HTML anchor changes (#quick-start -> #getting-started).
  • Any external blog post or internal link pointing to /docs/guide#quick-start still resolves HTTP 200 to the page, but the browser fails to scroll to the target section (a Broken Fragment error).
  • Configure lychee in a .lychee.toml configuration to catch dead external URLs, broken relative paths, and missing heading anchor fragments.

Optimal Production `.lychee.toml` Configuration:

toml
# Lychee Link Checker Configuration

# Verbose output mode
verbose = true

# Check both HTTP links and fragment anchors (#heading-slug)
check_anchors = true

# Maximum number of concurrent network requests
max_concurrency = 16

# Timeout per request in seconds
timeout = 10

# Retry failed requests up to 3 times with exponential backoff
max_retries = 3

# HTTP status codes accepted as successful
accept = [200, 204, 206, 429]

# Exclude local private IP ranges and staging domains
exclude = [
    '^https?://10\\..*',
    '^https?://192\\.168\\..*',
    '^https?://localhost.*',
    '^https?://127\\.0\\.0\\.1.*',
    'https://twitter.com/.*', # Rate-limits headless crawlers
    'https://www.linkedin.com/.*'
]

# Map root-relative URLs (/docs/...) to the local repository directory
remap = [
    "https://docs.company.com/ /build/",
    "^/ /build/"
]

Architectural Walkthrough:

  1. check_anchors = true: Instructs Lychee to parse the compiled HTML document, extract all <a name="..."> and <h2 id="..."> anchor targets, and assert that fragment URLs (#heading) actually exist.
  2. remap: Maps root-relative links (/docs/install) to the local static build directory, testing internal navigation offline before deployment.
  3. Prevents silent link rot across thousands of enterprise technical documents.

Conclusion & Architectural Summary#

  1. Docs-as-Code Tenets: Treat technical documentation like software code: plain-text markup in Git, peer review via Pull Requests, and automated CI/CD deployment.
  2. Static Site Generators: Select the right SSG for your ecosystem: Docusaurus for React/MDX flexibility, MkDocs for Pythonic speed, Hugo for massive scale, and Starlight for Astro performance.
  3. Automated Governance: Enforce style rules with Vale, validate link and anchor integrity with Lychee, and verify structural formatting with Markdownlint.
  4. Version Control Discipline: Freeze documentation versions alongside software releases using snapshotting; communicate status via automated unreleased/unmaintained banners.

Key Architectural Takeaways

  • The Docs-as-Code philosophy manages documentation in Git repositories with pull requests, CI/CD, and automated testing.
  • Static Site Generators parse Markdown frontmatter and ASTs into static HTML pre-rendered pages with SPA hydration.
  • Vale automates editorial style guides, flagging passive voice, prohibited terminology, and casing violations in CI.
  • Automated DocTests execute code blocks within documentation to detect breaking changes across SDK releases.

Hands-on Code Playground Available

Execute, edit, and experiment with code for this topic in the live sandbox.

Practice Check • Quick Quiz

Test Your Understanding

What is the primary role of a prose linter like Vale in a production Docs-as-Code CI/CD pipeline?

A
To convert Markdown files into Docker containers
B
To programmatically enforce editorial style guides, voice, passive-voice limits, and approved terminology in CI
C
To deploy static HTML files to AWS S3 buckets
D
To generate synthetic user traffic to the documentation portal