Docs Site Setup
Show the whole prompt
Read https://intentius.io/chant/lexicon-authoring/docs-site/.
For the lexicon I name, create `src/codegen/docs.ts` and `src/codegen/docs-cli.ts` as the page shows. Register `docs()` in `src/plugin.ts` and add the `docs` script. Write its prose pages under `docs/pages/` with a `diataxis` field on each.
Run `npm run prepack` on a clean checkout, then `npm run docs` in the lexicon and `chant dev check-lexicon lexicons/<name>`, and open a pull request with the sources. Do not commit the generated pages.
Never deploy to a real cloud account or shared environment (with `chant run`, `chant components` or a platform's own CLI), approve (a pull request review, `chant approve` or `chant run approve`), override a policy denial (`chant approve policy-override`), or merge; never cut or publish a release (`chant release cut --yes` or `chant release publish`), push or delete a `ci/*` tag (`chant ci tick` pushes them), or change the value of an existing flag; never touch `.chant/allowed_signers` or `chant/lifecycle`.Optional: hand this page to your coding agent
Read https://intentius.io/chant/lexicon-authoring/docs-site/.For the lexicon I name, create `src/codegen/docs.ts` and `src/codegen/docs-cli.ts` as the page shows. Register `docs()` in `src/plugin.ts` and add the `docs` script. Write its prose pages under `docs/pages/` with a `diataxis` field on each.Run `npm run prepack` on a clean checkout, then `npm run docs` in the lexicon and `chant dev check-lexicon lexicons/<name>`, and open a pull request with the sources. Do not commit the generated pages.Never deploy to a real cloud account or shared environment (with `chant run`, `chant components` or a platform's own CLI), approve (a pull request review, `chant approve` or `chant run approve`), override a policy denial (`chant approve policy-override`), or merge; never cut or publish a release (`chant release cut --yes` or `chant release publish`), push or delete a `ci/*` tag (`chant ci tick` pushes them), or change the value of an existing flag; never touch `.chant/allowed_signers` or `chant/lifecycle`.The docs are built with Astro Starlight. Where they end up depends on where the package lives.
- In the chant repository they are a section of the main site: the pages are served at
/chant/lexicons/<name>/, the sidebar holds them in one collapsed group, and one search index covers them and the core docs (chant #3993). - Anywhere else, such as a package scaffolded by
chant init-lexiconin its own repository, they get a standalone Starlight site indocs/.
writeDocsSite (packages/core/src/codegen/docs.ts) picks between the two, so your docs.ts is the same either way. It looks for the chant repository’s docs/ above outDir; set mode: "in-site" or mode: "standalone" in the DocsConfig, or CHANT_DOCS_MODE, to choose explicitly.
The pipeline owns the output
Section titled “The pipeline owns the output”In the chant repository, writeDocsSite writes every page to
docs/src/content/docs/lexicons/<name>/ and, beside them, _sidebar.json: the
sidebar group, built by buildSectionGroup
(packages/core/src/codegen/docs-in-site.ts) from the pages’ diataxis
frontmatter. The main site’s astro.config.mjs reads every fragment through
docs/src/lexicon-sidebar.mjs. The whole directory is build output, gitignored
and replaced on every run, so an edit there is lost.
In standalone mode it generates docs/astro.config.mjs, docs/package.json,
docs/tsconfig.json, docs/src/content.config.ts,
docs/src/rehype-base-url.mjs, and every page under docs/src/content/docs/,
with the sidebar built by buildSidebar
(packages/core/src/codegen/docs-sidebar.ts). Editing any of them by hand loses
the edit on the next run.
What you write is two things: src/codegen/docs.ts, which builds a DocsConfig
and calls the pipeline, and your prose under docs/pages/*.mdx.
-
Create
src/codegen/docs.tswith agenerateDocsfunction.lexicons/k3s/src/codegen/docs.tsis the shortest one in the repo:import { dirname, join } from "path";import { fileURLToPath } from "url";import { docsPipeline, writeDocsSite, type DocsConfig } from "@intentius/chant/codegen/docs";const overview = `Prose for the generated index page, as markdown.`;export async function generateDocs(opts?: { verbose?: boolean }): Promise<void> {const pkgDir = dirname(dirname(dirname(fileURLToPath(import.meta.url))));const config: DocsConfig = {name: "my-lexicon",displayName: "My Lexicon",description: "What this lexicon targets",distDir: join(pkgDir, "dist"),outDir: join(pkgDir, "docs"),basePath: process.env.DOCS_BASE_PATH ?? "/chant/lexicons/my-lexicon/",overview,};const result = docsPipeline(config);writeDocsSite(config, result);if (opts?.verbose) console.error(`Generated ${result.pages.size} documentation pages`);}The other
DocsConfigfields are optional:packageJsonPath,outputFormat,serviceFromType,extraSections,suppressPages,srcDir,examplesDir,pagesDir(defaults to<outDir>/pages),modeandsiteDir.basePathmatters only to a standalone site. The full type is inpackages/core/src/codegen/docs-types.ts. -
Create
src/codegen/docs-cli.ts, three lines, identical in every lexicon:#!/usr/bin/env tsximport { generateDocs } from "./docs";await generateDocs({ verbose: true });k3d and k3s shipped for months with a
docsscript pointing at a file neither had, sonpm run docsfailed for both (chant #1815). Create it. -
Register the
docs()member insrc/plugin.ts.chant dev check-lexiconfails at tier 1 without it:async docs(options?: { verbose?: boolean }): Promise<void> {const { generateDocs } = await import("./codegen/docs");await generateDocs(options);}, -
Write your prose pages under
docs/pages/, then runnpm run docs -w @intentius/chant-lexicon-my-lexicon. The pipeline readsdist/manifest.jsonanddist/meta.json, so runnpm run prepackfirst on a clean checkout.
Where Pages Live
Section titled “Where Pages Live”A lexicon docs site has two kinds of page:
- Generated reference tables (
index,intrinsics,pseudo-parameters,rules,serialization), produced bydocsPipeline()from the packageddist/manifest.json,dist/meta.jsonand the rule files undersrc/. Never edit these; they carry aGENERATED-BY-CHANT-DOCSmarker and are rewritten on every run. - Authored pages under
docs/pages/*.mdx. The pipeline expands{{file:...}}markers, stamps the same marker (pointing back at the source file) and copies each one into the output:docs/src/content/docs/lexicons/<name>/of the main site, or the standalone site’sdocs/src/content/docs/. The copy is removed when the source is deleted.
Each authored page names its Diátaxis quadrant, and the sidebar is grouped from it. Only non-empty groups appear.
---title: "Getting Started"description: "First project with the my-lexicon lexicon"diataxis: tutorial---
Content goes here. Use {{file:examples/basic/src/main.ts}} to inline a filefrom the directory `examplesDir` points at.The quadrant labels the sidebar groups by are fixed: tutorial becomes
“Tutorials”, how-to becomes “How-to guides”, reference becomes “Reference”,
explanation becomes “Explanation” (packages/core/src/codegen/docs-pages.ts:35).
Frontmatter the pipeline reads, on top of Starlight’s title and description:
| Field | Required | Meaning |
|---|---|---|
diataxis | yes | tutorial, how-to, reference or explanation. The sidebar group. One mode per page; a page that needs two gets split |
label | no | Sidebar label; defaults to title |
group | no | A nested subgroup inside the quadrant, e.g. "Vendor Composites" |
order | no | Lower sorts first within its group; pages without it follow, by label |
hidden | no | true keeps the page out of the sidebar |
docsPipeline throws on an authored page whose frontmatter has no diataxis, or
one whose value is not a quadrant. chant dev check-lexicon gates the same
condition, plus reachability: a generated page that no sidebar entry names, and
that is not hidden, can only be reached by typing its URL.
A mature lexicon ships eight or more doc pages, which chant dev check-lexicon
warns about at tier 2. These ten are the set the shipped lexicons converged on,
all authored, all under docs/pages/.
| Page | File | Quadrant |
|---|---|---|
| Getting Started | getting-started.mdx | tutorial |
| Concepts | <product>-concepts.mdx | explanation |
| Resources | resources.mdx | reference |
| Composites | composites.mdx | reference |
| Parameters & Outputs | parameters-outputs.mdx | reference |
| Lint Rules | lint-rules.mdx | reference (the generated rules table is the complete list; this page explains the important ones) |
| Importing | importing.mdx | how-to |
| Examples | examples.mdx | how-to |
| Operational playbook | operational-playbook.mdx | how-to |
| AI Skills | skills.mdx | reference |
extraPages (prose in docs.ts template literals) and sidebarExtra (a
hand-listed content file) were removed in chant #1757. docs/pages/ is the only
way to add a prose page. scripts/extract-lexicon-doc-pages.ts <lexicon> lifts
old extraPages[].content literals out of a docs.ts into docs/pages/*.mdx if
you inherit one that predates the change.
Pages on the main site
Section titled “Pages on the main site”A page about lexicon authoring, the CLI, or a cross-cutting guide belongs on the
main site (docs/), not a lexicon site. That site’s sidebar is hand-maintained,
so getting a page to appear takes two edits:
- Add the file under
docs/src/content/docs/<section>/<slug>.mdxwithtitle,descriptionanddiataxisin its frontmatter. - Add a
{ label, slug: '<section>/<slug>' }entry todocs/astro.config.mjs, inside the group matching the page’s quadrant.
node scripts/check-docs-diataxis.mjs enforces both. It fails any page that has
no quadrant or sits in a sidebar group contradicting it, and any page that
appears in no group at all. Only index and whats-new are exempt. It checks
the generated sections too, once scripts/build-docs.sh has written them: each
of those pages has to carry the quadrant of the subgroup it sits in. The
docs-check workflow runs the script after the docs build.
Building the Docs
Section titled “Building the Docs”Regenerate before previewing; the preview server reads what the pipeline wrote.
# In the chant repository: regenerate your section of the main site, then# preview the main site.npm run docs -w @intentius/chant-lexicon-my-lexiconnpm run --prefix docs dev
# Or build the whole site, every lexicon included, and serve it../scripts/build-docs.sh && npx serve .docs-dist
# Outside the repository, a standalone site previews on its own.cd docs && npm install && npm run devWiring into Package Scripts
Section titled “Wiring into Package Scripts”Add a docs script to your lexicon’s package.json, pointing at the
docs-cli.ts from step 2:
{ "scripts": { "docs": "tsx src/codegen/docs-cli.ts" }}Links Between Pages
Section titled “Links Between Pages”scripts/build-docs.sh generates every section and then runs one Astro build of docs/, written to .docs-dist/chant/. It derives the list from the filesystem: any package with a src/codegen/docs-cli.ts is generated. It used to carry one hardcoded block per package. k3d and k3s were absent from that list, so both published as 404s and lychee failed every page linking to them (chant #1720). scripts/build-docs.test.ts now fails if a lexicon with docs is left out.
One package imports another’s generated surface when it is bundled (forgejo imports lexicons/github/src/generated), so the script runs forgejo after github and everything else at once. The comments in the script say how that was measured.
Astro’s base: '/chant' only prefixes its own sidebar and slug entries. Root-relative links written in MDX body content are not prefixed automatically.
A shared rehype-base-url plugin (packages/core/src/codegen/rehype-base-url.mjs) closes the gap. It walks the HAST tree and prepends a base to <a href> values that start with /. On the main site a generated page’s base is /chant/lexicons/<name>, the same as on a standalone site, so a root-relative link means the section’s own root wherever it is built. A standalone site gets a copy of the plugin in docs/src/rehype-base-url.mjs, wired in whenever basePath is set.
Two gotchas:
- Linking across namespaces from a lexicon page. Its base is
/chant/lexicons/<name>, so[guide](/guide/composite-resources/)inlexicons/aws/...gets prefixed to/chant/lexicons/aws/guide/composite-resources/, which is wrong. Write the full project path,/chant/guide/composite-resources/, and the plugin’s idempotency check (projectBase: '/chant') leaves it alone. - Sibling pages in MDX.
[foo](./foo)fromaks-composites.mdxresolves toaks-composites/foo, a child, not the siblingfoopage. Use../foo/, or the absolute/chant/...form.
just docs-check-links runs lychee against the built .docs-dist/chant/ tree and catches both classes of breakage.
Reference
Section titled “Reference”| Path | What it is |
|---|---|
packages/core/src/codegen/docs.ts | docsPipeline and writeDocsSite |
packages/core/src/codegen/docs-in-site.ts | the in-site mode: mode detection, the sidebar fragment, the standalone pins |
docs/src/lexicon-sidebar.mjs | reads every lexicon’s _sidebar.json for the main site’s sidebar |
packages/core/src/codegen/docs-types.ts | DocsConfig, DocsResult, SidebarPage, Quadrant |
packages/core/src/codegen/docs-pages.ts | authored-page parsing, quadrant labels |
packages/core/src/codegen/docs-sidebar.ts | sidebar generation for a standalone site |
lexicons/k3s/src/codegen/docs.ts | the smallest lexicon docs.ts |
lexicons/k8s/docs/pages/ | the largest set of authored pages |
scripts/build-docs.sh | generates every lexicon’s section, then builds the site |
scripts/check-docs-diataxis.mjs | the quadrant and reachability gate, core and lexicon pages |