# Mkdocs Site Bootstrap

> Bootstrap or maintain MkDocs Material docs sites with GitHub Pages, strict two-pass i18n plus default-language llms.txt, copy-to-LLM, page/nav helpers, and zh-TW translation. Use when setting up or publishing docs, adding bilingual or multilingual docs, repairing nearly empty llms.txt or i18n strict-build failures, or migrating an older mkdocs-site-bootstrap site. Consent-gated; never rewrites existing docs without approval.

- Skill: `daviddwlee84/mkdocs-site-bootstrap` (Agent Skill, multi-file: 31 files)
- Install (CLI): `npx skillmds@latest add daviddwlee84/mkdocs-site-bootstrap`
- Raw SKILL.md: https://api.skillmd.com/api/skills/daviddwlee84/mkdocs-site-bootstrap/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: AI & ML
- Author: daviddwlee84 (https://skillmd.com/u/daviddwlee84)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/daviddwlee84/mkdocs-site-bootstrap

---


# mkdocs-site-bootstrap

Bootstrap and (optionally) deploy a MkDocs Material documentation site for a
repository, then keep helping the user add pages over time.

This skill is **consent-gated**. It records the user's preferences in
`.skills/preferences.yaml` and never migrates existing content or configuration
without an explicit request. Its migration command audits by default and only
writes with `--apply`. If the user changes their mind,
`scripts/check-preferences.sh --reset mkdocs_site_bootstrap` clears the recorded
decision so the next invocation starts fresh.

## When to trigger

- User asks to "set up docs", "create a docs site", "add a documentation
  site", "publish docs to GitHub Pages"
- User has loose markdown notes / a `docs/` directory and wants it browsable
- User wants the same docs stack as the `daviddwlee84/agent-skills` repo
  applied to a new project
- User says they want an LLM-friendly docs site (llms.txt, copy-to-LLM)
- User asks for "bilingual docs", "雙語 docs", "i18n", "multilingual",
  "add Traditional Chinese", "add zh-TW", "translate the docs", or to add
  any non-English language to an existing site → jump to step 7
- User reports that `llms.txt` / `llms-full.txt` became empty, only contains
  one locale, or that adding i18n broke a strict build → jump to step 8

## When NOT to trigger

- User just wants to write a single doc file → don't scaffold a whole site
- User explicitly opted out (preferences.yaml says `enabled: false`) → defer
  unless the user is now reversing that decision
- User wants to evaluate or benchmark a skill → use `skill-creator`
- User wants to author a new agent skill → use `skill-author`

## Workflow

### 1. Read preferences first

Before doing anything, check whether this repo already has a recorded
decision:

```bash
bash skills/local/mkdocs-site-bootstrap/scripts/check-preferences.sh \
  --get mkdocs_site_bootstrap
```

Possible states:

| State | Meaning | What to do |
|---|---|---|
| File doesn't exist or key missing | Never asked | Proceed to step 2 (interview) |
| `enabled: true, pages_deployed: true` | Site is live | Skip to step 6 (ongoing helpers) |
| `enabled: true, pages_deployed: false` | Bootstrapped but not deployed | Skip to step 5 (deploy) |
| `enabled: false` | User opted out | Confirm they want to reverse that, then `--reset` and re-run |

### 2. Interview (only if no recorded decision)

Ask the user explicitly:

1. "Do you want a MkDocs Material documentation site for this project?
   (yes / no / I'll think about it)"
2. If yes: "Should it auto-deploy to GitHub Pages on push to main?
   (yes / no — I'll deploy manually)"

Record both answers immediately so an interrupted session doesn't lose them:

```bash
bash skills/local/mkdocs-site-bootstrap/scripts/check-preferences.sh \
  --set mkdocs_site_bootstrap.enabled=true \
  --set mkdocs_site_bootstrap.stack=mkdocs-material \
  --set mkdocs_site_bootstrap.auto_deploy=true
```

If the user said no, record `enabled: false` and stop. Don't pester on
future invocations.

### 3. Detect existing docs (consent gate)

Before scaffolding, scan the target repo. Read
`references/existing-docs-handling.md` for the full decision tree, but
the short version:

- If `mkdocs.yml` already exists → report "looks like an mkdocs site already
  exists at <path>; not overwriting" and stop.
- If `docs/` exists and is non-empty → list the files, ask the user one of:
  (a) skip — leave my docs alone, just create `mkdocs.yml` pointing at them
  with `nav` omitted for filesystem auto-navigation;
  (b) wrap — create `mkdocs.yml` with my Markdown paths included as-is in an
  explicit, alphabetical nav;
  (c) manual — let me reorganize first, then re-run.
- If neither exists → safe to scaffold from scratch.

Record the decision under `mkdocs_site_bootstrap.existing_docs_decision`.

### 4. Scaffold

Run `init-docs-site.sh`. It writes (or refuses to overwrite) `mkdocs.yml`,
`pyproject.toml` (with `[project.optional-dependencies] docs = […]`), the
`docs/` skeleton, `.github/workflows/docs.yml`, the managed
`scripts/build-docs-site.py` helper, and stub assets for copy-to-llm. It also
appends `/site/` (the build output dir) to `.gitignore` so the generated site is
never committed — plus `/.cache/` when `--social` is on.

```bash
bash skills/local/mkdocs-site-bootstrap/scripts/init-docs-site.sh \
  --site-name "My Project" \
  --repo-slug owner/repo \
  --site-url https://owner.github.io/repo/
```

Use `--dry-run` first to preview. The script always preserves any existing
files unless `--force` is passed, and refuses symlinks anywhere in an existing
`docs/` tree rather than risking writes outside the repository. In `skip` and
`wrap` modes it omits `copy-to-llm`, whose build hook writes generated assets
under `docs/`; users can opt into that plugin separately after choosing where
those generated files belong.

Add `--social` to enable OG/Twitter preview cards (see Gotchas → "Social cards
are opt-in"). Leave it off unless the user wants rich link unfurls and can take
the Cairo/Pango system dependency; a plain scaffold builds dependency-free.

After scaffolding, build the complete strict artifact:

```bash
uv sync --extra docs
uv run python scripts/build-docs-site.py
```

For multilingual sites that keep llmstxt, this runs two isolated strict passes:
default-language LLM output, then the full multilingual HTML site. A direct
`uv run mkdocs build --strict` remains a safe HTML-only preview because
`llmstxt` is disabled by default in `mkdocs.yml`; do not deploy that preview
when `/llms.txt` is part of the site's contract.

### 5. Enable Pages and trigger first deploy (consent gate)

This calls the GitHub API (`gh api -X POST .../pages -f build_type=workflow`)
and then triggers the workflow. **Always confirm with the user first** —
say exactly which API call you're about to make and which repo it'll affect.
Only proceed on explicit yes.

```bash
bash skills/local/mkdocs-site-bootstrap/scripts/enable-pages.sh \
  --repo owner/repo
```

Flags:
- `--dry-run` — print the `gh` calls without running them
- `--no-trigger` — enable Pages but don't run the workflow yet

After success, set `pages_deployed=true` and `pages_enabled_at=$(date +%F)`
in preferences.

### 6. Ongoing: add docs pages

For each new doc the user wants, use the helper instead of hand-editing
`mkdocs.yml`:

```bash
bash skills/local/mkdocs-site-bootstrap/scripts/add-docs-page.sh \
  --section workflows \
  --title "My new workflow" \
  --slug my-new-workflow
```

It creates `docs/<section>/<slug>.md` from the page template and inserts a
nav entry into `mkdocs.yml` under the matching section heading. Idempotent
— re-running with the same slug is a no-op.

If the project has additional languages configured in
`.skills/preferences.yaml` (`mkdocs_site_bootstrap.languages`), `add-docs-page.sh`
also generates `*.<LANG>.md` stubs for every non-default language, with the
terminology-rule admonition pre-injected. Use `--lang LANG` to add only the
translation for one specific language without re-creating the default.

### 7. Optional: add a non-English language

The skill supports bilingual / multi-language sites via the
`mkdocs-static-i18n` plugin (suffix layout: `index.md` + `index.zh-TW.md`).
This step is opt-in and decoupled from initial bootstrap.

Trigger: user asks for "bilingual docs", "雙語 docs", "add zh-TW", "i18n",
"add Traditional Chinese", "translate the docs", or similar.

Read `references/i18n-guide.md` first — it covers the **terminology
preservation rule** ("中文 (English original)" format on first mention; no
invented translations) which authors must follow on non-English pages.

Then run:

```bash
bash skills/local/mkdocs-site-bootstrap/scripts/add-language.sh \
  --lang zh-TW
```

This inserts the i18n plugin into `mkdocs.yml`, creates `*.zh-TW.md` stub
siblings of every existing page (with the terminology admonition
pre-injected), installs/configures the managed strict build helper, enables the
plugin guards required by the two-pass build, uncomments `mkdocs-static-i18n`
in `pyproject.toml`, and records the choice in `.skills/preferences.yaml`.
Idempotent — re-running with the same `--lang` is a no-op.

`--remove-llmstxt` remains an explicit opt-out. The old `--drop-strict` flag is
a deprecated no-op because removing strict never fixed the corrupted output;
`--keep-llmstxt` is accepted as a deprecated alias for today's default.

After it runs, re-sync deps and rebuild:

```bash
uv sync --extra docs
uv run python scripts/build-docs-site.py
```

#### After the script finishes — translation is a separate step

`add-language.sh` only produces **structural placeholders**: each
`*.<LANG>.md` stub contains the terminology admonition plus a "Translation
pending" warning. The script never auto-translates body prose, because LLM
translation routinely violates the "no invented translations" terminology
rule (e.g. coining 「嵌入」/「向量」/「内嵌」 for `embedding` when none is
canonical).

Once stubs exist, **explicitly ask the user whether to translate them now**
— do not assume. A reasonable script:

> "Stubs created. Want me to translate the bodies page-by-page? I'll do
> 4-6 pages per batch, run `mkdocs build` between batches to verify, and
> follow the terminology rule (English original on first mention, never
> invent translations)."

Only proceed on explicit yes. If the user defers, leave the stubs in place
— they're already valid pages and the build passes.

When translating, also offer to populate `nav_translations` in `mkdocs.yml`
for top-level section headings (see `references/i18n-guide.md`
§nav_translations). This is independent from page-body translation and the
user may want one without the other.

### 8. Repair an older i18n + llmstxt scaffold

Updating the installed skill only downloads the new tooling; it does **not**
rewrite the downstream project. Read `references/i18n-llmstxt-migration.md`
before migrating an existing site, then audit first:

```bash
npx skills@latest update mkdocs-site-bootstrap --project --yes
bash .agents/skills/mkdocs-site-bootstrap/scripts/migrate-i18n-llmstxt.sh \
  --target-dir . --json
```

Exit `10` means the legacy affected shape was detected (including a dry-run
preview). Preview the conservative patch with `--apply --dry-run`, then run
`--apply --verify`. The migration only patches recognizable scaffold-owned
shapes; custom `docs_dir`, plugin guards, CI, Makefiles, or a foreign build
helper become explicit manual actions instead of being overwritten. Unsafe
relative llms/sidecar links in localized sources are also reported individually
for manual replacement with `site_url`-based URLs.

## Available scripts

- **`scripts/check-preferences.sh`** — Read, set, or reset
  `.skills/preferences.yaml`. Always-safe to run.
  - Flags: `--get KEY`, `--set KEY=VALUE` (repeatable), `--reset NAMESPACE`,
    `--list`, `--dry-run`, `--json`.
- **`scripts/init-docs-site.sh`** — Scaffold the site files.
  - Flags: `--site-name`, `--repo-slug`, `--site-url`, `--existing skip|wrap`,
    `--no-workflow`, `--no-skeleton`, `--social`, `--dry-run`, `--force`.
  - Always appends `/site/` (the `mkdocs build` output dir) to `.gitignore`,
    idempotently.
  - `--social` opts into OG/Twitter cards: expands the `__SOCIAL_*__` markers in
    the templates with the blocks in `assets/social/`, and adds `/.cache/` to
    `.gitignore`. Off by default (keeps the scaffold Cairo/Pango-free).
- **Project-local `scripts/build-docs-site.py`** — Canonical strict production
  build helper, copied from `assets/build-docs-site.py` into each scaffold.
  Monolingual sites use one pass; multilingual sites with llmstxt isolate
  default-language LLM output from the full HTML build, validate the merged
  artifact, then replace `site/` only after all checks pass. JSON goes to
  stdout; build diagnostics go to stderr.
  - Flags: `--target-dir DIR`, `--config-file FILE`, `--site-dir DIR`,
    `--dry-run`, `--keep-temp`.
  - Exit codes: `0` success; `2` invalid/missing input; `3` MkDocs failed;
    `4` generated output failed validation.
- **`scripts/enable-pages.sh`** — Enable Pages and trigger first deploy via
  `gh api`. Requires `gh auth status` to pass first.
  - Flags: `--repo OWNER/REPO`, `--no-trigger`, `--dry-run`.
- **`scripts/add-docs-page.sh`** — Create a new page, update explicit nav when
  present, and keep `llmstxt.sections` synchronized. With auto-navigation
  (`nav` omitted), `_root` pages remain filesystem-discovered instead of
  creating a partial explicit nav. A named `--section` must already exist and
  is validated before any page is written. If multiple languages are
  configured, also writes `*.<LANG>.md` stubs for every non-default language.
  - Flags: `--section`, `--title`, `--slug`, `--template PATH`, `--lang LANG`
    (single-language stub only), `--dry-run`, `--force`.
- **`scripts/add-language.sh`** — Retrofit a non-default language into an
  existing site. Inserts `plugins.i18n`, creates `*.<LANG>.md` stubs with
  the terminology admonition, updates preferences, uncomments the static-i18n
  dep, and configures the managed strict build path. Keeps default-language
  `mkdocs-llmstxt` output by default. Idempotent.
  - Flags: `--lang LANG` (required), `--name NAME`, `--default-lang LANG`,
    `--target-dir DIR`, `--no-stubs`, `--remove-llmstxt`, deprecated
    `--drop-strict` / `--keep-llmstxt`, `--dry-run`, `--force`.
  - Exit `11` means the language was added but custom downstream shapes still
    require the migration guide's manual actions.
- **`scripts/migrate-i18n-llmstxt.sh`** — Audit or conservatively migrate a
  downstream site created by an older version of this skill. Audit is the
  default; writes require `--apply`. JSON goes to stdout and diagnostics to
  stderr. It stages and validates candidates before replacement, is
  idempotent, and never overwrites a foreign build helper.
  - Flags: `--target-dir DIR`, `--apply`, `--dry-run`, `--verify`, `--json`.
  - Exit codes: `0` safe/migrated; `10` affected legacy config found during
    audit/dry-run; `11` manual actions remain; `12` strict verification failed.

## Reference files

- `references/preferences-schema.md` — Schema for
  `.skills/preferences.yaml` and conventions for cross-skill use. Read this
  whenever you're touching a preferences key for the first time.
- `references/existing-docs-handling.md` — Full decision tree for handling
  user's pre-existing `docs/` content without surprises. Read this **before
  step 3** of every fresh bootstrap.
- `references/docs-stack-recipe.md` — Verbatim stack recipe (mkdocs.yml,
  pyproject.toml, workflow, linking rules). Useful when the user asks "what
  exactly is this stack?" or wants to apply pieces manually.
- `references/i18n-guide.md` — Bilingual / multi-language docs setup using
  `mkdocs-static-i18n`. Read this **before** running `add-language.sh`. Includes
  the verbatim "preserve English originals" terminology rule for zh-TW pages.
- `references/i18n-llmstxt-migration.md` — Audit/apply/verify migration guide,
  exit-code contract, automatic patch boundary, and manual fallback. Read this
  **before repairing or upgrading an existing i18n + llmstxt site**.
- `references/mkdocs-2-and-zensical.md` — Why the stack pins `mkdocs<2` and
  `mkdocs-material<10`. Captures the MkDocs 2.0 plugin-removal situation,
  Material team's Zensical replacement, and the criteria for lifting the
  caps. Read whenever a user asks about upgrading MkDocs or why the docs
  build is on 1.x.

## Bundled assets

Templates the scripts copy from. Edit them here, not in the user's repo.

- `assets/mkdocs.yml.template` — Material theme + environment-guarded llmstxt,
  copy-to-llm, i18n-ready `docs_dir`, and pymdownx.snippets; parameterized with
  `{{SITE_NAME}}`, `{{REPO_SLUG}}`, `{{SITE_URL}}`. Carries `__SOCIAL_*__`
  marker lines that `init-docs-site.sh` expands (with `--social`) or deletes.
- `assets/pyproject.toml.template` — Minimal `[project]` block + the docs
  optional-deps group. Has a `__SOCIAL_IMAGING__` marker.
- `assets/build-docs-site.py` — Managed two-pass helper copied to the downstream
  project's `scripts/build-docs-site.py`. Keep its managed marker intact so the
  migration tool can distinguish it from a user-owned script.
- `assets/docs-workflow.yml.template` — `.github/workflows/docs.yml` with
  paths filter, uv setup, the managed strict build helper, and Pages deploy.
  Has a `__SOCIAL_CI__` marker for the Cairo/Pango + card-cache steps.
- `assets/social/` — snippets injected at the `__SOCIAL_*__` markers when
  `init-docs-site.sh --social` is passed: `mkdocs-plugin.yml` (the `social`
  plugin block), `pyproject-dep.txt` (`mkdocs-material[imaging]`), and
  `ci-steps.yml` (the Cairo/Pango install + `.cache/plugin/social` cache).
  Pre-indented for their insertion points — keep the indentation.
- `assets/docs-skeleton/` — `index.md`, `getting-started.md`, `_snippets/`
  examples, `assets/copy-to-llm/` JS+CSS files copied from this repo.
- `assets/page.md.template` — Used by `add-docs-page.sh`.
- `assets/translation-stub.md.template` — Stub used for non-default-language
  pages by `add-language.sh` and `add-docs-page.sh`. Contains the verbatim
  terminology-rule admonition.
- `assets/i18n-plugin.yml.snippet` — Reference YAML block for the
  `mkdocs-static-i18n` plugin (used by `references/i18n-guide.md`; the script
  builds the equivalent block via `yq`).

## Gotchas

- **Social cards (OG previews) are opt-in — off by default.** Pass `--social`
  to `init-docs-site.sh` to enable them; a plain scaffold has no social plugin,
  no `[imaging]` dep, and no Cairo/Pango CI step, so it builds with zero system
  dependencies (a bare `mkdocs build` renders in well under a second). Enable
  it when the user wants rich link unfurls and accepts the cost below. To turn
  it on for an already-scaffolded site, add the `social` plugin block to
  `mkdocs.yml`, `mkdocs-material[imaging]` to the docs deps, the Cairo/Pango +
  cache steps to `docs.yml`, and `/.cache/` to `.gitignore` (copy from
  `assets/social/*` — the same snippets `--social` injects).
- **The `social` plugin needs system Cairo/Pango + the `[imaging]` extra.**
  `mkdocs-material[imaging]` pulls cairosvg/pillow, but those bind to system
  `libcairo`/`libpango` — without them the build aborts with a libcairo load
  error. With `--social`, `docs-workflow.yml.template` apt-installs them
  (`libcairo2-dev libpango1.0-dev libfreetype6-dev …`) and caches
  `.cache/plugin/social`. Locally: `brew install cairo pango` (macOS) or the
  same apt packages (Linux). `--social` also adds `/.cache/` to `.gitignore` —
  the plugin writes ~1 card PNG per page (tens of MB) plus a downloaded font
  there. Cold build renders one card per page (network-fetches the font from
  Google Fonts); warm cache is seconds.
- **CJK/arrow page titles render as tofu boxes (□□□) on social cards unless
  the card font covers them.** The plugin's default font (Roboto) and plain
  "Noto Sans" are Latin-only, so Chinese/Japanese/Korean titles — and even a
  `→` in an English title — come out as boxes. `add-language.sh` auto-sets a
  CJK-capable `social.cards_layout_options.font_family` (Noto Sans TC/SC/JP/KR)
  when you add a CJK language, but only if the social plugin is present and no
  font is already set (so scaffold with `--social` *before* adding zh-TW, or
  set the font yourself afterward). Verify by opening a rendered
  `site/assets/images/social/**/<page>.png`, not just by trusting the build.
- **`yq` in these scripts is mikefarah yq (v4), which has NO `if/then/else/end`
  syntax** — that's jq. Use `(.plugins[] | select(has("x")) | .x.y) = z` or
  `+= […]`, never `.plugins |= map(if has("x") then … else . end)` (it dies
  with a `lexer: invalid input text "if …"` error). This bit the i18n
  second-language append path historically; keep new expressions in the
  `select()` form.
- **MkDocs strict mode rejects relative `.md` links pointing outside
  `docs/`.** Inside `docs/` → relative is fine. Outside `docs/` for
  `.md` files (e.g., linking to repo `TODO.md`) → use absolute GitHub URL.
  Outside `docs/` for directories or non-`.md` (`backlog/`, `pyproject.toml`)
  → relative is downgraded to INFO and tolerated. Templates already do this
  right; don't "fix" the absolute URLs.
- **`pymdownx.snippets` include fragments belong in recursive `exclude_docs`.**
  `not_in_nav` only suppresses warnings and still publishes standalone HTML;
  the template excludes root and nested `_snippets/` directories.
- **`gh api -X POST .../pages` is idempotent for `build_type=workflow`** but
  errors on `404 Not Found` if the repo isn't pushed to GitHub yet. Check
  `gh repo view` succeeds before running `enable-pages.sh`.
- **The Pages deploy workflow needs `permissions: pages: write,
  id-token: write`** at the workflow level. Template has it; if you copy
  pieces into an existing workflow, don't lose this.
- **`copy-to-llm` plugin's `repo_url` is the SITE URL, not the GitHub URL.**
  Counter-intuitive name.
- **Don't auto-migrate existing user docs.** Always ask. Migrating someone's
  hand-curated `docs/` into a new structure is a high-trust action that
  should be the user's explicit decision, not the agent's default.
- **`.skills/preferences.yaml` is per-repo, not global.** Don't write it to
  `~/.skills/` or `~/.config/`. Each repo has its own decisions.
- **`mkdocs-static-i18n` requires `theme.language` set to the *default*
  language code.** The plugin warns when it's missing. `add-language.sh`
  sets it on first run; if you copy pieces by hand, don't forget.
- **`docs_structure: suffix` only.** `add-language.sh` writes the suffix
  layout (`index.md` + `index.zh-TW.md` siblings); the `i18n_structure: folder`
  preference key is reserved but not implemented. Don't paste a `folder`
  config into `mkdocs.yml` and expect the script to keep it consistent.
- **A single multilingual build silently corrupts `mkdocs-llmstxt` output.**
  `mkdocs-static-i18n` performs a full build per locale, while llmstxt clears
  its page state and overwrites the same root `llms.txt` / `llms-full.txt` on
  every pass. The final locale can therefore leave nearly empty or wrong-locale
  output. Removing `--strict` only hides warnings; it does not repair the files.
  Always use `scripts/build-docs-site.py` for the deployable artifact. Root
  `/llms.txt`, `/llms-full.txt`, and `.md` sidecars intentionally contain only
  the default language.
- **`add-language.sh` removes `navigation.instant`** from `theme.features`
  because the language switcher's contextual link is incompatible with
  instant navigation. Material's plugin emits the warning itself; the script
  is just acting on it.
- **Don't translate technical terms in zh-TW pages without the English
  original.** The terminology rule (kept English in parens on first mention,
  no invented translations) is non-negotiable; the stub template injects
  the rule as an admonition so authors see it before they start.

## Updating an existing site (not bootstrapping)

If `mkdocs.yml` already exists, don't silently "upgrade" it; customizations may
not match the template. For an explicit i18n/llmstxt repair request, use
`migrate-i18n-llmstxt.sh` audit-first. Apply only after showing its planned
changes and preserve every shape it reports as a manual action.

