Skip to main content

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 levelEngineSSG languageHow
NativeMkDocsPythonMkDocsAdapter — reads mkdocs.yml, resolves i18n, enforces nav
NativeZensicalPythonZensicalAdapter — reads zensical.toml, zero-YAML
NativeDocusaurusNode.jsDocusaurusAdapter — reads docusaurus.config.ts, resolves i18n
AgnosticVanillaanyVanillaAdapter — works on any Markdown folder; orphan check disabled
ExtensibleHugo (example)GoThird-party adapter via zenzic.adapters entry-point
ExtensibleJekyll (example)RubyThird-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.yml present → MkDocsAdapter
  • docusaurus.config.ts (or .js) present → DocusaurusAdapter
  • neither config present, no locales declared → VanillaAdapter (orphan check disabled)
CLI bridge — Signal-to-noise controls

Engine selection and Sentinel verbosity are independent concerns. Use CLI Commands: Global flags to tune policy per run:

  1. --strict to elevate warnings and enforce external URL validation.
  2. --exit-zero for non-blocking observation runs.
  3. --show-info to inspect informational topology findings.
  4. --quiet for 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:

  • !ENV tags — silently treated as null. 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.
  • Macrosmkdocs-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: true is set, at least one language entry must have default: true. If none does, Zenzic raises ConfigurationError immediately — 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 tomllibzero 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.
Structural Custodian Rule

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_default ambiguity
  • No "which plugin block applies?" heuristics
  • ConfigurationError is 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:

  1. Reads docusaurus.config.ts as text and extracts structural data — i18n.locales, i18n.defaultLocale, docs plugin routeBasePath and path, and sidebar configuration.
  2. Resolves locale directories under the standard Docusaurus i18n layout (i18n/{locale}/docusaurus-plugin-content-docs/current/).
  3. Maps sidebars from sidebars.ts or sidebars.js using 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:

  1. Version list — read from versions.json in the project root.
  2. Versioned content — discovered under versioned_docs/version-{version}/.
  3. 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 the routeBasePath root — no version label in the URL.
    • Example: versioned_docs/version-1.1.0/hello.md with versions.json = ["1.1.0", "1.0.0"]/docs/hello/.
  • Older versions retain their version label in the URL.
    • Example: versioned_docs/version-1.0.0/hello.md/docs/1.0.0/hello/.

This matches Docusaurus's own behavior exactly, preventing false positive broken-link reports against latest-version pages.

Ghost Routing

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 with routeBasePath.
    • 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: setup in guide/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.

note

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.ts with 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 pluginsDocusaurusAdapter currently 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.


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 → always False
  • resolve_asset → always None
  • is_shadow_of_nav_page → always False
  • get_nav_pathsfrozenset()
  • get_ignored_patternsset()

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.