Guide
Customizing
The ladder from config values to replaced files, what each rung costs in upgrades, and how to use theme tokens, static files, eject and the jx fragment.
On this page
Every Avunu documentation site looks the same on purpose. When a project needs something different, use the cheapest rung of the ladder that works. Each rung below the first costs a little of the project's ability to follow package updates, and the system makes that cost visible instead of hiding it.
The ladder
| Rung | Mechanism | Follows package updates | Guard rails | Semver |
|---|---|---|---|---|
| 0 | The seven identity keys, and the optional branch, docs and images | Yes | Schema, validateConfig, preflight | Covered |
| 1 | theme.light and theme.dark token overrides in the config | Yes (token names are semver surface) | An unknown token is an error; check re-runs the WCAG contrast pairs on the result | Covered |
| 2 | Files in docs-site/public/: favicon, logo, og image, other static files | Yes | Each shadowed package file is printed and recorded; CNAME may not be supplied | Covered |
| 3 | Files in docs-site/overrides/{components,layouts,pages}/: replace or add a Jx file | No, for replaced files | Printed on every build and recorded in the manifest; doctor reports drift | Not covered: file names are, contents are not |
| 4 | A jx fragment in the config | No | Merge warnings are printed and recorded | Not covered |
| 5 | A change to site/ in Avunu/docusystem, by pull request | Yes, for everyone | Review and the fleet job | Covered |
Rung 5 is the right answer when the change would help other projects too. See CONTRIBUTING.md.
Rung 1: theme tokens
Override design tokens by custom-property name, for the light scheme, the dark scheme or both:
{
"theme": {
"light": { "--color-action": "#4B2A99" },
"dark": { "--color-action": "#CBB8FF" }
}
}Each token name must exist in the installed package. The tokens are the
stylekeys that start with--insite/project.base.jsoninside the package (forlight) and the keys of itsstyle["@--dark"]object (fordark). An unknown name is an error that names it.Values are plain CSS. The schema forbids
;, braces, angle brackets and backslashes, andvalidateConfigalso refusesurl(and@import.docusystem checkresolves the effective tokens and runs the WCAG contrast pairs on the result. A value that drops a pair below its minimum fails the check: for example#C9B8FFas a light-scheme--color-actionfails at 1.78<1>1>, while#4B2A99passes. Contrast failures are never downgraded, not even by--lenient.
The package's other design rules (one style object with @--dark overrides, light and dark by prefers-color-scheme and the data-color-scheme toggle contract, the 46rem prose measure, components prefixed docs-) are internal. Only token names and file and tag names are covered by semver.
Rung 2: static files
Put a file in docs-site/public/ and it is published at the same path. Package files of the same name lose, and each one that is shadowed is printed on every build and recorded in the manifest.
docs-site/
public/
favicon.svg
brand/logo.svg
og.pngCNAME is generated from domain and may not be supplied: a public/CNAME is an error. Symbolic links are followed only inside the repository (see the symlink policy).
Rung 3: replacing a file
Put a Jx file at the same relative path in docs-site/overrides/components/, layouts/ or pages/ and it replaces the package's file. A path the package does not ship is an addition: a new page or layout is allowed.
The safe way to start is eject, which copies the package's current file into overrides/ and records where it came from. Run it in docs-site/:
npx @avunu/docusystem eject components/docs-footer.json
npx @avunu/docusystem eject --alleject takes one or more components/<file>, layouts/<file> or pages/<file> arguments, or --all. It exits 1 for an unknown file or when an override already exists; --force overwrites. It records each file in overrides/.ejected.json:
{
"components/docs-footer.json": { "from": "0.1.0", "sha256": "<SHA256_OF_THE_PACKAGE_FILE>" }
}Rules:
Precedence, lowest to highest: the package's
site/, thenoverrides/, then the generated files (project.json,docusystem.config.json,CNAME, the data files and the staged docs). Withinpublic/: the package's, then yours, thenCNAME.Granularity is the file name. An override replaces the package file of the same relative path as a whole.
Components are flat. Jx registers only
<root>/components/*.json, sooverrides/components/must be flat: a nested file is an error. A new component that should also be a Markdown directive needs thecontent.docs.$elementslist restated through thejxfragment (an array, so it replaces the package's list and prints the warning below). That path has not been exercised yet.Overrides are visible. Every override and addition is printed on every build, recorded in
.docusystem/manifest.json(files,shadowed,added) and outside semver.Drift is reported.
docusystem doctorcompares each ejected file with the installed package:current,changed(the package's file changed since the eject: "copied from 0.1.0, package file changed"),stale(the package no longer ships the file, an error) ornot ejected(an override of a package file made by hand, which cannot be drift-checked). To take the package's newer file, move your override aside, runejectagain, and re-apply your changes to the fresh copy.
An overridden file no longer follows package updates. A design change, a new token or an accessibility fix in the package's version of that file does not reach your site until you merge it yourself.
Rung 4: the jx fragment
The config's jx object is merged into the generated Jx project.json after the theme. It is an unsupported escape hatch, outside semver:
{
"jx": {
"$head": [
{
"tagName": "meta",
"attributes": { "name": "google-site-verification", "content": "TOKEN-FROM-SEARCH-CONSOLE" }
}
]
}
}The merge rules:
Objects merge key by key.
nulldeletes a key.The array at
$headis appended to the package's, package entries first.Every other array replaces the package's, and the build prints
overrides: jx.<path> replaces N entries of the package's list. That is a warning, and it is listed in the manifest.The fragment cannot override
name,urlorcontent.docs.sourcewithout a warning that says so.
The fragment's $head entries are the site-wide level of each page's <head>, and the layout and the page are merged over them: an entry whose key matches a tag the layout sets (a meta with the same name or property, such as description, robots or an og: or twitter: tag) builds and passes check, but the layout's tag is the one in the page. Add tags the package does not set, such as the verification tag above or an extra link. The configuration reference lists the keys the layout takes.
Every build lists the fragment with the overrides (the overrides: lines and the closing build: overrides: summary), as do docusystem info and the CI job summary, and docusystem doctor warns about it.
Not possible on purpose
A different design language (fork the package or make a new one), a per-project structure of the shared header and footer, per-project build scripts or post-build steps, a different route scheme and a different font set. These would make the sites diverge, which is what the system exists to prevent.
Images
images defaults to "optimize", which keeps WebP and AVIF output and needs the native sharp module. Set "images": "off" to skip raster-image optimization and sharp entirely, for example on NixOS (see troubleshooting).