Migrating MkDocs Documentation to VitePress

MkDocs with the Material theme is one of the most popular documentation setups, especially in Python projects. Teams move off it for a few recurring reasons: the rest of their tooling is JavaScript and the Python build environment is the odd one out; they want Vue components in their docs; or build times on large sites have grown. VitePress is a natural destination for teams in the Vue and Vite ecosystem, because it keeps the same model — Markdown files plus a config — while replacing the Python toolchain.

This guide migrates the docs for a JavaScript SDK — 380 pages, MkDocs 1.6 with Material 9.5, admonitions, content tabs, snippets and a custom domain — to VitePress 1.5, keeps every URL working, and measures the result. It belongs to Docs Frameworks: Docusaurus, Starlight and VitePress; the companion guide Migrating from MkDocs to Starlight covers the same move to Starlight.

Prerequisites

  • The MkDocs site building cleanly, with its list of output URLs captured.
  • Node.js 20 and a new branch for the migration.
  • An inventory of Markdown extensions used, from the markdown_extensions list in mkdocs.yml.

Step 1: Capture URLs and Inventory Extensions

mkdocs build -d /tmp/mkdocs-site
find /tmp/mkdocs-site -name index.html | sed 's#/tmp/mkdocs-site##; s#index.html$##' | sort > urls-before.txt
grep -rhoE '^(!!!|\?\?\?|===|--8<--)' docs | sort | uniq -c

The site had 380 pages and used:

MkDocs syntaxOccurrencesVitePress equivalent
!!! note / !!! warning admonitions612::: tip / ::: warning containers
??? note collapsible admonitions48::: details
=== "npm" content tabs214::: code-group for code, custom component otherwise
--8<-- snippets37<!--@include: ./file.md-->
{ .class } attribute lists22Mostly removed; 4 kept via markdown-it-attrs
Abbreviations1 fileDropped
Occurrences of MkDocs-specific syntax in the source Horizontal bars. Admonitions 612 occurrences. Content tabs 214. Collapsible admonitions 48. Snippet includes 37. Attribute lists 22. MkDocs syntax that needed converting Admonitions (!!!) 612 Content tabs (===) 214 Collapsible (???) 48 Snippets (--8<--) 37 Attribute lists 22
Counted with grep over 380 Markdown files; everything else was plain Markdown that VitePress renders unchanged.

Step 2: Convert the Syntax With a Script

Admonitions in MkDocs are indented blocks; VitePress containers are fenced. A Node script handles the indentation-to-fence conversion, the type mapping and titles:

const TYPE = { note: 'info', info: 'info', tip: 'tip', warning: 'warning', danger: 'danger', example: 'details' };
function convertAdmonitions(src) {
  return src.replace(/^(!!!|\?\?\?)\s+(\w+)(?:\s+"([^"]*)")?\n((?:(?: {4}|\t).*\n|\n)+)/gm,
    (_, kind, type, title, body) => {
      const t = kind === '???' ? 'details' : (TYPE[type] ?? 'info');
      const inner = body.replace(/^(?: {4}|\t)/gm, '').trimEnd();
      return `::: ${t}${title ? ' ' + title : ''}\n${inner}\n:::\n\n`;
    });
}

Content tabs became ::: code-group when every tab held one code block — 191 of the 214 — with the tab label moved into the code fence as [npm]. The other 23 held prose and code together and used a small <Tabs> Vue component registered in the theme. Snippets became VitePress includes, with paths adjusted from the snippets base directory to relative paths.

After the script, a grep for !!!, ???, === and --8<-- returned nothing, and a visual review of 40 pages found six where indentation inside lists had confused the conversion; those were fixed by hand.

Step 3: Generate the Sidebar From mkdocs.yml

The nav tree maps directly onto VitePress's sidebar. A one-off script reads mkdocs.yml and writes .vitepress/sidebar.ts:

import yaml from 'js-yaml';
const nav = yaml.load(fs.readFileSync('mkdocs.yml', 'utf8')).nav;
const toLink = (p) => '/' + p.replace(/(index)?\.md$/, '');
const walk = (items) => items.map((it) => {
  const [text, val] = Object.entries(it)[0];
  return Array.isArray(val) ? { text, collapsed: true, items: walk(val) } : { text, link: toLink(val) };
});
fs.writeFileSync('.vitepress/sidebar.ts', `export default ${JSON.stringify(walk(nav), null, 2)}`);

The generated file is now maintained by hand. Top-level nav sections became VitePress's top navigation bar, with a separate sidebar per section.

Step 4: Keep Every URL

MkDocs served /guide/retries/ (directory URLs). VitePress with cleanUrls: true links to /guide/retries and writes guide/retries.html. To keep the old slash-terminated URLs working, the site deploys to Cloudflare, which serves retries.html for /guide/retries automatically and redirects /guide/retries/ to the clean form with a 308. That kept every old link working, with one redirect hop for links that included the slash. Teams that want no hop can instead keep the trailing-slash form by not enabling cleanUrls and arranging files as retries/index.md.

Comparing URL lists after the build:

npx vitepress build docs
find docs/.vitepress/dist -name '*.html' | sed 's#docs/.vitepress/dist##; s#\.html$##; s#/index$#/#' | sort > urls-after.txt
diff <(sed 's#/$##' urls-before.txt) <(sed 's#/$##' urls-after.txt)

Three pages differed — MkDocs had used README.md as an index, which VitePress does not — and got explicit redirects.

Steps of the MkDocs to VitePress migration Five steps. Capture the MkDocs URL list and extension inventory. Convert admonitions, tabs and snippets with a script. Generate the sidebar from the mkdocs.yml nav. Build with VitePress and diff the URL lists. Deploy with redirects for the three changed paths. The migration pipeline Capture URLs and syntax inventory Convert syntax admonitions, tabs, snippets Sidebar from nav one-off script Build + URL diff 3 differences Deploy with 3 redirects
The URL diff is the gate: the migration ships only when it shows no unexplained differences.

Step 5: Replace Material Features You Relied On

Material for MkDocs includes features VitePress handles differently:

Running Both During the Transition

The migration branch lived for three weeks while writers kept editing the MkDocs source on main. Rather than freeze content, the conversion script was rerun against main before merging, so late edits were converted with the same rules. Writers previewed the VitePress build on a preview deploy of the branch, and filed issues for pages that looked wrong. On the day of the switch, the script ran one last time, the branch merged, and the CI workflow changed from mkdocs gh-deploy to the VitePress build and deploy in the same pull request. The Python requirements file was removed a week later, after nothing had needed it.

What Writers Noticed

The writers' experience changed less than expected. They still edit Markdown files in the same folders, preview locally with one command (npm run docs:dev instead of mkdocs serve), and open pull requests the same way. Three differences came up in the first month. The dev server reloads a changed page in well under a second, against two to four seconds with MkDocs on this site, which writers liked. Admonition syntax changed, so a cheat sheet in the contributing guide showed the new container forms next to the old ones. And VitePress reports Markdown and Vue errors in the browser overlay rather than the terminal, which took a day to get used to. None of the writers asked to go back.

Measured Impact

MeasureMkDocs MaterialVitePressTool
Cold build, 380 pages54 s11 shyperfine, 5 runs
CI job including install1 min 52 s48 sGitHub Actions
Render-blocking CSS46 KB21 KBLighthouse
JavaScript per page39 KB72 KBDevTools network panel
LCP, mobile median1.5 s1.3 sLighthouse 12
Broken URLs after migration—0URL diff + edge 404 log

VitePress ships more JavaScript, because it hydrates pages for client-side navigation, but less render-blocking CSS, which is what moved LCP.

Build time and CI time before and after the migration Paired bars. Cold build went from 54 seconds to 11 seconds. CI job including dependency install went from 112 seconds to 48 seconds. Render-blocking CSS went from 46 to 21 kilobytes. Before and after on the same content MkDocs Material VitePress Cold build (s) 54 11 CI job (s) 112 48 Render-blocking CSS (KB) 46 21
The CI job time also dropped because the Python environment setup disappeared.

Pitfalls & Rollback

  • Indented admonitions inside lists. The script misreads some; review pages with nested lists by eye.
  • Content tabs holding prose. code-group is for code only; use a component for mixed tabs.
  • Forgetting README.md indexes. They change URLs; redirect them.
  • Dropping features without noticing. List the Material features you use before starting.
  • Rollback: keep the MkDocs config and workflow on a branch for a month; the Markdown changes are mechanical and the script can be run in reverse for admonitions if needed.

Conclusion

Moving from MkDocs Material to VitePress is mostly syntax conversion and URL care. Admonitions, tabs and snippets convert with a script, the sidebar comes from mkdocs.yml, and a URL diff proves nothing broke. For this 380-page SDK site, the build went from 54 seconds to 11 and the docs toolchain went from Python plus Node to Node alone.

FAQ

Can VitePress read MkDocs Markdown files directly?

Mostly. Plain Markdown, tables, code blocks and links work unchanged. MkDocs extensions do not: admonitions written with three exclamation marks, content tabs with three equals signs, snippets includes, abbreviations and attribute lists all need converting to VitePress containers, code groups and includes. A script handles the common ones; a few pages need hand editing.

Do MkDocs and VitePress produce the same URLs?

Not by default. MkDocs with directory URLs serves pages at paths ending in a slash, while VitePress serves page.html with clean URLs as page without the extension. Setting cleanUrls in VitePress and configuring the host to serve the HTML files at slash-terminated paths, or adding redirects, keeps existing links working.

What replaces the nav section of mkdocs.yml?

VitePress's themeConfig sidebar, which is a JavaScript structure of groups and links. The nav tree in mkdocs.yml maps onto it closely, so a short script can generate the sidebar from mkdocs.yml once, after which it is maintained by hand or generated from the folder structure.

Is VitePress faster than MkDocs?

On the 380-page site in this guide, VitePress built in 11 seconds against 54 seconds for MkDocs Material with its default plugins, and pages shipped less render-blocking CSS. MkDocs remains a good choice for Python teams who want to stay in Python tooling; the migration is mainly worth it for teams already working in the JavaScript ecosystem.

Filed underHow-tosVitePress