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-skillsrepo 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.txtbecame 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 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:
- "Do you want a MkDocs Material documentation site for this project? (yes / no / I'll think about it)"
- 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 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.ymlalready exists → report "looks like an mkdocs site already exists at ; 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 createmkdocs.ymlpointing at them withnavomitted for filesystem auto-navigation; (b) wrap — createmkdocs.ymlwith 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 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:
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 skills/local/mkdocs-site-bootstrap/scripts/enable-pages.sh \
--repo owner/repo
Flags:
--dry-run— print theghcalls 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 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 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:
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 buildbetween 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:
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.
- Flags:
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/(themkdocs buildoutput dir) to.gitignore, idempotently. --socialopts into OG/Twitter cards: expands the__SOCIAL_*__markers in the templates with the blocks inassets/social/, and adds/.cache/to.gitignore. Off by default (keeps the scaffold Cairo/Pango-free).
- Flags:
- Project-local
scripts/build-docs-site.py— Canonical strict production build helper, copied fromassets/build-docs-site.pyinto 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 replacesite/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:
0success;2invalid/missing input;3MkDocs failed;4generated output failed validation.
- Flags:
scripts/enable-pages.sh— Enable Pages and trigger first deploy viagh api. Requiresgh auth statusto pass first.- Flags:
--repo OWNER/REPO,--no-trigger,--dry-run.
- Flags:
scripts/add-docs-page.sh— Create a new page, update explicit nav when present, and keepllmstxt.sectionssynchronized. With auto-navigation (navomitted),_rootpages remain filesystem-discovered instead of creating a partial explicit nav. A named--sectionmust already exist and is validated before any page is written. If multiple languages are configured, also writes*.<LANG>.mdstubs for every non-default language.- Flags:
--section,--title,--slug,--template PATH,--lang LANG(single-language stub only),--dry-run,--force.
- Flags:
scripts/add-language.sh— Retrofit a non-default language into an existing site. Insertsplugins.i18n, creates*.<LANG>.mdstubs with the terminology admonition, updates preferences, uncomments the static-i18n dep, and configures the managed strict build path. Keeps default-languagemkdocs-llmstxtoutput 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
11means the language was added but custom downstream shapes still require the migration guide's manual actions.
- Flags:
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:
0safe/migrated;10affected legacy config found during audit/dry-run;11manual actions remain;12strict verification failed.
- Flags:
Reference files
references/preferences-schema.md— Schema for.skills/preferences.yamland 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-existingdocs/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 usingmkdocs-static-i18n. Read this before runningadd-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 pinsmkdocs<2andmkdocs-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-readydocs_dir, and pymdownx.snippets; parameterized with{{SITE_NAME}},{{REPO_SLUG}},{{SITE_URL}}. Carries__SOCIAL_*__marker lines thatinit-docs-site.shexpands (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'sscripts/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.ymlwith 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 wheninit-docs-site.sh --socialis passed:mkdocs-plugin.yml(thesocialplugin block),pyproject-dep.txt(mkdocs-material[imaging]), andci-steps.yml(the Cairo/Pango install +.cache/plugin/socialcache). 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 byadd-docs-page.sh.assets/translation-stub.md.template— Stub used for non-default-language pages byadd-language.shandadd-docs-page.sh. Contains the verbatim terminology-rule admonition.assets/i18n-plugin.yml.snippet— Reference YAML block for themkdocs-static-i18nplugin (used byreferences/i18n-guide.md; the script builds the equivalent block viayq).
Gotchas
- Social cards (OG previews) are opt-in — off by default. Pass
--socialtoinit-docs-site.shto 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 baremkdocs buildrenders 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 thesocialplugin block tomkdocs.yml,mkdocs-material[imaging]to the docs deps, the Cairo/Pango + cache steps todocs.yml, and/.cache/to.gitignore(copy fromassets/social/*— the same snippets--socialinjects). - The
socialplugin needs system Cairo/Pango + the[imaging]extra.mkdocs-material[imaging]pulls cairosvg/pillow, but those bind to systemlibcairo/libpango— without them the build aborts with a libcairo load error. With--social,docs-workflow.yml.templateapt-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).--socialalso 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.shauto-sets a CJK-capablesocial.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--socialbefore adding zh-TW, or set the font yourself afterward). Verify by opening a renderedsite/assets/images/social/**/<page>.png, not just by trusting the build. yqin these scripts is mikefarah yq (v4), which has NOif/then/else/endsyntax — that's jq. Use(.plugins[] | select(has("x")) | .x.y) = zor+= […], never.plugins |= map(if has("x") then … else . end)(it dies with alexer: invalid input text "if …"error). This bit the i18n second-language append path historically; keep new expressions in theselect()form.- MkDocs strict mode rejects relative
.mdlinks pointing outsidedocs/. Insidedocs/→ relative is fine. Outsidedocs/for.mdfiles (e.g., linking to repoTODO.md) → use absolute GitHub URL. Outsidedocs/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.snippetsinclude fragments belong in recursiveexclude_docs.not_in_navonly suppresses warnings and still publishes standalone HTML; the template excludes root and nested_snippets/directories.gh api -X POST .../pagesis idempotent forbuild_type=workflowbut errors on404 Not Foundif the repo isn't pushed to GitHub yet. Checkgh repo viewsucceeds before runningenable-pages.sh.- The Pages deploy workflow needs
permissions: pages: write, id-token: writeat the workflow level. Template has it; if you copy pieces into an existing workflow, don't lose this. copy-to-llmplugin'srepo_urlis 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.yamlis per-repo, not global. Don't write it to~/.skills/or~/.config/. Each repo has its own decisions.mkdocs-static-i18nrequirestheme.languageset to the default language code. The plugin warns when it's missing.add-language.shsets it on first run; if you copy pieces by hand, don't forget.docs_structure: suffixonly.add-language.shwrites the suffix layout (index.md+index.zh-TW.mdsiblings); thei18n_structure: folderpreference key is reserved but not implemented. Don't paste afolderconfig intomkdocs.ymland expect the script to keep it consistent.- A single multilingual build silently corrupts
mkdocs-llmstxtoutput.mkdocs-static-i18nperforms a full build per locale, while llmstxt clears its page state and overwrites the same rootllms.txt/llms-full.txton every pass. The final locale can therefore leave nearly empty or wrong-locale output. Removing--strictonly hides warnings; it does not repair the files. Always usescripts/build-docs-site.pyfor the deployable artifact. Root/llms.txt,/llms-full.txt, and.mdsidecars intentionally contain only the default language. add-language.shremovesnavigation.instantfromtheme.featuresbecause 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.