Skip to content

Docs Site Setup

llms.txt lists every page for an agent
Optional: hand this page to your coding agentThe steps work by hand too.
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`.

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-lexicon in its own repository, they get a standalone Starlight site in docs/.

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.

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.

  1. Create src/codegen/docs.ts with a generateDocs function. lexicons/k3s/src/codegen/docs.ts is 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 DocsConfig fields are optional: packageJsonPath, outputFormat, serviceFromType, extraSections, suppressPages, srcDir, examplesDir, pagesDir (defaults to <outDir>/pages), mode and siteDir. basePath matters only to a standalone site. The full type is in packages/core/src/codegen/docs-types.ts.

  2. Create src/codegen/docs-cli.ts, three lines, identical in every lexicon:

    #!/usr/bin/env tsx
    import { generateDocs } from "./docs";
    await generateDocs({ verbose: true });

    k3d and k3s shipped for months with a docs script pointing at a file neither had, so npm run docs failed for both (chant #1815). Create it.

  3. Register the docs() member in src/plugin.ts. chant dev check-lexicon fails at tier 1 without it:

    async docs(options?: { verbose?: boolean }): Promise<void> {
    const { generateDocs } = await import("./codegen/docs");
    await generateDocs(options);
    },
  4. Write your prose pages under docs/pages/, then run npm run docs -w @intentius/chant-lexicon-my-lexicon. The pipeline reads dist/manifest.json and dist/meta.json, so run npm run prepack first on a clean checkout.

A lexicon docs site has two kinds of page:

  • Generated reference tables (index, intrinsics, pseudo-parameters, rules, serialization), produced by docsPipeline() from the packaged dist/manifest.json, dist/meta.json and the rule files under src/. Never edit these; they carry a GENERATED-BY-CHANT-DOCS marker 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’s docs/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.

docs/pages/getting-started.mdx
---
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 file
from 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:

FieldRequiredMeaning
diataxisyestutorial, how-to, reference or explanation. The sidebar group. One mode per page; a page that needs two gets split
labelnoSidebar label; defaults to title
groupnoA nested subgroup inside the quadrant, e.g. "Vendor Composites"
ordernoLower sorts first within its group; pages without it follow, by label
hiddennotrue 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/.

PageFileQuadrant
Getting Startedgetting-started.mdxtutorial
Concepts<product>-concepts.mdxexplanation
Resourcesresources.mdxreference
Compositescomposites.mdxreference
Parameters & Outputsparameters-outputs.mdxreference
Lint Ruleslint-rules.mdxreference (the generated rules table is the complete list; this page explains the important ones)
Importingimporting.mdxhow-to
Examplesexamples.mdxhow-to
Operational playbookoperational-playbook.mdxhow-to
AI Skillsskills.mdxreference

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.

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:

  1. Add the file under docs/src/content/docs/<section>/<slug>.mdx with title, description and diataxis in its frontmatter.
  2. Add a { label, slug: '<section>/<slug>' } entry to docs/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.

Regenerate before previewing; the preview server reads what the pipeline wrote.

Terminal window
# In the chant repository: regenerate your section of the main site, then
# preview the main site.
npm run docs -w @intentius/chant-lexicon-my-lexicon
npm 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 dev

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"
}
}

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/) in lexicons/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) from aks-composites.mdx resolves to aks-composites/foo, a child, not the sibling foo page. 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.

PathWhat it is
packages/core/src/codegen/docs.tsdocsPipeline and writeDocsSite
packages/core/src/codegen/docs-in-site.tsthe in-site mode: mode detection, the sidebar fragment, the standalone pins
docs/src/lexicon-sidebar.mjsreads every lexicon’s _sidebar.json for the main site’s sidebar
packages/core/src/codegen/docs-types.tsDocsConfig, DocsResult, SidebarPage, Quadrant
packages/core/src/codegen/docs-pages.tsauthored-page parsing, quadrant labels
packages/core/src/codegen/docs-sidebar.tssidebar generation for a standalone site
lexicons/k3s/src/codegen/docs.tsthe smallest lexicon docs.ts
lexicons/k8s/docs/pages/the largest set of authored pages
scripts/build-docs.shgenerates every lexicon’s section, then builds the site
scripts/check-docs-diataxis.mjsthe quadrant and reachability gate, core and lexicon pages

chant