Docs as Code skill for Codex
Docs that ship in the same PR as the code. Codex inspects the existing docs setup before proposing anything, picks a platform from concrete trade-offs such as built-in versioning and search, and makes the build fail on broken internal links. It wires markdownlint, Vale with a project vocabulary and PR previews into CI, and prepares the publish job but asks before deploying anything public.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- docs-as-code Installs to ~/.agents/skills/docs-as-code/ for every project on this machine.
curl -fsSL codexskills.dev/cli.mjs -o codex-skills.mjsnode codex-skills.mjs install docs-as-code The CLI also searches, updates and removes skills. Run it with no arguments to pick from a list. Needs Node 20.12+.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- --project docs-as-code Installs to .agents/skills/docs-as-code/ in the current repository. Commit it and everyone who runs Codex there gets it.
curl -fsSLO codexskills.dev/downloads/docs-as-code.zipunzip -o docs-as-code.zip -d ~/.agents/skills/ The archive holds the skill folder with its generated agents/openai.yaml. Unzip into .agents/skills/ to scope it to a repo.
& ([scriptblock]::Create((irm codexskills.dev/install.ps1))) docs-as-code PowerShell 5.1 or 7. Installs to %USERPROFILE%\.agents\skills\. Add -Project for the current repo.
104-line POSIX script. No sudo, no telemetry. Read install.sh
Use it in Codex
Start a prompt with $docs-as-code, or pick it from /skills.
You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.
What Codex reads before loading it
Sets up and maintains documentation sites that live in the repo and ship through CI - Docusaurus, Astro Starlight and Mintlify (MkDocs as an escape hatch) - with Markdown/MDX sources, PR previews, strict link checking, Vale prose linting, markdownlint and versioned docs. Use when choosing or scaffolding a docs site, wiring docs builds and link checks into GitHub Actions, adding versioning for a new major release, migrating from a wiki, or fixing a docs build that breaks on broken links.
Use it when
- You are choosing or scaffolding a documentation site that lives in the repo
- Docs builds need lint, link checks and previews in GitHub Actions
- A new major release needs versioned docs or a wiki is moving into Markdown
Use something else when
- You need to decide which pages to write and how to classify them: use diataxis-docs
- You are auditing existing docs content for gaps and staleness: use documentation-audit
- Only the API reference content needs work: use api-documentation
How to check the result
The docs build passes locally, fails when one internal link is deliberately broken, and the CI workflow runs on a test pull request with lint results and a preview link.
The mistake it prevents
Renaming or moving doc pages without redirects, which silently breaks every inbound link from search results and other sites.
Docs-as-code is working when docs change in the same pull request as the code they describe, a broken link or invalid page fails CI before merge, reviewers can see a rendered preview, and publishing is a merge rather than a manual upload.
Inspect first
cat AGENTS.md 2>/dev/null | head -80
ls docs website site 2>/dev/null; ls docusaurus.config.* astro.config.* docs.json mint.json mkdocs.yml 2>/dev/null
cat package.json 2>/dev/null | head -40 # node version, docs scripts
ls .github/workflows; rg -l "docs" .github/workflows 2>/dev/null
ls .vale.ini .markdownlint* 2>/dev/null
If a platform already exists, keep it unless the user asked to migrate. Most “docs site” problems are content and CI problems, not platform problems.
Choose a platform
| Situation | Default | Why |
|---|---|---|
| Open-source project or product docs, React team, needs versioning and i18n | Docusaurus 3 | Built-in versioning, mature plugin ecosystem |
| Wants fast static site, minimal JS, built-in search, Astro or framework-agnostic team | Astro Starlight | Pagefind search built in, strong defaults, accessible |
| API-first SaaS wanting hosted docs with API playground and little maintenance | Mintlify | Hosted, docs.json config, OpenAPI pages; paid tiers for some features |
| Python project already on MkDocs | Stay on MkDocs | mkdocs build --strict gives the CI gate; migration rarely pays off |
| Internal docs for one service | Markdown in docs/ rendered by the Git host | No site needed until there are ~20+ pages or outside readers |
Versioning trade-off: Docusaurus versions by copying the docs folder per version
(versioned_docs/version-x/). Starlight has no built-in versioning (community plugins
exist). Mintlify versions through navigation.versions in docs.json. If you must keep
several majors documented, that difference often decides the platform.
Platform-specific setup, config snippets and gotchas live in references/platforms.md; read it once the platform is chosen.
Workflow
- [ ] 1. Inspect existing docs, platform and CI (commands above)
- [ ] 2. Choose or confirm the platform (table above); confirm with the user before migrating
- [ ] 3. Scaffold or fix config; make the build fail on broken links
- [ ] 4. Add lint: markdownlint-cli2 for structure, Vale for prose
- [ ] 5. Add CI: build + lint on every PR touching docs; preview deployment
- [ ] 6. Add publish job on main (confirm the deploy target with the user)
- [ ] 7. Set versioning policy if the product ships majors
- [ ] 8. Document the docs workflow in AGENTS.md / CONTRIBUTING.md
Scaffolding and installs need network (npm create, npx, pip install). In a Codex
sandbox with network off, request approval or prepare the files and leave the install
for the user, saying so in the report.
Repository layout
docs/ # content (Markdown/MDX), one folder per section
getting-started/
guides/
reference/
static/ or public/ # images, downloads
.vale.ini # prose lint config
styles/ # Vale styles and project vocabulary
config/vocabularies/Project/accept.txt
.markdownlint-cli2.jsonc
.github/workflows/docs.yml
- Keep content separate from site code so writers do not touch React or Astro files.
- Images next to the page or in one
static/img/<section>/tree; name them by content (webhook-retry-timeline.png), always with alt text. - One Markdown file per URL. File and folder names become slugs; renames break links, so add redirects when you move pages.
Lint configuration
# .vale.ini
StylesPath = styles
MinAlertLevel = suggestion
Vocab = Project
Packages = Google, write-good
[*.{md,mdx}]
BasedOnStyles = Vale, Google
Google.Headings = NO # turn off rules that fight the house style
vale sync # downloads packages listed above (network)
vale docs/
npx markdownlint-cli2 "docs/**/*.md"
Add product names, acronyms and code terms to
styles/config/vocabularies/Project/accept.txt (one per line, regex allowed) instead of
disabling the spelling rule.
CI pipeline (GitHub Actions, Docusaurus or Starlight)
name: docs
on:
pull_request:
paths: ['docs/**', 'website/**', 'package.json', '.github/workflows/docs.yml']
push:
branches: [main]
permissions:
contents: read
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/setup-node@v5
with: { node-version: 22, cache: npm }
- run: npm ci
- run: npx markdownlint-cli2 "docs/**/*.md"
- uses: errata-ai/vale-action@v2
with: { files: docs }
- run: npm run build # fails on broken links when configured to throw
- uses: actions/upload-pages-artifact@v4
if: github.ref == 'refs/heads/main'
with: { path: build } # Starlight/Astro: dist
deploy:
if: github.ref == 'refs/heads/main'
needs: build
runs-on: ubuntu-latest
permissions: { pages: write, id-token: write }
environment: { name: github-pages, url: '${{ steps.d.outputs.page_url }}' }
steps:
- id: d
uses: actions/deploy-pages@v4
Pin action majors to whatever is current when you write the workflow, and pin by SHA if
the repo’s security policy requires it. The deploy job publishes to the public internet:
add or change it only after the user confirms the target (GitHub Pages, Netlify, Vercel,
Cloudflare Pages) and that the content is meant to be public.
PR previews: Netlify, Vercel and Cloudflare Pages create them per PR through their Git integration; Mintlify generates preview deployments for PRs when the GitHub app is installed. Prefer the host’s integration over custom preview jobs.
Link checking
| Layer | Tool | Catches |
|---|---|---|
| Internal links during build | Platform setting (onBrokenLinks: 'throw', mkdocs build --strict, starlight-links-validator, mint broken-links) | Moved or renamed pages, bad anchors |
| External links | lychee on a schedule, not on every PR | Rotted third-party URLs |
Run external link checks nightly or weekly; third-party outages should not block merges.
lychee --offline docs/ # local files and anchors only, no network
lychee --max-retries 2 --accept 200,429 docs/ # full check, needs network
Versioning policy
- Version docs only when users run several majors in production. Otherwise one “latest” set plus a changelog is cheaper and less confusing.
- Cut a docs version at release time, never before:
npm run docusaurus docs:version 2.0snapshots the currentdocs/intoversioned_docs/version-2.0/. - Fix typos in every maintained version; backport only factual corrections to old ones.
- Drop versions that are out of support and redirect them to the latest.
- Show an “unreleased” banner on the
nextdocs so readers do not use unshipped APIs.
Gotchas
- MDX is JSX: bare
<,{and unclosed HTML tags in Markdown break the build. Escape or wrap in code spans. - Moving or renaming files without redirects breaks inbound links from search engines and other sites. Add redirects in the same PR.
- Generated reference (API, CLI) committed by hand drifts. Generate it in the build or in a CI job that fails when the output differs.
- Versioned docs multiply build time and search noise; each version is a full copy.
- Search indexing (Algolia DocSearch) needs a crawler config; local search plugins are simpler for small sites.
- Making a whole workflow pass by setting link checks to
warn. Fix or explicitly exclude the specific link instead.
Verification
npm run build(or platform equivalent) passes locally and fails when you deliberately break one internal link; revert the break afterwards.vale docs/andmarkdownlint-cli2run clean, or remaining alerts are listed.- CI runs on a test PR and shows a preview link.
- Every page reachable from navigation; no orphans (compare sidebar entries to files).
Report
**Platform:** Astro Starlight (kept existing), Node 22
**Build gate:** starlight-links-validator added; build now fails on 3 broken links, all fixed
**Lint:** Vale (Google + Project vocab, 41 accepted terms), markdownlint-cli2
**CI:** .github/workflows/docs.yml, PR build + lint; deploy job prepared but disabled pending your confirmation of GitHub Pages
**Versioning:** not enabled; single latest docs (one supported major)
**Not run:** vale sync and npm ci (network off in sandbox) What the zip contains
Codex reads bundled files only when a step points to them. Every one is shown here, so you can review it before installing.
agents/openai.yaml
Gives the Codex app a display name, a one-line description and a starter prompt for this skill.
interface:
display_name: "Docs as Code"
short_description: "Docs that ship in the same PR as the code"
default_prompt: "$docs-as-code Set up a Docusaurus docs site in this repo with link checking in CI" references/platforms.md
# Platform notes: Docusaurus, Starlight, Mintlify, MkDocs
Read only the section for the platform in use. Versions move quickly; check the project's
lockfile and the platform's release notes before copying a config key, and prefer the
pinned version in `package.json` over `@latest`.
## Docusaurus 3
Scaffold (network): `npx create-docusaurus@latest website classic --typescript`
Key config in `docusaurus.config.ts`:
```ts
import type { Config } from '@docusaurus/types';
const config: Config = {
title: 'Acme Docs',
url: 'https://docs.acme.dev',
baseUrl: '/',
onBrokenLinks: 'throw',
onBrokenAnchors: 'throw',
markdown: {
format: 'detect', // .md parsed as CommonMark, .mdx as MDX
hooks: { onBrokenMarkdownLinks: 'throw' },
},
presets: [
['classic', {
docs: {
routeBasePath: '/', // docs-only site
sidebarPath: './sidebars.ts',
editUrl: 'https://github.com/acme/acme/edit/main/website/',
versions: { current: { label: 'Next', banner: 'unreleased' } },
},
blog: false,
}],
],
};
export default config;
```
- Older 3.x releases accept `onBrokenMarkdownLinks` at the top level; recent 3.x versions
deprecate that in favour of `markdown.hooks.onBrokenMarkdownLinks`. Use whichever the
installed version documents; the build warns if you pick the deprecated one.
- `markdown.format: 'detect'` avoids MDX parsing errors in plain `.md` files that contain
`<` or `{`.
- Versioning: `npm run docusaurus docs:version 2.0` copies `docs/` to
`versioned_docs/version-2.0/` and the sidebar to `versioned_sidebars/`. Edits to old
versions go in those folders, not in `docs/`.
- Redirects: `@docusaurus/plugin-client-redirects` with `redirects: [{ from, to }]`.
Client-side only; set server redirects too if the host supports them.
- Search: Algolia DocSearch (apply for the free program for open source) or a local
search plugin for private or small sites.
- Build output: `build/`. Preview locally with `npm run serve`.
## Astro Starlight
Scaffold (network): `npm create astro@latest -- --template starlight`
```js
// astro.config.mjs
import { defineConfig } from 'astro/config';
import starlight from '@astrojs/starlight';
import starlightLinksValidator from 'starlight-links-validator';
export default defineConfig({
site: 'https://docs.acme.dev',
integrations: [
starlight({
title: 'Acme Docs',
editLink: { baseUrl: 'https://github.com/acme/acme/edit/main/' },
plugins: [starlightLinksValidator()],
sidebar: [
{ label: 'Start here', items: ['getting-started', 'installation'] },
{ label: 'Guides', autogenerate: { directory: 'guides' } },
{ label: 'Reference', autogenerate: { directory: 'reference' } },
],
}),
],
});
```
```ts
// src/content.config.ts
import { defineCollection } from 'astro:content';
import { docsLoader } from '@astrojs/starlight/loaders';
import { docsSchema } from '@astrojs/starlight/schema';
export const collections = {
docs: defineCollection({ loader: docsLoader(), schema: docsSchema() }),
};
```
- Content lives in `src/content/docs/`; the file path is the URL.
- Search: Pagefind runs at build time, nothing to configure. It only indexes the built
site, so `astro dev` shows no search results; test with `astro build && astro preview`.
- `starlight-links-validator` (community plugin, install it) fails the build on broken
internal links and anchors.
- Versioning: none built in. Options are a community plugin, separate deployments per
major on subdomains, or a single latest site.
- Redirects: `redirects` in `astro.config.mjs` (Astro core).
- Build output: `dist/`.
## Mintlify
Install the CLI (network): `npm i -g mint`. The older `mintlify` package name is legacy.
```bash
mint dev # local preview at localhost:3000
mint broken-links # internal link check; run from the folder containing docs.json
```
`docs.json` replaced `mint.json` as the config file. Minimal shape:
```json
{
"$schema": "https://mintlify.com/docs.json",
"theme": "mint",
"name": "Acme",
"colors": { "primary": "#0F766E" },
"navigation": {
"groups": [
{ "group": "Get started", "pages": ["index", "quickstart"] },
{ "group": "API reference", "openapi": "openapi.json" }
]
}
}
```
- Pages are `.mdx` with frontmatter (`title`, `description`). Page paths in `navigation`
omit the extension.
- OpenAPI: point a group or tab at the spec to generate endpoint pages with a playground.
Pair with the api-documentation skill for spec quality.
- Versions: `navigation.versions` arrays, each with its own groups.
- Deployment happens through Mintlify's GitHub app on push; CI in your repo should still
run `mint broken-links` on PRs. A folder with a Python `.venv` inside the docs root can
confuse the CLI; keep docs in their own directory.
- Some features (custom domains on certain plans, analytics, AI assistant) depend on the
paid tier. Confirm with the user before relying on them.
## MkDocs (escape hatch)
For Python projects already using it:
```bash
pip install mkdocs-material # or the project's pinned docs requirements
mkdocs serve
mkdocs build --strict # warnings (broken links, missing nav files) fail the build
```
- `validation:` options in `mkdocs.yml` (MkDocs 1.6+) control how strictly links, anchors
and nav entries are checked.
- Versioning with `mike` (`mike deploy 2.0 latest --update-aliases`) publishes each
version to a `gh-pages` branch; this pushes to the remote, so confirm before running it.
- Check the current maintenance status of the theme you rely on before starting a new
site on it; prefer the platforms above for greenfield projects.
About this skill
Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.