<--- SPDX-FileCopyrightText: 2026 PythonWoods dev@pythonwoods.dev → <--- SPDX-License-Identifier: Apache-2.0 →
Why We Dropped Docusaurus: The Ontological Limits of Static Analysis¶
Zenzic Engineering · June 13, 2026
We spent a full development cycle building a Docusaurus adapter for Zenzic. We ran forensic audits on real Docusaurus projects. Then we deleted every line of it.
This is the story of why — and why we think it was the right call.
What Zenzic Does¶
Zenzic is a static documentation linter. It parses Markdown and reStructuredText source files, builds a Virtual Site Map of every page and anchor in your project, and validates that every internal link resolves to a real target. When a link is broken, Zenzic tells you exactly where, why, and what the Document Quality Score penalty is.
The operative word is static. Zenzic reads source files. It does not render HTML. It does not execute JavaScript. It does not run a bundler.
This is a deliberate architectural constraint, not a limitation we plan to engineer around.
The Docusaurus Adapter¶
Docusaurus is one of the most widely used documentation frameworks in the JavaScript ecosystem. It is maintained by Meta, has excellent theming, and its MDX support makes it genuinely powerful for interactive documentation.
When we decided to build a Docusaurus adapter for Zenzic, we believed the challenge was primarily one of path resolution and slug normalization — mapping Docusaurus file conventions to Zenzic's internal link model. We were wrong about the scope of the problem.
The Forensic Audit¶
We ran zenzic check against the official Docusaurus documentation — the project's own docs, built with Docusaurus itself. The results surfaced a category of Z102 errors (broken anchor references) that we could not resolve through slug normalization alone.
We extracted the ground truth by building the Docusaurus site and diffing the generated HTML against Zenzic's predictions. Three representative targets:
Target 1: docs/api/docusaurus.config.js.mdx → #hooks.onBrokenMarkdownLinks
Zenzic predicted: hooks.onbrokenmarkdownlinks (standard GitHub slugifier output).
Actual DOM: <tr id="hooks.onBrokenMarkdownLinks" tabindex="0">
The anchor was not generated from a Markdown heading. It was injected by the <APITable> React component, which iterates over a data structure and applies the object key directly as an HTML id attribute — preserving exact camelCase and dot notation, bypassing any slugification entirely.
Target 2: docs/api/plugins/plugin-ideal-image.mdx → #disableInDev
Identical root cause. <APITable> component, React-injected ID, no Markdown heading involved.
Target 3: docs/api/plugins/plugin-content-blog.mdx → #tags-file
Here the anchor was a valid Markdown heading — but it did not exist in plugin-content-blog.mdx. It existed in docs/api/plugins/_partial-tags-file-api-ref-section.mdx, imported via an MDX import statement. Docusaurus and Webpack merge MDX partials at bundle time, placing the anchor into the parent file's rendered output. Zenzic parses files in isolation.
The Diagnosis: Structural Invisibility¶
The three targets revealed two distinct failure categories, both structural rather than incidental:
React component-injected IDs. Anchors generated by components like <APITable> exist only in the rendered DOM. They are not present in Markdown source in any form. No Python AST parser can see them without executing the React component tree — which requires Node.js, Webpack, and the full Docusaurus build pipeline.
MDX partial merging. Anchors defined in imported partial files are resolved at bundle time. The relationship between a parent .mdx file and its imported partials is a runtime dependency graph, not a static filesystem relationship. Resolving it correctly would require reimplementing a significant portion of the MDX bundler in Python.
Both categories share the same root cause: Docusaurus is not a documentation engine. It is a React Single-Page Application that uses Markdown as a database. The HTML it produces is the output of a compiler and renderer, not a static transformation of source files.
The Incompatibility Is Ontological¶
We use the word ontological deliberately.
A SQL linter and a React frontend are not incompatible because of missing features. They are incompatible because they operate on different categories of artifact. The linter reasons about query structure; the frontend renders query results. Expecting the linter to validate the rendered HTML is a category error.
The relationship between Zenzic and Docusaurus is the same. Zenzic reasons about Markdown source structure. Docusaurus produces rendered React output. The anchors Zenzic needs to validate — the ones that matter for link correctness — are generated during React rendering and MDX bundling, not during Markdown parsing.
This is not a gap we could close with more engineering. It is a boundary between two fundamentally different models of what a documentation artifact is.
Why We Deleted the Adapter¶
At this point, we had a working adapter that validated approximately 99% of Zenzic's internal link rules correctly against Docusaurus projects. A reasonable engineering team might ship it with a note in the documentation: "React-injected IDs and MDX partials are not supported."
We chose not to, for one concrete reason: <APITable> is not an edge case in Docusaurus. It is the officially recommended component for API reference tables. It appears throughout the Docusaurus project's own documentation. A user following Docusaurus best practices will use it extensively.
An adapter that generates false positives on the dominant usage pattern of its target framework does not have a 99% accuracy rate. It has a 0% accuracy rate for the subset of users who matter most — the ones building exactly the kind of documentation Docusaurus is designed for.
Zenzic's Document Quality Score is a signal. A signal polluted by structural false positives is not a degraded signal. It is noise. Users do not think "I understand the ontological limits of static analysis." They think "this linter is broken" and remove it from their CI pipeline.
We applied Pillar 4 of the Zenzic Manifesto — Zero Technical Debt — and deleted the adapter.
What Zenzic Supports¶
Zenzic supports documentation engines whose anchor output is deterministically derivable from Markdown source without executing external runtime code.
In practice, this means:
- MkDocs — anchors derived from
python-markdown's heading slugifier, stable and documented - Sphinx — anchors derived from
docutilsAST, fully introspectable from Python - Hugo — anchors derived from
goldmark's slugifier, deterministic from spec - Jekyll — anchors derived from
kramdown, deterministic from spec - Zensical — our own engine, Python-native, anchor generation by definition under our control
Docusaurus falls outside this perimeter. Not because we did not try, and not because we plan to revisit it with more engineering effort. Because the architecture of Docusaurus is incompatible with the architecture of Zenzic at a level that cannot be bridged without abandoning what Zenzic is.
The Lesson We Are Publishing¶
Linters die when they try to become compilers. The moment a static analysis tool starts executing code to validate the output of that code, it has left the domain of static analysis and entered the domain of integration testing — with all the fragility, runtime dependencies, and maintenance burden that entails.
We came close to making that mistake. We had the architecture ready: a subprocess call to Node.js, isolated behind an adapter boundary, documented as an exception to our NO-Subprocess pillar. It was clean. It was well-reasoned. It would have worked, technically.
It also would have required Node.js in every CI environment running Zenzic. It would have coupled our release cadence to Docusaurus's component API. And it would have established a precedent: that Zenzic's core architectural principles have documented exceptions when the engineering case is sufficiently compelling.
A constitution with exceptions is not a constitution. It is a list of suggestions.
We deleted the Node.js call. We deleted the adapter. We wrote this post.
Zenzic is a pure-Python static documentation linter. Source: github.com/pythonwoods/zenzic