Markdown versions of all docs pages are available by appending .md to any docs URL.
Testing
Run the bundled harness against your own built site, and understand which specs apply.
Run the harness locally
The local-invocation pattern depends on the consumer repo’s setup. Two working examples today:
Pattern A — consumer ships Makefile targets that drive the harness
from a sibling clone. Recommended for repos where multiple developers
will run tests regularly. Targets are prefixed framework-test-* so they
don’t collide with the test-* namespace some consumers reserve for doc
tests (code blocks executed against a cluster):
# One-time: clone docs-theme-extras as a sibling
git clone https://github.com/solo-io/docs-theme-extras ../docs-theme-extras
cd <consumer-repo>
make framework-test-install # npm + Playwright browsers in the sibling
# Build the test fixture / site, then run a project
make framework-test # all projects (static, browser, cross-browser)
make framework-test-static # fastest loop — ~2s after Hugo build
make framework-test-browser # chromium only
make framework-test-cross-browser # chromium + firefox + webkit
make framework-test-content CONTENT_DIR=<product> # scope content scan to one product subtree (hubs)
# Override the sibling location if needed
make framework-test FRAMEWORK_EXTRAS_DIR=/abs/path/to/docs-theme-extrasThe sibling-clone pattern, the FRAMEWORK_EXTRAS_DIR override variable,
and the per-target signatures are conventions a Makefile-shipping consumer
opts into. See Makefile examples in the consumer repos for the full template.
Pattern B — consumer invokes the harness directly. Works for any consumer without Makefile scaffolding; useful as a starter or for ad-hoc runs:
# One-time: clone docs-theme-extras as a sibling and install
git clone https://github.com/solo-io/docs-theme-extras ../docs-theme-extras
cd ../docs-theme-extras && npm ci && npx playwright install --with-deps chromium
# Build the consumer site, then run the harness against it
cd <consumer-repo> && hugo --gc --minify
cd ../docs-theme-extras && \
DOCS_TEST_CONFIG=$(pwd)/../<consumer-repo>/.docs-test.toml \
npx playwright test --project=staticEither pattern resolves builtRoot from the consumer’s .docs-test.toml,
so any consumer can run the same harness against its own public/ once
the config is in place.
Why a consumer may run fewer specs than the module’s self-test
A consumer that imports the module will see strictly fewer Playwright tests
than make test-oss / make test-enterprise runs against the bundled
fixture. This is by design, not a bug — different specs need different
inputs, and a real product site can’t provide all of them.
Four mechanisms determine which specs run for a given .docs-test.toml:
1. Fixture-aware specs require /everything/ and /rebased/ pages
These specs were written against the bundled fixture, which contains a
/everything/ page (calls every shortcode the framework cares about) and
a /rebased/ page (alternate-render path). Each spec greps target.pages
for a URL ending in those segments; if neither match, the whole spec is
test.skip’d. Affected specs:
browser.spec.ts— tabs, mermaid SVG, copy-md script, theme toggle, console errorscross-browser.spec.ts— same checks across chromium/firefox/webkitpresence.spec.ts— DOM-structure assertionsauto-cards.spec.ts—<div class="hextra-cards">child assertionsgithub-shortcode.spec.ts—{{< github >}}rendered-body presenceversioning.spec.ts— needs/everything/to exist under each declared version
To opt in, declare [[pages]] entries in your .docs-test.toml whose URLs
end in /everything/ and /rebased/. Real product docs rarely have pages
that call every shortcode in one place, so most consumers leave these
specs off and rely on the module’s own self-test for fixture-dependent
checks.
2. [checks] table can disable specs explicitly
[checks]
codeBlockIntegrity = false # e.g. a consumer with a known backlog of fenced-block fragmentationAll checks default to enabled. Setting false skips that spec entirely.
2a. [crawl].maxFiles caps the browser crawl
[crawl]
maxFiles = 0 # 0 = unlimited; default 50Only the browser crawl (console-errors.spec.ts, the browser-crawl
project) is capped — opening pages in Chromium is expensive, so it samples 50
by default. Set maxFiles = 0 to open every built page. The cheap file-read
scans (content project) always walk every page regardless.
For the consumer’s own build the static project already crawls
everything, so the default cap is the right choice.
2b. [[gateAxes]] enables gate-axis-collision.spec
[[gateAxes]]
name = "oss site"
condition = "kubernetes" # url mode: the condition IS the section
sections = []
[[gateAxes]]
name = "docs-hub / agentgateway"
condition = "agentgateway" # siteParams mode: a product id
sections = ["kubernetes", "standalone"]conditional-text gates on two axes — the build condition and the page’s
section segment — through one token namespace, so a token that names a section
on one axis and a product on the other is true twice and both sides of an
intended either/or render. Each entry describes one build the same source
tree ships through; the lint reports a gate pair only when some
(condition, section) combination fires both, through different axes.
List the downstream build as well as your own, because that is the point of the check: your build renders correctly and the breakage appears only in the other one.
Two ways to get sections wrong, both of which produce noise rather than
silence, so they show up the first time you run it:
- A
url-mode build takessections = []. There the condition IS the section, so the sections are enumerated by having one entry per section. Give such an entry a section list as well and you ask the lint about states that cannot exist — conditionkuberneteswith sectionstandalone— and every legitimate section pair reports. - List only sections that resolve for that build, not every key registered
or inherited. A hub that inherits section keys from an imported module but
serves those pages at
/<product>/<version>/resolves none of them, becauseutils/section-segment.html’s positional rule refuses them; the effective list is[].
With no [[gateAxes]] the spec skips rather than passes — with no
combinations there is nothing to evaluate against, and a green run would be a
false all-clear. Needs scanRoots too, since it reads source, not built HTML.
3. buildLog enables hugo-warnings.spec
buildLog = "./.build.log" # path the harness reads for Hugo warningsThe Hugo-warnings spec parses the build log for unallowlisted warnings.
If buildLog isn’t set, the spec skips. The consumer’s make/CI must
arrange for Hugo to write the log at that path (hugo > .build.log 2>&1).
4. scanRoots controls source-side lints
scanRoots = ["./content/docs"]Source-tree lints (currently curl-quotes.spec.ts) walk every *.md
under each listed root. An empty or omitted list means the lint skips —
useful when a corpus has too many pre-existing violations to ratchet
in one go.
What still runs without any of the above
Specs that operate on the whole built tree by crawling builtRoot work
for every consumer regardless of [[pages]]:
static.spec.ts— shortcode delimiter leaks, raw markdown bleed, copy-as-md script presence, image alt textcontrast.spec.ts/viewport.spec.ts(mostly) — sampled across crawled pages
So the floor for any consumer is structural HTML quality. Fixture-driven interactive checks are bonus coverage that only the module’s self-test (or a consumer that ships a synthetic fixture page) can provide.