Problem: Documentation Becomes Outdated and Disconnected from Code
Your team writes docs once, then they drift out of sync with the codebase. Developers skip updating docs because the workflow is clunky, and users hit outdated information.
You'll learn:
- How to keep docs in version control with code
- Automated testing and validation for documentation
- CI/CD workflows that enforce doc quality
Time: 20 min | Level: Intermediate
Why This Happens
Traditional documentation lives in separate systems (Confluence, Google Docs) with different workflows than code. Developers don't update docs because:
Common symptoms:
- Code examples in docs throw errors
- API docs reference removed endpoints
- Installation steps fail on current versions
- No one reviews docs in PRs
Solution
Step 1: Co-locate Docs with Code
Move documentation into your repository alongside the code it describes.
# Recommended structure
my-project/
├── src/
├── docs/
│ ├── getting-started.md
│ ├── api/
│ └── guides/
├── examples/ # Working code that docs reference
└── .github/workflows/ # CI for docs validation
Why this works: Docs become part of the same PR review process as code changes. Version control tracks when docs were last updated.
If it fails:
- Team resists: Start with API docs only, expand gradually
- Docs too large: Use git submodules or separate repo with synchronized releases
Step 2: Extract Code Examples from Tests
Don't write code examples manually—extract them from your test suite.
// tests/examples/quickstart.test.ts
test('quickstart example from docs', async () => {
// DOCS_START: quickstart
import { createClient } from '@myapp/sdk';
const client = createClient({
apiKey: process.env.API_KEY
});
const result = await client.users.list();
// DOCS_END: quickstart
expect(result.length).toBeGreaterThan(0);
});
Then extract for docs:
# Extract code between markers
sed -n '/DOCS_START: quickstart/,/DOCS_END: quickstart/p' \
tests/examples/quickstart.test.ts \
| grep -v 'DOCS_' > docs/snippets/quickstart.ts
Expected: Every code example in docs passes tests. Breaking changes in code automatically break doc builds.
Step 3: Validate Links and References
# .github/workflows/docs-validation.yml
name: Validate Docs
on: [pull_request]
jobs:
validate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
# Check for broken links
- name: Check markdown links
uses: gaurav-nelson/github-action-markdown-link-check@v1
with:
config-file: '.markdown-link-check.json'
# Verify code examples compile
- name: Test code snippets
run: |
npm install
npm run test:docs-examples
# Check for outdated version references
- name: Validate versions
run: |
# Fail if docs reference versions older than package.json
node scripts/check-version-refs.js
Why this works: CI catches broken links, outdated code, and version mismatches before merge.
Step 4: Automate Doc Freshness Checks
// scripts/check-doc-freshness.js
const fs = require('fs');
const { execSync } = require('child_process');
// Get files changed in last 90 days
const recentFiles = execSync(
'git log --since="90 days ago" --name-only --pretty=format: | sort -u'
).toString().split('\n');
const sourceFiles = recentFiles.filter(f => f.startsWith('src/'));
const docFiles = recentFiles.filter(f => f.startsWith('docs/'));
// If source changed but docs didn't, flag it
sourceFiles.forEach(srcFile => {
const relatedDoc = `docs/${srcFile.replace('src/', '').replace('.ts', '.md')}`;
if (fs.existsSync(relatedDoc) && !docFiles.includes(relatedDoc)) {
console.warn(`⚠️ ${srcFile} changed but ${relatedDoc} wasn't updated`);
}
});
Expected: Weekly reports showing which docs might be stale based on related code changes.
Step 5: Document in Pull Requests
Add a docs checklist to your PR template:
<!-- .github/pull_request_template.md -->
## Documentation Changes
- [ ] Updated relevant markdown files in `/docs`
- [ ] Added/updated code examples (extracted from tests)
- [ ] Verified all links work locally
- [ ] Updated CHANGELOG.md with user-facing changes
- [ ] No changes needed (internal refactor only)
Why this works: Makes doc updates a required step, not an afterthought.
Verification
Test the workflow:
# 1. Make a code change
echo "export function newFeature() {}" >> src/api.ts
# 2. Update docs
echo "## New Feature\n\n..." >> docs/api.md
# 3. Extract examples
npm run docs:extract-examples
# 4. Run validation
npm run docs:validate
# 5. Preview locally
npm run docs:serve
You should see:
- ✅ All links valid
- ✅ Code examples compile
- ✅ No version mismatches
- 🌐 Local preview at
localhost:3000
What You Learned
- Co-locating docs with code enables version control and review
- Extracting examples from tests prevents outdated code
- CI/CD for docs catches issues before users see them
- Automation reduces the burden on developers
Limitations:
- Requires cultural shift—team must value docs as code
- Initial setup takes 1-2 days for CI integration
- Not ideal for non-technical docs (marketing, legal)
Tooling Recommendations
Static Site Generators (Hugo-Compatible)
# For Hugo sites, add to config.toml
[markup.goldmark.renderer]
unsafe = true # Allow HTML in markdown for code extraction
[outputs]
home = ["HTML", "JSON"] # Generate JSON for API docs
<!-- Custom shortcode for tested examples -->
<!-- layouts/shortcodes/tested-example.html -->
{{ $file := .Get "file" }}
{{ readFile (printf "docs/snippets/%s" $file) | highlight (.Get "lang") "" }}
<small>✓ Tested in <code>tests/examples/</code></small>
Alternative: VitePress (Modern, Fast)
// .vitepress/config.js
export default {
title: 'My Project Docs',
themeConfig: {
// Automatic sidebar from file structure
sidebar: 'auto',
// Show last updated from git
lastUpdated: true,
// Edit link in GitHub
editLink: {
pattern: 'https://github.com/user/repo/edit/main/docs/:path',
text: 'Edit this page'
}
}
}
Documentation Linting
# Install vale (prose linter)
# brew install vale
# .vale.ini
StylesPath = styles
MinAlertLevel = suggestion
[*.md]
BasedOnStyles = write-good, Microsoft
Real-World Examples
Stripe's API Docs:
- Code examples tested in CI
- Version selector shows different SDK versions
- "Try it" sandbox uses same code as docs
Rust's Documentation:
cargo testruns code in doc comments- Docs build fails if examples don't compile
- Every release includes tested docs
Kubernetes Docs:
- YAML manifests validated against schemas
- kubectl commands tested in CI
- Community PRs required for API changes
Migration Checklist
Moving from traditional docs to docs-as-code:
- Audit existing docs (what's still relevant?)
- Set up docs directory in repository
- Configure static site generator
- Add CI validation workflow
- Extract first set of code examples from tests
- Update PR template with docs checklist
- Train team on markdown and git workflow
- Deprecate old documentation system (after 90 days)
- Set up redirect from old URLs to new site
Timeline: 2-3 weeks for initial setup, 1-2 months for full team adoption.
Tested with Hugo 0.122+, VitePress 1.0+, GitHub Actions, GitLab CI