Duvlify
GitHubGet started
PublishingMigrating an existing site
Publishing

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.mdx moves to /start/intro. Set contentIds: 'preserved' in docs.config.ts to keep the path exactly. folder/index.mdx is the page folder either 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. depth nests groups, so a four-level tree carries across.

  • Frontmatter limits. description is capped at 170 characters and title needs 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. description is optional, so a page that never had one needs none; metaTitle keeps 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.png in public/ bypasses the image pipeline. npm run images:optimize shrinks anything wider than 1,600px and npm run images:check also 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.mp4
    MDX
    <video src="/images/demo.mp4" autoPlay muted loop playsInline />
  • Alt text. An importer often fills alt with a file name or a word like “image”. Search for those and write real ones, or leave alt="" on a purely decorative image.

  • House style. test/copy-style.test.mjs fails on any em dash in content/. If your corpus does not follow that rule, set DOC_ROOTS to [] 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:

MintlifyHere
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, keywordsDelete. 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. The old site’s sidebar becomes the navigation export: its tabs become NavTab entries 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 theme and 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:

  1. 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.
  2. Plain files, not generated ones. The sidebar goes in src/navigation.ts and the redirects in src/redirects.ts, both re-exported from docs.config.ts as plain bindings. No src/generated/ folder.
  3. One copy of the spec. Import it as a JSON module, never read it from disk, and generate /openapi.yaml from it if old links point there (see API reference pages).
  4. 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.
  5. 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.
  6. 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, build main and the branch and compare dist/ 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.

Use these docs with your AI tools

An AI agent can read this documentation directly. You do not need an account or an API key. Everything here is public and read-only.

Query these docs via MCP

Recommended

Add this server to Claude, Claude Code, Cursor, Mistral, or any tool that supports MCP. Your agent can then search Duvlify documentation and read it in full, instead of answering from memory.

https://duvlify.dev/mcp
  • searchFind the passages that answer a question.
  • fetchRead one page in full, as Markdown.
  • list_pagesSee every page in this documentation.

Query these docs over HTTP

The same tools also work as plain web requests. Use this for scripts, or for any tool that does not support MCP. There is one endpoint per tool. Arguments go in the query string, and the answer comes back as JSON.

https://duvlify.dev/api/docs/search?query=custom+domain

Read the OpenAPI description. It is built from the same definitions as the tools, so it always matches what the endpoints do.

Read these docs as Markdown

Add .md to any page URL to get its Markdown source. You can also send the headerAccept: text/markdown to the page URL itself.

To read the whole documentation in one file, open llms-full.txt. For a short index of every page, open llms.txt.