Migrating from MkDocs to Starlight
Material for MkDocs is one of the best documentation themes ever built, and most teams that leave it do so reluctantly. The usual triggers are specific: builds that pass two or three minutes as the site grows, content that wants real components rather than Markdown extensions, or a platform team consolidating every site onto one JavaScript toolchain. When those apply, Starlight is the smoothest landing — it shares Material's information architecture (tabs, sidebar, admonitions, search), and its content is still Markdown.
This guide walks through a migration of a 900-page Material for MkDocs site: the conversion script, navigation, URL parity, search and the numbers afterwards. It extends Docs Frameworks: Docusaurus, Starlight and VitePress and applies the general playbook from Migrating Between Static Site Generators.
Prerequisites
- The MkDocs site's
mkdocs.yml,docs/folder and a list of enabled Markdown extensions (markdown_extensions:in the config). - The current production URL list — a crawl or the generated
sitemap.xml. - Node.js 20 or later, and a fresh Starlight project.
- A preview host so the migrated site can be compared page by page against production before cutover.
Inventory the Markdown Extensions
MkDocs content is Markdown plus whichever Python-Markdown and PyMdown extensions the site enables. Every one of those is syntax that Starlight will render literally unless you convert it. Count usage before writing any code:
cd docs
grep -rhoE '^!!!\s*\w+|^\?\?\?\+?\s*\w+' . | awk '{print $1, $2}' | sort | uniq -c # admonitions
grep -rc '^=== "' . | awk -F: '$2>0' | wc -l # files with tabs
grep -rhoE '\{\s*\.[a-z-]+\s*\}' . | sort | uniq -c # attr_list classes
grep -rhoE ':[a-z-]+:' . | sort | uniq -c | sort -rn | head # emoji / icons
grep -rl '--8<--' . | wc -l # snippets includes
On the 900-page site the inventory found 2,140 admonitions (of seven types), 186 files with content tabs, 37 files using --8<-- snippet includes, 412 Material icon shortcodes and 64 attr_list class annotations. Those five constructs were the whole conversion job; everything else was already standard Markdown.
Convert With a Script, Not by Hand
A Node script over the files handles the mechanical constructs. Admonitions are the largest group and the easiest: Material's indented body under !!! type "Title" becomes a fenced aside.
// scripts/convert-mkdocs.mjs (excerpt)
const TYPE = { note: 'note', info: 'note', tip: 'tip', success: 'tip',
warning: 'caution', danger: 'danger', bug: 'danger', example: 'note' };
export function convertAdmonitions(src) {
return src.replace(
/^(!!!|\?\?\?\+?)\s+(\w+)(?:\s+"([^"]*)")?\n((?:(?: {4}|\t).*\n|\n)+)/gm,
(_, _kind, type, title, body) => {
const t = TYPE[type] ?? 'note';
const text = body.replace(/^( {4}|\t)/gm, '').trimEnd();
return `:::${t}${title ? `[${title}]` : ''}\n${text}\n:::\n\n`;
});
}
Content tabs need MDX. Files containing === "Tab" blocks are renamed to .mdx, get import { Tabs, TabItem } from '@astrojs/starlight/components'; at the top, and each tab group becomes a <Tabs> element with <TabItem label="..."> children. Snippet includes (--8<-- "path") become MDX imports of the referenced file rendered as a component, or — for code samples — Starlight's <Code code={...} lang="..." /> fed from a ?raw import.
Icon shortcodes such as :material-check: were replaced with plain text in tables ("Yes") and removed in headings, where they had been decorative. The 64 attr_list annotations were reviewed by hand: 51 were button styles on links, which became a small <LinkButton> component, and 13 were obsolete.
Run the script, build, and commit the output in one pull request per top-level section so reviewers see bounded diffs.
Rebuild Navigation From mkdocs.yml
MkDocs defines navigation explicitly in nav:; Starlight takes a sidebar array. Translating is a thirty-line script that walks the YAML tree and emits Starlight entries, with one rule: where a MkDocs section lists every file in a folder in alphabetical order, emit autogenerate: { directory } instead of an explicit list, so new pages appear without config changes. Ordering that was explicit in mkdocs.yml becomes sidebar: { order: n } in each page's front matter, a conversion covered in general in Converting Front Matter at Scale During Migration.
Material's top-level navigation tabs (navigation.tabs) have no direct Starlight equivalent. The site used five tabs; they became five top-level sidebar groups, with the most-used group expanded by default and the others collapsed, which usability sessions showed readers found as quickly as the tabs.
Keep Every URL
MkDocs publishes docs/guides/install.md at /guides/install/; Starlight with trailingSlash: 'always' publishes src/content/docs/guides/install.md at the same path. The two differences to handle are case (MkDocs keeps filename case; Starlight lowercases slugs) and index.md versus README.md handling. Generate the old URL list from the production sitemap, the new list from the Starlight build, and diff them in CI:
curl -s https://docs.example.com/sitemap.xml | grep -oP '(?<=<loc>)[^<]+' \
| sed 's#https://docs.example.com##' | sort > old.txt
find dist -name index.html | sed 's#^dist##; s#index.html$##' | sort > new.txt
comm -23 old.txt new.txt > missing.txt && wc -l missing.txt
The first run reported 23 missing URLs, all from mixed-case filenames. Each got a redirect in the host's _redirects file, and the diff became a required CI check — the technique from Keeping Redirects Working After an SSG Migration.
Search
Material's built-in search builds a lunr index at build time and ships it as one JSON file — 3.8 MB uncompressed on this site, downloaded in full on the first search. Starlight's Pagefind indexes the built HTML into fragments and downloads only what a query needs: the first query fetched 96 KB. Search quality was comparable in spot checks of the fifty most common queries from the old analytics; Pagefind ranked the expected page first in 44 of 50 against 41 for lunr. The trade-offs are covered in Search for Static Sites.
Measured Impact
Both sites were built on the same GitHub Actions runner and timed with hyperfine over five runs; page metrics are Lighthouse 12 mobile medians over five templates.
| Measure | Material for MkDocs 9.5 | Starlight 0.34 |
|---|---|---|
| Cold build | 142 s | 47 s |
| Warm build (one page changed) | 139 s (no incremental mode used) | 16 s |
| Compressed JS per page | 38 KB | 11 KB |
| First search download | 3.8 MB index (610 KB compressed) | 96 KB |
| Lab LCP, simulated 4G | 1.5 s | 1.1 s |
| Engineer time to migrate | — | 4 days |
Pitfalls & Rollback
- Leaving
!!!in plain text. Any admonition the regex misses renders as literal exclamation marks. After conversion,grep -rn '^!!!' src/content/docsmust return nothing; make it a CI check. - Indented code inside admonitions. Material bodies are indented four spaces; a code fence inside a body is indented eight. Dedent by exactly one level, or code blocks lose their fences.
- MDX surprises in converted files. Only files with tabs or imports should become
.mdx. Plain Markdown stays.mdso a stray{in prose does not break the build. - Lost
mkdocs-macrosoutput. If the site used the macros plugin, its Jinja expressions render literally. Replace them with Astro components or build-time data before cutover. - Rollback: keep the MkDocs build deployable until the Starlight site has run two weeks without a missing-URL report. Cutover is a DNS or routing change; so is reverting it.
Conclusion
A Material for MkDocs to Starlight migration is a bounded job once the extension inventory exists: five conversion rules, a navigation script, a URL diff in CI and a search swap. On this 900-page site it took four engineer-days, cut cold builds from 142 to 47 seconds and single-page rebuilds from 139 to 16, and removed a 3.8 MB search index from the first query. If your MkDocs site is small, fast and loved, stay — but if build time or component needs are pushing you out, this is the lowest-friction exit.
FAQ
Why would a team leave MkDocs at all?
Usually for build speed on large sites, component-based content, or to consolidate onto a JavaScript toolchain the rest of the team already uses. Material for MkDocs remains an excellent choice for small to medium Python-centric projects.
How do MkDocs admonitions map to Starlight?
Material's !!! note blocks become Starlight asides written with :::note and a closing :::. The four Starlight types are note, tip, caution and danger, so warning and similar Material types need mapping to the closest one.
Will my URLs change?
They can keep the same form. MkDocs serves directory-style URLs with trailing slashes by default, and Starlight does the same when trailingSlash is set to always. Files named index.md map identically; renamed or moved files need redirects.
How long does a migration take?
For a 900-page site with a conversion script, about four engineer-days including navigation, redirects and review. The conversion itself runs in seconds; the time goes on edge cases such as nested tabs and custom Markdown extensions.
Related
- Parent: Docs Frameworks: Docusaurus, Starlight and VitePress — why Starlight is the landing spot.
- Migrating a Docs Site from Jekyll to Hugo — the same playbook between two other generators.
- Porting Shortcodes and Includes Between Generators — converting extension syntax in general.
- Customizing Starlight Without Forking the Theme — matching Material's look afterwards.
- Adding Pagefind to an Astro Site — how the new search works under the hood.