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 auto-detects:

  • 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"

If zensical.toml is absent when engine = "zensical" is declared, Zenzic raises ConfigurationError immediately:

ConfigurationError: engine 'zensical' declared in zenzic.toml but zensical.toml is missing
hint: create zensical.toml or set engine = 'mkdocs' for MkDocs projects

There is no fallback. There is no silent degradation. Engine identity must be provable from the files on disk.

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.

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.

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.