Docusaurus site contract
This repository validates its Docusaurus site against a source-level contract before docs builds, broader lint workflows, and release verification.
The goal is to stop template drift early.
Instead of relying on maintainers to remember a long checklist of nav items, preset pages, inspector links, favicons, sidebar hooks, hero content, and docs workspace scripts, the repository keeps those expectations in one contract file and validates them automatically.
Why this existsβ
A lot of the painful Docusaurus regressions in plugin templates are not single-file lint problems.
They are usually cross-file site-contract problems, such as:
- navbar order drifting away from the intended UX
- preset pages existing but not being linked from the sidebar or navbar
- logo, favicon, or manifest assets going stale
- sidebar styling hooks getting renamed without updating CSS
- docs workspace build scripts forgetting required prebuild steps
- inspector links or hero cards disappearing during refactors
That is why this feature is implemented as a dedicated validator script instead of an ESLint rule.
In other words: this is not an ESLint rule.
What it validatesβ
The current validator supports both structured Docusaurus config checks and generic source-file checks.
Structured checksβ
These are the higher-confidence checks that inspect the actual Docusaurus config shape:
- required top-level config properties
- required plugins and themes
- client modules
- navbar items, order, positions, dropdown contents, and logo presence
- footer columns, titles, required links, item-count balance, and logo presence
- local search plugin options
- favicon and theme image presence
File and source checksβ
These are repo-agnostic text and filesystem checks:
- required files exist
- manifest fields and icon files exist
- package scripts include required commands
- source files include required snippets
- source files forbid legacy snippets
- source files require regex patterns
- source files forbid regex patterns
- source snippets appear in a required order
- source regex matches appear in a required order
CLI usageβ
From the repository root:
npm run docs:check-site-contract
You can also invoke the CLI directly:
node packages/docusaurus-site-contract/cli.mjs --config docs/docusaurus/site-contract.config.mjs
Validate a different contract file relative to a target repository root:
node packages/docusaurus-site-contract/cli.mjs \
--root ../your-plugin-repo \
--config docs/docusaurus/site-contract.config.mjs
Emit machine-readable JSON for CI or editor tooling:
node packages/docusaurus-site-contract/cli.mjs --json --config docs/docusaurus/site-contract.config.mjs
Show built-in help:
node packages/docusaurus-site-contract/cli.mjs --help
Core filesβ
The feature is split into two real layers plus one repo-local contract:
packages/docusaurus-site-contract/index.mjs
- Generic validation engine.
- Internal workspace package source of truth.
packages/docusaurus-site-contract/cli.mjs
- Workspace-owned CLI entrypoint.
- Supports
validatebehavior by default plus theinitsubcommand. - Supports
--help,--json,--root, and--config.
docs/docusaurus/site-contract.config.mjs- Repository-local blueprint.
- Encodes this repository's actual Docusaurus expectations.
How to reuse this without copying four filesβ
Yes, there is an easier way.
The current repository layout no longer needs a copied scripts/ layer.
The intended local-private model is:
- generic engine
- package CLI
- repo-local contract file
That means the minimum durable setup is now:
- the local private package under
packages/docusaurus-site-contract - one repo-local contract file under
docs/docusaurus/ - root/docs
package.jsonwiring
If you are working inside a repo copied from this template, the package should already be present.
If you are retrofitting an existing repo, use the init command to vendor and wire it automatically.
Using init in projectsβ
If the target repository already contains the vendored package:
node packages/docusaurus-site-contract/cli.mjs init --root . --skip-vendor-package
If the target repository installs the package from npm or links it locally instead:
docusaurus-site-contract init --root . --skip-vendor-package
If you are bootstrapping a different ESLint-plugin repo from this template repository:
node packages/docusaurus-site-contract/cli.mjs init --root ../your-eslint-plugin-repo
Or via the root script this template now exposes:
npm run docs:site-contract:init
Preview the bootstrap without mutating files:
node packages/docusaurus-site-contract/cli.mjs init --root . --skip-vendor-package --dry-run --json
By default, init will:
- vendor the private package into
packages/docusaurus-site-contract - add
docs:check-site-contractto the rootpackage.json - add
docs:site-contract:initto the rootpackage.json - patch
docs/docusaurus/package.jsonbuild scripts to run the check first - generate
docs/docusaurus/site-contract.config.mjs - generate
docs/docusaurus/site-docs/developer/docusaurus-site-contract.md - register the guide in
docs/docusaurus/sidebars.tswhen the developer sidebar follows recognizable template structure - add guide links to
docs/docusaurus/site-docs/developer/index.mdwhen that page uses recognizable headings
If those docs surfaces are heavily customized, init leaves them alone and you can wire the guide manually.
Do I need to update the generated config before I run the script?β
Usually: yes, at least a little.
The config written by init is intentionally a starter contract, not a perfect final one.
It now starts from the conservative side so that a typical ESLint-plugin docs site can adopt it without immediately failing on repo-specific choices that have not been decided yet.
That is important because sibling plugin docs sites already vary in real ways.
For example:
eslint-plugin-typefestusesminimal,recommended,recommended-type-checked,strict,all, and focused family presets.eslint-plugin-immutable-2usesfunctional-lite,functional,immutable,recommended, andall.eslint-plugin-copilotkeeps a simplerminimal/recommended/strict/allstyle and different developer/footer copy.
That means a new repo should not blindly inherit this repository's exact preset names, footer titles, hero text, or sidebar labels.
Recommended processβ
- Run
init. - Open
docs/docusaurus/site-contract.config.mjs. - Adjust the assumptions that are repo-specific:
- preset names
- navbar labels
- footer section titles
- package-specific badges and inspector links
- optional search-plugin and manifest rules
- any stricter source snippets that should be unique to the target repo
- Then run
npm run docs:check-site-contract.
If you skip that review step, the contract may be too strict for the new repo for the wrong reasons.
What should stay optional in a new repo?β
Treat these as optional until the target repo confirms them:
- exact preset names
- exact footer column names
- exact hero phrasing
- exact developer sidebar wording
- exact upstream package links
Treat these as strong default candidates:
- having a Docusaurus config file
- having a favicon / theme image / manifest
- wiring a docs-site contract check into docs builds
- requiring generated assets and key docs entry files to exist
- validating basic navbar/footer structure once the repo settles on names
Useful flags:
node packages/docusaurus-site-contract/cli.mjs init \
--root . \
--force \
--owner acme \
--repo eslint-plugin-example \
--package-name eslint-plugin-example
--forceoverwrites generated init files--owneroverrides detected GitHub owner--repooverrides detected repository name--package-nameoverrides detected npm package name--dry-runpreviews changes without writing them--skip-docs-guideskips generating the maintainer guide page--skip-docs-registrationskips patching the sidebar and developer index--skip-vendor-packageassumes the package already exists in the target repo
This is the private local path I would recommend for template-derived repos.
Published npm backupβ
For safekeeping, the package has also been published as:
docusaurus-site-contract@0.1.0
That published package should be treated as a backup/distribution artifact, not the primary maintenance model for this template.
The preferred workflow for template-derived repos is still:
- keep the package vendored locally under
packages/docusaurus-site-contract - use
initto scaffold and wire new repos - customize the generated starter contract before enforcing repo-specific naming
If you ever wanted to publish it anywayβ
The cleaner public-package distribution model would be:
- publish the engine + CLI as a small dev-only package
- keep only one repo-local contract file in the consumer repo
- optionally provide an
initcommand that patches scripts automatically
Recommended consumer experienceβ
In the target repo, the ideal setup should be:
npm i -D @your-scope/docusaurus-site-contract
Then add one small contract file:
import { defineDocusaurusSiteContract } from "@your-scope/docusaurus-site-contract";
export default defineDocusaurusSiteContract({
docusaurusConfig: {
path: "docs/docusaurus/docusaurus.config.ts",
requireFavicon: true,
},
});
Then add one root script:
{
"scripts": {
"docs:check-site-contract": "docusaurus-site-contract --config docs/docusaurus/site-contract.config.mjs"
}
}
That means the consumer repo does not copy the engine, CLI, or local declaration files.
The public-package version of init would look like:
npx @your-scope/docusaurus-site-contract init \
--owner acme \
--repo eslint-plugin-example \
--package eslint-plugin-example
That command should:
- create
docs/docusaurus/site-contract.config.mjs - add
docs:check-site-contractto the rootpackage.json - optionally patch
docs/docusaurus/package.jsonbuild scripts to run the check beforedocusaurus build - print follow-up instructions instead of silently mutating unrelated files
If that ever exists, reuse becomes a one-command setup instead of a multi-file copy exercise.
Why I would still not make this a Docusaurus pluginβ
I would package it as a CLI/tooling package, not as a normal Docusaurus runtime plugin.
Reason:
- some checks are outside
docusaurus.config.ts - some checks inspect arbitrary source files and docs pages
- some checks validate
package.jsonscripts and manifest assets - some checks should run before docs builds and in CI, not only inside a Docusaurus plugin lifecycle
So the right reuse model is:
- package = CLI + validation library
- consumer repo = one contract file + one script
Not:
- consumer repo copies engine files into
scripts/ - or consumer repo tries to force everything through a runtime plugin hook
Contract anatomyβ
A contract file exports either default or siteContract.
export default {
docusaurusConfig: {
path: "docs/docusaurus/docusaurus.config.ts",
requiredPluginNames: ["@docusaurus/plugin-pwa"],
requiredThemeNames: ["@easyops-cn/docusaurus-search-local"],
requireFavicon: true,
navbar: {
requireLogo: true,
orderedItems: [
{
labelPattern: /Docs/v,
position: "left",
type: "dropdown",
},
],
},
},
manifestFiles: [
{
path: "docs/docusaurus/static/manifest.json",
requiredFields: {
name: "Your docs site",
},
requireExistingIconFiles: true,
},
],
packageJsonFiles: [
{
path: "docs/docusaurus/package.json",
requiredScripts: [
{
name: "build",
includes: "docs:check-site-contract",
},
],
},
],
requiredFiles: [
"docs/docusaurus/static/img/logo.svg",
],
sourceFiles: [
{
path: "docs/docusaurus/src/js/modernEnhancements.ts",
requiredSnippets: [
'window.addEventListener("load", handleWindowLoad, { once: true });',
],
forbiddenSnippets: [
'document.addEventListener("DOMContentLoaded", handleDOMContentLoaded);',
],
orderedPatterns: [
{
description: "load bootstrap before export",
pattern: /handleWindowLoad/v,
},
{
description: "global assignment after bootstrap",
pattern: /window\.initializeAdvancedFeatures/v,
},
],
},
],
};
docusaurusConfigβ
Use this for structural assertions that should come from the parsed config rather than string matching.
This is the right place for things like:
- navbar order
- footer balance
- required search plugin options
- required plugin/theme names
- favicon presence
manifestFilesβ
Use this for PWA/app metadata and icon integrity.
This is the right place for things like:
- required
nameandshort_name - minimum number of icons
- ensuring declared icon files actually exist
packageJsonFilesβ
Use this when the docs site relies on scripts remaining wired together.
Examples:
- inspector prebuild scripts
- contract check integration before
docusaurus build - required docs workspace commands
requiredFilesβ
Use this for assets or docs pages that must always exist.
Examples:
- logo and favicon assets
- preset pages
- site entry files
- maintainer docs pages
sourceFilesβ
Use this for text-level invariants.
Supported checks:
requiredSnippetsforbiddenSnippetsorderedSnippetsrequiredPatternsforbiddenPatternsorderedPatterns
Prefer structured checks when you can. Use source-text checks when the value is inherently file-local or intentionally literal.
Repo-agnostic adoption checklistβ
When adopting this validator in another ESLint plugin repo:
- Use
initinstead of manually copying engine files whenever possible. - Create or review the repo-local contract file instead of editing the engine.
- Update paths, asset names, package scripts, and navbar/footer expectations.
- Keep the starter contract conservative until the docs UX settles.
- Add at least one passing test against the real repo and one failing fixture test.
- Wire the check into docs build and release validation.
The engine should stay generic. Repository identity belongs in the contract file, not in the validator core.
When to use source patterns vs structured config checksβ
Use structured config checks when the invariant is semantic and can be parsed reliably.
Good examples:
- βthe navbar must include GitHub, Dev, and Blog in that orderβ
- βthe footer must define three columns with balanced item countsβ
- βthe search plugin must keep the search bar on the leftβ
Use source-text checks when the invariant is intentionally literal or spans non-config files.
Good examples:
- βthis CSS class hook must exist for sidebar stylingβ
- βthis page must keep the hero card gridβ
- βthis runtime enhancement must bootstrap from
load, notDOMContentLoadedβ
What not to encode hereβ
Do not try to force everything into this validator.
This contract is good for stable structure and source-level conventions. It is not the right tool for:
- pixel-perfect layout guarantees
- rendered DOM slot order that only exists after theme composition
- animation details that need browser assertions
- subjective style preferences with high churn
If you need to enforce things like exact theme-toggle placement relative to search and custom navbar actions, use a swizzled component plus browser-level tests.
Workflow integration in this repoβ
This repository currently runs the contract through:
npm run docs:check-site-contract- docs workspace build commands
- broader lint-all workflows
- release verification
That means a drifted docs site should fail long before publish time.