Tutorial2 days ago

3. Migrating an MkDocs Project, and What Vanishes

It read our mkdocs.yml with no conversion step and built the site correctly. Then it dropped four plugins without printing a single word about any of them.

The WJS Desk

Sep 22, 2026 · updated 2 days ago · 7 min read

Photo by SHVETS production on Pexels

Zensical is written by the Material for MkDocs team, and Material for MkDocs is what a very large share of open source documentation already runs on. So the question almost everyone has is the same: can it read the project I already have?

We tested it. The answer is yes, further than we expected, with one gap that produces no error message and will quietly change your site.

The test

We built a minimal but realistic MkDocs project, the shape of thousands of real repositories, and pointed zensical at it without converting anything:

site_name: Legacy Docs
site_url: https://example.com/
theme:
  name: material
nav:
  - Home: index.md
  - Guide: guide.md

Then simply:

$ zensical build

Build started
No issues found
Build finished in 0.24s

No conversion step, no zensical.toml, no flag telling it to look for MkDocs config. It found mkdocs.yml, read it, and built.

What survived

We checked the output rather than trusting "No issues found", which is a lesson this series keeps teaching.

From mkdocs.ymlResult
site_nameUsed. Title rendered as "Home - Legacy Docs"
navHonoured. "Guide" appears in the navigation
Both pagesBuilt, at index.html and guide/index.html
theme: materialAccepted without complaint

It also produced three files we did not ask for and that matter for documentation: sitemap.xml, search.json, and objects.inv. That last one is the Sphinx style inventory that lets other projects cross-reference your docs, and generating it automatically is a thoughtful detail.

Markdown extensions work too. We gave it a config declaring admonition and pymdownx.superfences, then wrote an admonition:

!!! note "An admonition"
    This uses the admonition extension.

The output contains class="admonition note" and class="admonition-title", and zero occurrences of the literal !!! note. It rendered properly rather than passing the syntax through as text.

For a tool at version 0.0.63, reading a competitor's config format and producing a correct site from it is a genuinely strong result.

We tested the extensions properly

Markdown extensions are where a MkDocs migration usually dies, because Material projects lean on them heavily and a missing one turns your syntax into literal text on the page. So we wrote a page using nine of the most common ones and checked the rendered HTML rather than eyeballing the browser.

markdown_extensions:
  - admonition
  - tables
  - footnotes
  - attr_list
  - def_list
  - pymdownx.superfences
  - pymdownx.tabbed
  - pymdownx.details
  - toc
ExtensionChecked forResult
admonitionclass="admonition warning"Rendered
pymdownx.details<details>Rendered
pymdownx.tabbedtabbed-setRendered
tables<table>Rendered
def_list<dl>Rendered
footnotesclass="footnote"Rendered
tocheading anchorsRendered

Seven for seven, and we specifically checked that no raw syntax leaked through: no literal !!! warning, no ??? note, no === "Tab one" anywhere in the output. The collapsible details blocks and the tabbed content sets, which are the two most visually obvious if they fail, both produced correct markup.

That is a better result than we expected and it changes the migration calculus. Extensions are the part of a Material site that is most tedious to replace by hand, and they came across intact.

What silently disappeared

Here is the finding that should decide whether you migrate today.

We added four plugins to the config, all common in real Material projects:

plugins:
  - search
  - social
  - tags
  - minify

The build output, in full:

Build started
No issues found
Build finished in 0.20s

We then grepped the entire build output for each plugin name. Zero mentions of social. Zero mentions of tags. Zero mentions of minify. Not a warning, not a note, not a line saying "ignoring unsupported plugin". They were read and discarded in silence, and the build reported no issues.

Why this matters more than it sounds. The Material social plugin generates your Open Graph preview cards. The tags plugin builds your tag index pages. If you migrate a site that uses them, your social previews stop being generated and your tag pages stop existing, and nothing in the build tells you. You will find out when someone shares a link and it renders as a bare URL, or when a reader hits a 404 on a tag page that used to work.

This is not the tool being broken. Plugins are a MkDocs extension mechanism and zensical is a different program that is not obliged to implement them. The problem is purely the silence: "No issues found" is an active claim, and in this case it is not accurate.

The migration checklist we would use

Given that, here is how we would approach an actual migration rather than just running the build and seeing green.

1. List what your plugins do before you start.

grep -A20 "^plugins:" mkdocs.yml

For each one, write down what would be missing from the site if it vanished. That list is your real migration scope, and the build will not produce it for you.

2. Build both and compare the file trees. This is the check that actually catches the silence:

mkdocs build -d /tmp/mkdocs-out
zensical build
diff <(cd /tmp/mkdocs-out && find . -type f | sort) \
     <(cd site && find . -type f | sort)

Missing tag pages and missing social cards show up immediately as absent files. A green build does not.

3. Check your markdown extensions individually. Admonitions worked for us. We did not test the long tail of pymdownx extensions, and neither should you assume. Write one page using every extension your docs rely on, build it, and read the HTML.

4. Keep mkdocs.yml. Zensical reads it directly, so there is no forced conversion and no burned bridge. You can run both against the same source for as long as you like, which is the correct way to evaluate this.

What we did not test

The boundaries of this result, so you can judge how far to trust it.

Theme overrides. We tested stock theme: name: material. A project with a custom_dir full of overridden Jinja templates is a completely different question, and Material's template structure is deep. If your site has overrides, assume they do not carry across until you prove otherwise.

The long tail of pymdownx. Nine extensions is a good sample and it is not the whole set. Material projects reach for pymdownx.emoji, pymdownx.highlight, pymdownx.snippets and others we did not exercise.

Multi-language sites. Zensical advertises more than 60 languages, and we built in English only.

Large real corpora. Our compatibility test was two pages. The 203 page test in part 2 used native config, not an MkDocs one.

None of these are reasons to expect failure. They are the things we would check on your specific project before committing, and the file tree diff below finds all of them at once.

Running both side by side

The single most useful property of this compatibility: zensical reads mkdocs.yml directly, so there is no conversion and nothing to undo. You can keep both installed and build the same source with each.

mkdocs build -d /tmp/mkdocs-out
zensical build

diff <(cd /tmp/mkdocs-out && find . -type f | sort) \
     <(cd site && find . -type f | sort)

Every silent failure in this part shows up in that diff as a missing file. Tag pages that stopped existing, social cards that were never generated, a nav page that did not get built. None of them appear in the build log; all of them appear here.

Run that once before you switch and you have converted an invisible problem into a list.

What we would not migrate yet

  • Anything depending on plugins for content. Tags, social cards, redirects, i18n. Not because they are unsupported forever, but because they are unsupported silently today.
  • Sites with heavy theme overrides. We tested stock theme: material. A project with a custom_dir full of template overrides is a different question and we have not answered it.
  • Anything where a missing page is expensive. Until --strict works, which is part 4, you have no build-time guarantee that what you had is what you get.

Why the config reads so familiarly

One structural note that explains a lot of this part. The scaffolded zensical.toml from part 1 uses the same key names as MkDocs: site_url, site_name, site_description, repo_url, edit_uri, extra_css, nav. The container changed from YAML to TOML and the vocabulary did not.

That is what makes reading mkdocs.yml cheap to implement rather than a compatibility shim bolted on afterwards: the two config formats are describing the same model. It is also why the nav translated correctly without us doing anything, and why we would expect most simple config keys to keep working as the project matures.

What we would migrate now

A straightforward documentation site: markdown, a nav, standard extensions, no plugin-generated content. That describes a large fraction of project docs, and for those the migration is genuinely "point it at the existing directory and it works", which is a remarkable thing to be able to write about a 0.0.x release.

The honest summary: the compatibility is real and better than we expected, and the failure mode is silence rather than breakage. Verify by diffing file trees, not by reading the build output.

Next

Part 4 puts it in CI, looks at the GitHub Actions workflow it scaffolds for you, and covers the limitation that matters most for automation: strict mode does not exist yet, and broken links exit zero.

Share

Zensical read our mkdocs.yml and built it correctly with no conversion. Then dropped 4 plugins in total silence. No issues found, it said. #StaticSite #MkDocs #OpenSource

Never miss a ship

The best stuff that shipped this week, delivered every Thursday. Free, no spam. We read all the boring stuff so you get the fun parts.

Keep reading