Migrating an existing site
Bringing a documentation set over from Mintlify, Docusaurus, or Starlight, with what carries across, what to convert, and how to brief an agent to do it.
What already carries across
Two pieces of compatibility are deliberate and already shipped. A migration that requires touching every page is a migration nobody finishes.
Component names. The MDX vocabulary accepts the tags most documentation is
already written with: Card, CardGroup, Steps, Tabs, Accordion,
ParamField, ResponseField, Frame, plus aliases like
FileTree/Folder/File and RequestExample/ResponseExample. A page
authored for Mintlify usually renders unedited.
Icon names. Content written for other platforms names Font Awesome icons,
while this site draws from Lucide. src/icon-aliases.ts maps the common ones,
so icon="rocket" resolves either way.
What to check on an imported corpus
A corpus written elsewhere hits a few rules that a hand-written one never does.
-
URLs. A file path becomes a lowercase URL by default, so
Start/Intro.mdxmoves to/start/intro. SetcontentIds: 'preserved'indocs.config.tsto keep the path exactly.folder/index.mdxis the pagefoldereither way. -
Pages with no sidebar entry. The build fails on a published page that the navigation does not list. If the old site published such pages by their link only, give them
hidden: true: they stay routed and noindexed and leave the sidebar, search and sitemap. -
Deep sidebars.
depthnests groups, so a four-level tree carries across. -
Frontmatter limits.
descriptionis capped at 170 characters andtitleneeds two. An imported page that runs over fails with the file named. Shorten it at the source; the cap is what keeps search snippets from being cut. -
Descriptions and titles.
descriptionis optional, so a page that never had one needs none;metaTitlekeeps a custom<title>as it was. A#heading in the body is listed in “On this page” as a section, with##as its subsection. -
Undefined expressions. MDX evaluates
{PAGE}in prose, and fails the build where another tool printed it as nothing. Escape it as\{PAGE\}. -
Icons. An unknown icon name renders nothing, and the build now logs it. Map the common Font Awesome Pro names in
src/icon-aliases.ts. -
Images. Content that points at
/images/x.pnginpublic/bypasses the image pipeline.npm run images:optimizeshrinks anything wider than 1,600px andnpm run images:checkalso fails on a file over 25 MiB, which Workers assets refuse and the deploy reports poorly. -
Screen recordings. Convert each GIF to MP4, which is often ten times smaller, and embed it as a
<video>. The build reads its width and height from the file, as it does for an image, so the page does not jump when it loads:Terminal ffmpeg -i demo.gif -movflags +faststart -pix_fmt yuv420p \ -vf "scale=trunc(iw/2)*2:trunc(ih/2)*2" demo.mp4MDX <video src="/images/demo.mp4" autoPlay muted loop playsInline /> -
Alt text. An importer often fills
altwith a file name or a word like “image”. Search for those and write real ones, or leavealt=""on a purely decorative image. -
House style.
test/copy-style.test.mjsfails on any em dash incontent/. If your corpus does not follow that rule, setDOC_ROOTSto[]in that file. Keep the test: it still checks the framework’s own strings.
Frontmatter from Mintlify
title, description, sidebarTitle, icon, noindex and hidden mean the
same thing in both. The rest converts or goes:
| Mintlify | Here |
|---|---|
openapi: "GET /widgets" | pageType: api-endpoint, apiMethod: GET, apiPath: /widgets |
tag: "New" | badge: "New" |
og:*, twitter:* | Delete. The build writes every social tag and a share card per page. Use image and imageAlt for a custom card. |
mode, iconType, keywords | Delete. Nothing reads them. |
The build ignores a key it does not know, so leftover keys never fail it: they
just stay in every file, and the next person reads them as configuration.
Remove them in one commit with a Page-dates: keep line in its message, so
the change does not mark every page as updated that day (see
Frontmatter).
Navigation and styling
- Navigation. The old site’s sidebar becomes the
navigationexport: its tabs becomeNavTabentries and its groups nest inside them, with pages named by content id. There is no importer yet. See Configuration. - Custom CSS. A stylesheet written against another theme’s class names has
nothing to apply to. Restyle through
themeand its tokens instead.
Redirects
Map every old URL in redirects in docs.config.ts, or in src/redirects.ts
on a large site (see Configuration).
Mintlify’s redirects list converts one to one: source becomes the key and
destination the value. Two differences:
- No wildcards. A source such as
/old/:slug*has to become one entry per page. - No chains. Each old path points straight at its final page. The build fails on a chain, a loop, an external destination or a target that does not exist.
Migrate with an agent
Most migrations are now run by a coding agent. A 360-page Mintlify site, migrated that way and then cleaned up, showed where an agent goes wrong when nobody tells it otherwise. Put these points in its brief:
- One source from the cutover on. While the old site is still edited, a sync script can copy from it. Once it is not, delete the sync, its generated files and their “do not edit” headers in the same change. A file marked “Generated, do not edit” that people edit by hand misleads everyone who opens it.
- Plain files, not generated ones. The sidebar goes in
src/navigation.tsand the redirects insrc/redirects.ts, both re-exported fromdocs.config.tsas plain bindings. Nosrc/generated/folder. - One copy of the spec. Import it as a JSON module, never read it from
disk, and generate
/openapi.yamlfrom it if old links point there (see API reference pages). - No manifest for media. The build measures PNG, WebP, SVG and MP4 files itself. A size list written by the importer goes stale the first time someone replaces a file.
- No comparisons in the code. A comment that says “like Mintlify” or “Mintlify’s order” describes a site that no longer exists. Say what the code does and why.
- Proof from the build, not from reading. Run
npm test, not only the build: it boots the Worker, and some failures only appear there. For a cleanup that should change nothing visible, buildmainand the branch and comparedist/file by file.
A brief that carries those points:
Migrate the Mintlify docs in <old repo> into this Duvlify repository.
Read AGENTS.md, CONTRIBUTING.md and content/guides/migrating.mdx first.
- This repository becomes the only source. Leave no sync script, no
src/generated/ folder, and no "do not edit" file.
- Sidebar in src/navigation.ts, redirects in src/redirects.ts, both
re-exported from src/docs.config.ts. Expand wildcard redirects and point
every old URL straight at its final page.
- The OpenAPI spec is src/openapi.json, imported as a JSON module.
- Convert frontmatter with the table in migrating.mdx and delete keys
nothing reads. Convert GIFs to MP4. Write no image manifest.
- No comment or doc compares this code with Mintlify.
- Done means: npm test passes, every URL in the old sitemap answers 200 or
a 301 to a page that does, and you list what you could not convert.Not automated yet
There is no importer yet: the navigation, the frontmatter pass and the redirect map are done by you or your agent, with the points above. An importer that fetches a published site and a check that every old URL still resolves are planned. If you are migrating now and want to compare notes, open a discussion.