Engine Configuration Guide
Zenzic is agnostic — it works with MkDocs, Zensical, or a bare folder of Markdown files without requiring any build framework to be installed. It is also opinionated: when you declare an engine, you must prove it. This guide explains how to configure Zenzic for each supported engine and what the rules are.
Cross-ecosystem reach
Zenzic is a Python tool, but its reach is not limited to the Python documentation ecosystem. Because Zenzic analyses source Markdown files and configuration as plain data — never invoking a build engine, never importing framework code — it can validate documentation for any static site generator (SSG), regardless of what language that generator is written in.
| Support level | Engine | SSG language | How |
|---|---|---|---|
| Native | MkDocs | Python | MkDocsAdapter — reads mkdocs.yml, resolves i18n, enforces nav |
| Native | Zensical | Python | ZensicalAdapter — reads zensical.toml, zero-YAML |
| Native | Docusaurus | Node.js | DocusaurusAdapter — reads docusaurus.config.ts, resolves i18n |
| Agnostic | Vanilla | any | VanillaAdapter — works on any Markdown folder; orphan check disabled |
| Extensible | Hugo (example) | Go | Third-party adapter via zenzic.adapters entry-point |
| Extensible | Jekyll (example) | Ruby | Third-party adapter via zenzic.adapters entry-point |
The "Extensible" entries are examples of what the adapter system enables — not shipped adapters. A team maintaining Hugo or Jekyll documentation can write a third-party adapter package and install it alongside Zenzic without any change to Zenzic itself:
# Example: third-party adapter for a hypothetical Hugo support package
uv pip install zenzic-hugo-adapter # or: pip install zenzic-hugo-adapter
zenzic check all --engine hugo
This cross-language reach is a structural property, not a roadmap promise. The Adapter
protocol defines five methods; any Python package that implements them and registers under
the zenzic.adapters entry-point group is a valid Zenzic adapter — for any SSG.
Choosing an engine
The [build_context] section in zenzic.toml tells Zenzic which engine your project uses:
# zenzic.toml
[build_context]
engine = "mkdocs" # or "zensical" or "docusaurus"
If [build_context] is absent entirely, Zenzic deterministically discovers the engine:
mkdocs.ymlpresent →MkDocsAdapterdocusaurus.config.ts(or.js) present →DocusaurusAdapter- neither config present, no locales declared →
VanillaAdapter(orphan check disabled)
Engine selection and Sentinel verbosity are independent concerns. Use CLI Commands: Global flags to tune policy per run:
--strictto elevate warnings and enforce external URL validation.--exit-zerofor non-blocking observation runs.--show-infoto inspect informational topology findings.--quietfor one-line CI/pre-commit output.
MkDocs
MkDocsAdapter is selected when engine = "mkdocs" (or any unrecognised engine string).
It reads mkdocs.yml using a permissive YAML loader that silently ignores unknown tags
(such as MkDocs !ENV interpolation), so environment-variable-heavy configs work without
any preprocessing.
Static analysis limits
MkDocsAdapter parses mkdocs.yml as static data. It does not execute the MkDocs
build pipeline. This means:
!ENVtags — silently treated asnull. If your nav relies on environment variable interpolation at build time, the nav entries that depend on those values will be absent from Zenzic's view.- Plugin-generated nav — plugins that mutate the nav at runtime (e.g.
mkdocs-awesome-pages,mkdocs-literate-nav) produce a navigation tree that Zenzic never sees. Pages included only by these plugins will be reported as orphans. - Macros —
mkdocs-macros-plugin(Jinja2 templates in Markdown) is not evaluated. Links inside macro expressions are not validated.
For projects that rely heavily on dynamic nav generation, add the plugin-generated paths to
excluded_dirs in zenzic.toml to suppress false orphan reports until a native adapter
is available.
Minimal configuration
# zenzic.toml
docs_dir = "docs"
[build_context]
engine = "mkdocs"
default_locale = "en"
locales = ["it", "fr"] # non-default locale directory names (folder mode)
When locales is empty, Zenzic falls back to reading locale information directly from the
mkdocs-static-i18n plugin block in mkdocs.yml — zero configuration required for most
projects.
i18n: Folder Mode
In Folder Mode (docs_structure: folder), each non-default locale lives in a top-level
directory under docs/:
docs/
index.md ← default locale
assets/
logo.png ← shared asset
it/
index.md ← Italian translation
Zenzic reads the languages list from mkdocs.yml to identify locale directories. Files
whose first path component is a locale directory are excluded from the orphan check — they
inherit their nav membership from the default-locale original.
When fallback_to_default: true is set, asset links from docs/it/index.md that resolve
to docs/it/assets/logo.png (absent) are automatically re-checked against docs/assets/logo.png,
mirroring the build engine's actual fallback behaviour.
# mkdocs.yml
plugins:
- i18n:
docs_structure: folder
fallback_to_default: true
languages:
- locale: en
default: true
build: true
- locale: it
build: true
Rule: If
fallback_to_default: trueis set, at least one language entry must havedefault: true. If none does, Zenzic raisesConfigurationErrorimmediately — it cannot determine the fallback target locale.
i18n: Suffix Mode
In Suffix Mode (docs_structure: suffix), translated files are siblings of the originals:
docs/
guide.md ← default locale
guide.it.md ← Italian translation (same directory depth)
assets/
logo.png ← same relative path from both files
Zenzic reads the non-default locale codes from mkdocs.yml and generates *.{locale}.md
exclusion patterns (e.g. *.it.md, *.fr.md). These files are excluded from the orphan check.
Only valid ISO 639-1 two-letter lowercase codes produce exclusion patterns. Version tags
(v1, v2), build tags (beta, rc1), three-letter codes, and BCP 47 region codes are
silently rejected — they do not produce false exclusions.
Zensical
ZensicalAdapter is selected when engine = "zensical". It reads zensical.toml natively
using Python's tomllib — zero YAML. No mkdocs.yml is read or required.
Native Enforcement
# zenzic.toml
[build_context]
engine = "zensical"
Transparent Proxy (Migration Bridge)
To facilitate a smooth migration from MkDocs to Zensical, the adapter implements a Transparent Proxy mode. If zensical.toml is absent but mkdocs.yml is present in the project root, the Zensical adapter will automatically use the MkDocs configuration as a bridge.
This allows you to test Zenzic with the Zensical engine even before you have fully committed to a native zensical.toml. When this happens, the Sentinel banner will notify you:
SENTINEL: Zensical engine active via mkdocs.yml compatibility bridge.
While this bridge allows Zenzic to run, Zensical itself may handle some MkDocs features differently. Use the bridge as a temporary safety net during migration, and aim for a native zensical.toml to achieve full parity.
zensical.toml nav format
Zenzic reads the [nav] section to determine which pages are declared:
# zensical.toml
[project]
site_name = "My Docs"
[nav]
nav = [
{title = "Home", file = "index.md"},
{title = "Tutorial", file = "tutorial.md"},
{title = "API", file = "reference/api.md"},
]
Files listed under file (relative to docs/) are the nav set. Any .md file under docs/
that is not in this set and is not a locale mirror is reported as an orphan.
Why Zensical eliminates i18n complexity
MkDocs i18n relies on a plugin (mkdocs-static-i18n) with its own YAML configuration,
docs_structure switches, fallback_to_default logic, and languages lists. Zensical
defines i18n semantics natively in zensical.toml without plugin indirection. The result:
- No YAML to parse for locale detection
- No
fallback_to_defaultambiguity - No "which plugin block applies?" heuristics
ConfigurationErroris impossible for misconfigured i18n — the TOML schema is explicit
When Zensical's i18n configuration is available in zensical.toml, ZensicalAdapter will
read it directly. Until then, locale topology is sourced from [build_context] in zenzic.toml.
Docusaurus
DocusaurusAdapter is selected when engine = "docusaurus" or when docusaurus.config.ts
(or .js) is detected in the project root. It reads the Docusaurus configuration as
plain text using pattern matching — no Node.js runtime, no npm install, no JavaScript
evaluation.
Source-only analysis
Docusaurus is a Node.js framework built on React. Zenzic does not import or execute any
Node.js code. Instead, the DocusaurusAdapter:
- Reads
docusaurus.config.tsas text and extracts structural data —i18n.locales,i18n.defaultLocale, docs pluginrouteBasePathandpath, and sidebar configuration. - Resolves locale directories under the standard Docusaurus i18n layout
(
i18n/{locale}/docusaurus-plugin-content-docs/current/). - Maps sidebars from
sidebars.tsorsidebars.jsusing text-level pattern matching to determine which files are declared in navigation.
Minimal configuration
# zenzic.toml
docs_dir = "docs"
[build_context]
engine = "docusaurus"
default_locale = "en"
locales = ["it", "fr"]
When locales is empty, Zenzic reads locale information from the i18n block in
docusaurus.config.ts.
Versioning support
Zenzic supports Docusaurus multi-version documentation out of the box. It identifies:
- Version list — read from
versions.jsonin the project root. - Versioned content — discovered under
versioned_docs/version-{version}/. - Versioned translations — discovered under
i18n/{locale}/docusaurus-plugin-content-docs/version-{version}/.
The Virtual Site Map automatically maps these paths to their correct canonical URLs, following Docusaurus's official versioning rules:
- Latest version (the first entry in
versions.json) maps to therouteBasePathroot — no version label in the URL.- Example:
versioned_docs/version-1.1.0/hello.mdwithversions.json = ["1.1.0", "1.0.0"]→/docs/hello/.
- Example:
- Older versions retain their version label in the URL.
- Example:
versioned_docs/version-1.0.0/hello.md→/docs/1.0.0/hello/.
- Example:
This matches Docusaurus's own behavior exactly, preventing false positive broken-link reports against latest-version pages.
Versioned routes are treated as Ghost Routes: they are always considered reachable because Docusaurus automatically generates navigation for versioned documentation trees.
i18n layout
Docusaurus stores translations in a deep directory structure:
docs/
index.mdx ← default locale
assets/
logo.png ← shared asset
i18n/
it/
docusaurus-plugin-content-docs/
current/
index.mdx ← Italian translation
The adapter identifies i18n/{locale}/docusaurus-plugin-content-docs/current/ as the
locale mirror root. Files under these paths are excluded from the orphan check — they
inherit nav membership from the default-locale original.
Frontmatter slug rules
Docusaurus allows overriding the canonical URL of any page via the slug: frontmatter key.
Zenzic applies the same rules as Docusaurus itself:
- Absolute slug (starts with
/): always prepended withrouteBasePath.slug: /bonjour+routeBasePath: docs→/docs/bonjour/.slug: /bonjour+routeBasePath: ''(docs at site root) →/bonjour/.
- Relative slug (no leading
/): replaces the last path segment only.slug: setupinguide/install.md→/docs/guide/setup/.
Smart file collapsing
Zenzic mirrors Docusaurus's isCategoryIndex logic: a file collapses into its parent
directory URL when its name (case-insensitive) is:
index— e.g.guides/index.md→/docs/guides/readme— e.g.guides/README.md→/docs/guides/- The parent folder's name — e.g.
Guides/Guides.md→/docs/Guides/
This prevents Zenzic from reporting broken links when authors use any of these three conventions to create category landing pages.
@site/ alias resolution
Docusaurus projects frequently use the @site/ alias in links to reference files
relative to the project root:
[Architecture diagram](@site/static/img/arch.png)
[Source code](@site/docs/internals/architecture.mdx)
Zenzic resolves @site/ to the repository root automatically. Links starting with
@site/docs/ are resolved against the docs root; all other @site/ paths are checked
against the repository root. This means Zenzic validates these links without triggering
false-positive PathTraversal errors.
To enable full @site/ resolution, set repo_root in your zenzic.toml
[build_context] section, or run zenzic check from the project root so Zenzic
can detect it automatically.
MDX support
Docusaurus uses MDX (.mdx) files natively. The adapter treats .mdx files identically
to .md files for scanning, link validation, and orphan checking.
Static analysis limits
- Dynamic sidebar plugins — sidebars generated dynamically via JavaScript at build-time produce navigation that Zenzic cannot observe statically. Pages included only by custom sidebar plugins will be reported as orphans.
docusaurus.config.tswith complex TypeScript — the adapter uses pattern matching, not full TypeScript evaluation. Configurations that compute values at module scope or import from external modules may not be fully parsed.- Blog and pages plugins —
DocusaurusAdaptercurrently focuses on the docs plugin. Content under/blog/or custom pages is not validated.
For dynamic sidebar cases, add the generated paths to excluded_dirs in zenzic.toml.
Absolute Link Prohibition
This rule applies to every engine, unconditionally.
Links that begin with / are a hard error in all engine modes:
{/* Rejected — absolute path breaks portability */}
[Download](/assets/guide.pdf)
{/* Correct — relative path survives any hosting prefix */}
[Download](/assets/guide.pdf)
A link to /assets/guide.pdf presupposes the site is served from the domain root. When
documentation is hosted at https://example.com/docs/, the browser resolves
/assets/guide.pdf to https://example.com/assets/guide.pdf — a 404. The fix is always
a relative path.
The check runs before any adapter logic — before nav parsing, before locale detection, before path resolution. It cannot be suppressed by engine configuration.
External URLs (https://..., http://...) are not affected.
Vanilla (no engine)
VanillaAdapter is returned when no engine config file is present and no locales are
declared. All adapter methods are no-ops:
is_locale_dir→ alwaysFalseresolve_asset→ alwaysNoneis_shadow_of_nav_page→ alwaysFalseget_nav_paths→frozenset()get_ignored_patterns→set()
find_orphans returns [] immediately — without a nav, there is no reference set to
compare against. Snippet, placeholder, link, and asset checks still run normally.
This means Zenzic works out of the box on any other Markdown-based system without producing false positives.