Docs Site Setup
Each lexicon includes a documentation site built with Astro Starlight, living in lexicons/<name>/docs/. All seventeen lexicons have one.
The pipeline owns the site files
Section titled “The pipeline owns the site files”writeDocsSite (packages/core/src/codegen/docs.ts:289) 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/. Editing any of them by hand loses the edit on the
next run. The sidebar in particular is built by buildSidebar
(packages/core/src/codegen/docs-sidebar.ts) from the pages’ diataxis
frontmatter; there is no hand-maintained list.
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, andpagesDir(defaults to<outDir>/pages). 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 intodocs/src/content/docs/. The copy is reaped 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 page sitting in docs/src/content/docs/ that no
sidebar entry names can only be reached by typing its URL, and the pipeline warns
about each one by name.
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. The
docs-check workflow runs the script on every push.
Building the Docs
Section titled “Building the Docs”Regenerate before previewing; the preview server reads what the pipeline wrote.
# Regenerate the site from the pipeline (do this after every content change).npm run docs -w @intentius/chant-lexicon-my-lexicon
# Then preview it.cd lexicons/my-lexicon/docs && npm install && npm run dev
# Or build the whole unified tree and serve it../scripts/build-docs.sh && npx serve .docs-distWiring 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" }}Cross-Site Links
Section titled “Cross-Site Links”The 18 docs sites (main plus 17 lexicons) build independently and stitch into a single tree at .docs-dist/chant/. scripts/build-docs.sh derives the lexicon list from the filesystem, so any directory with a docs/ subdirectory is built. It used to carry one hardcoded block per lexicon. k3d and k3s were simply 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.
The ORDER constant in that script is a known-good build order, not decoration: forgejo and gitlab import lexicons/github/src/generated, so github must be generated first. A lexicon not named in ORDER is appended, which is where a new one lands.
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 closes the gap. It walks the HAST tree and prepends the site’s base to <a href> values that start with /. writeDocsSite copies it from packages/core/src/codegen/rehype-base-url.mjs to docs/src/rehype-base-url.mjs and wires it into the generated astro.config.mjs whenever basePath is set, so a new lexicon gets it for free. Setting basePath is what turns it on.
Two gotchas:
- Linking across namespaces from a lexicon page. A lexicon site’s 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-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 |
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 | the unified-site build |
scripts/check-docs-diataxis.mjs | the main site’s quadrant and reachability gate |