Skip to content
Blume
Esc
↑↓navigate↵open⌘Jpreview
Guides

Migrate

Migrate your docs from Docusaurus

Move a Docusaurus site's docs to Blume with a coding agent, and check the sidebars, admonitions, and React components it carries over.

By 9 min read

By the end of this guide, your Docusaurus docs are a Blume project: the same pages in the same folders, sidebars turned into folders and meta.ts files, admonitions still written as :::note, your existing URLs kept or redirected, and a site deployed where you choose. A coding agent does the conversion, and you review it.

The migration moves your docs, your blog, and your latest version. It doesn't move the React app around them. Swizzled theme components and React pages under src/pages are yours to rebuild or leave behind, and the agent lists each one.

What carries over

Docusaurus and Blume already share a lot: Markdown and MDX in folders, numeric filename prefixes for ordering, and ::: admonitions. Here's how the rest maps:

In DocusaurusIn Blume
docusaurus.config.jsblume.config.ts
An autogenerated sidebars.jsNavigation from the folder tree, with no config
_category_.jsonA meta.ts in the same folder
:::note, :::tip, :::caution:::note, :::tip, :::warning, in .mdx pages
<Tabs> of <TabItem><Tabs> of <Tab>
static/public/, at the same URLs
```bash npm2yarn```package-install
Blog postsPages with type: blog, with a feed at /blog/rss.xml
Mermaid and math pluginsBuilt in, in .mdx pages
OpenAPI doc pluginsopenapi() in reference
Algolia DocSearchBuilt-in local search, or the algolia() adapter

What doesn't carry over: swizzled components in src/theme, React pages like your landing page, the blog's generated tag, author, and archive pages, footer column titles, <TOCInline />, and redirects computed by a createRedirects function. If your site leans on those, or you translate through Crowdin, read Blume vs Docusaurus first. It covers when staying on Docusaurus is the better call.

Before you start

The agent edits your repository in place, so start on a new branch with a clean working tree:

git switch -c migrate-to-blume

Find the folder that holds docusaurus.config.js (or .ts). In a library's repository it's often website/. You'll run every command in this guide from there.

Check which major version of Docusaurus you're on, since version 2 and version 3 content need different fixes:

grep '"@docusaurus/core"' package.json

Then save the URLs your site serves today. The classic preset writes a sitemap.xml in production builds, so pull the paths out of the live one and keep the list outside the repository:

curl -s https://docs.acme.example/sitemap.xml \
  | grep -o '<loc>[^<]*' \
  | sed -e 's#<loc>https://docs.acme.example##' -e 's#\(.\)/$#\1#' \
  > ../old-urls.txt

Replace docs.acme.example with your domain in both places. The last expression drops any trailing slash, since Blume's URLs never end in one. You also need Node.js 22.12 or later, and Claude Code or Codex installed and signed in.

Decide where your docs live

Docusaurus serves docs under /docs/ by default, even when your config never mentions routeBasePath. Blume serves pages from the root. Settle which you want before you start, because the agent needs the answer and every URL depends on it.

Keep /docs. Set a basePath, and every page mounts under it while the sidebar stays the same:

basePath: "/docs",

You still write links as if the docs lived at the root, and Blume adds the prefix. Custom pages keep their own routes, so a landing page can still live at /. One catch: every content page mounts under basePath, the blog included, so posts move to /docs/blog/. Blume writes redirects under basePath too, so sending the old /blog/ URLs there takes a rule on your host.

Move to the root. Drop the prefix, and one pattern redirect covers every old docs URL, /docs itself included:

redirects: [{ from: "/docs/:slug*", to: "/:slug*", status: 308 }],

The catch here is hosting. A static build can't turn a pattern into redirect pages, so a host that reads no redirect file, like GitHub Pages, applies only exact redirects. On GitHub Pages, keeping /docs is the smaller change.

Four more settings shape your URLs:

  • url and baseUrl. The site URL becomes deployment.site, and a baseUrl other than / becomes deployment.base. Blume detects the site URL on Vercel, Netlify, and Cloudflare Pages, but not on GitHub Pages, so keep it there.
  • slug frontmatter. Docusaurus resolves a slug without a leading slash against the page's folder. Blume's slug is the whole route from the content root, so slug: start in guides/intro.md becomes slug: guides/start.
  • id frontmatter. Docusaurus builds a page's URL from its ID, so id: part1 in guides/hello.md serves the page at /docs/guides/part1. Blume has no id key, and the skill drops it, so the page moves to its file path, guides/hello. Where an ID differs from its filename, replace it with slug: guides/part1.
  • Versions. Docusaurus serves your latest release at /docs/ and the unreleased docs/ folder at /docs/next. The agent migrates the version readers see at /docs/. To keep older versions, see Version your docs.

Run the migration

From the folder that holds your Docusaurus config, run:

npx blume migrate docusaurus --claude

To use Codex, swap --claude for --codex. On pnpm 12, run it with pnpm dlx --allow-build=esbuild instead of npx, since pnpm 12 won't run esbuild's install script until you approve it. Leave out docusaurus and Blume detects the source from your docusaurus.config file. Run it with neither flag and it only prints where the skill lives, for another agent to use.

blume migrate converts nothing itself. It names the source, then opens the agent in your terminal on the blume-migrate skill that ships inside the package, pointed at the skill's Docusaurus reference. The agent runs interactively, so its edits go through its usual permission prompts. Tell it your decisions up front: whether to keep /docs, which versions to keep, and whether the repository also holds a library whose scripts and dependencies must stay. Following the skill, it:

  1. Writes blume.config.ts from your config, theme settings, presets, and plugins.
  2. Turns sidebars into folders, and each _category_.json into a meta.ts.
  3. Renames every .md page that uses admonitions, JSX, imports, or math to .mdx, and rewrites tabs, code block options, and theme imports.
  4. Moves static/ into public/, and turns blog posts into type: blog pages with their dates in frontmatter.
  5. Adds a redirect for every URL that moves.
  6. Swaps the Docusaurus dependencies for Blume, then runs blume build and blume validate until both pass.

It finishes with a summary of what it migrated, dropped, and approximated. Keep it: it's your checklist for the review.

Review the diff

Start with git diff --stat, run npx blume dev, and check each area:

  • Config. Title, logo, navbar links as header tabs or links, the edit URL as github, and the color mode. Anything left out falls back to a Blume default on purpose.
  • Navigation. Click through the sidebar. A category with a generated index page is now a folder with an index page, and its old /docs/category/ URL should have a redirect.
  • Category pages. The skill turns each <DocCardList /> into a <CardGroup> with a card per page, which goes stale as pages change. Setting directory: "card" in the folder's meta.ts lists them for you instead, below the index page's content, and suits a generated-index category too.
  • Blog. Posts should carry a date, and the old dated URLs like /blog/2024/01/31/post should redirect. Blume doesn't generate a blog index, so check that the agent wrote a blog/index.mdx that links your posts.
  • Math. The skill treats inline $x$ as unsupported, so the agent may have moved a formula onto its own line or dropped it. Blume renders inline math written with two dollar signs, like $$x^2$$ inside a sentence, so put those back.
  • Dependencies. Only the Docusaurus packages and scripts should be gone. If your docs share a package.json with your library, its own scripts and dependencies stay.

Pages that build but render wrong

The fixes most worth checking by hand are the ones the build can't catch. Blume reads a .md file as plain Markdown, so an admonition left in one renders as literal :::note text, and the build stays green. This lists any that remain:

grep -rl '^:::' --include='*.md' docs

On Docusaurus 2, admonition titles follow a space instead of brackets. Blume drops a title written that way without an error:

:::tip Before you deploy
Set the site URL first.
:::

Blume needs the title in brackets:

:::tip[Before you deploy]
Set the site URL first.
:::

These two searches find leftover v2 titles and highlight comments:

grep -rn '^:::[a-z]* [A-Za-z]' docs
grep -rn 'highlight-next-line\|highlight-start' docs

Docusaurus's // highlight-next-line comments ship as literal comments in Blume. Replace each with a line range in the fence, like ```js {3}, or a // [!code highlight] comment on the line itself.

Custom components become islands

React components you import into pages from src/components need a new home. In Blume, an interactive component goes in an islands/ folder at the project root, and its filename becomes a tag you can use in any .mdx page with no import:

import { useState } from "react";

export default function Playground() {
  const [count, setCount] = useState(0);
  return <button onClick={() => setCount(count + 1)}>Run {count}</button>;
}

Delete the import line from each page and keep the <Playground /> tag. Docusaurus APIs don't exist here: a component wrapped in <BrowserOnly> becomes an island with export const client = "only", and one that read useDocusaurusContext can read the site's config from useBlume() in blume/hooks. See Islands for the rest.

Check every old URL

With npx blume dev running, walk your saved list and print any old URL that no longer reaches a page:

while read -r path; do
  code=$(curl -sL -o /dev/null -w "%{http_code}" "http://localhost:4321$path")
  [ "$code" = "200" ] || echo "$code $path"
done < ../old-urls.txt

Redirects count as passing, since -L follows them. Add a redirect for each path it prints, and run it again until it prints nothing. blume dev answers pattern redirects, so the loop passes even for patterns a static host like GitHub Pages ignores. Run it once more against the deployed site. For status codes, patterns, and what each host does with them, see Move documentation URLs while preserving old links.

The dev server answers a slashed URL with a 404, which is why the list has no trailing slashes. If your Docusaurus config set trailingSlash: true, links elsewhere on the web point at the slashed form, so check a few of them on the deployed site too. Hosts differ in what they do with the extra slash.

Build and validate

Stop the dev server, then build the site and check its links:

npx blume build
npx blume validate --strict

blume build fails on any page, route, or config error. On a Docusaurus 2 site, expect MDX errors here: version 2 parsed content with MDX 1, which let through a bare < or { in prose, HTML comments, and style attributes written as strings. Each error names the file and line. Then blume validate --strict checks every internal link, heading anchor, and asset, and fails on warnings too. Fix what they report rather than passing --no-strict, which drops the pages that fail.

Deploy

Many Docusaurus sites live on GitHub Pages, and a Blume site can stay there. Blume can't detect your URL on GitHub Pages, so keep it as deployment.site:

import { defineConfig } from "blume";

export default defineConfig({
  // ...everything the migration wrote
  deployment: { site: "https://docs.acme.example" },
});

A project site without a custom domain, like https://acme.github.io/widget-sdk/, splits into site: "https://acme.github.io" and base: "/widget-sdk", where Docusaurus had url and baseUrl. Deploy Markdown docs to GitHub Pages has the full Actions workflow. Keep to exact redirects there, since GitHub Pages ignores patterns. For any other host, see Deployment.

Switch over

If you published with docusaurus deploy, your last Docusaurus build is still on the gh-pages branch. Switching the Pages source to GitHub Actions for Blume leaves that branch alone, so rolling back is switching the source back. On another host, keep the old deployment until the new one has served traffic and the URL check passes against it.

Search engines follow permanent redirects, but they recrawl on their own schedule. Submit the new sitemap.xml in Google Search Console and give results time to settle.

What you run now

There's no React app left to upgrade: no swizzled components to keep in step with the theme, and no plugins to update. In exchange, you give up swizzling and the plugin API. Blume's version is layout slots for replacing pieces of the page chrome, and blume eject when you want the whole Astro app. Translations move from Crowdin to blume translate, if you had them.

Troubleshooting

A page shows :::note as text

The page is a .md file. Rename it to .mdx, where directives work.

The build fails on a page's frontmatter

Blume's frontmatter schema is strict, and BLUME_FRONTMATTER_INVALID names the page and key. Docusaurus keys like pagination_next, hide_title, and sidebar_class_name have no Blume equivalent, so remove them. sidebar_label and sidebar_position map to sidebar.label and sidebar.order, and an id that set the URL becomes a slug.

Images 404

Docusaurus serves static/ at the site root and Blume serves public/. Move the folder's contents across, and every root-relative path keeps working.

A version folder shows up as current docs

BLUME_VERSIONS_UNCONFIGURED_VERSION means a version-shaped folder isn't listed in versions.archived. Add it there, or delete the folder if you're only keeping the latest version.

A redirect works locally but not on GitHub Pages

It's a pattern. Replace it with one exact redirect per old URL.

A redirect fails the build

BLUME_REDIRECT_MATCHES_PAGE means a pattern also covers a page you kept. Narrow it to the URLs that moved.

Next step

Migrate your docs

Run it at the root of your Docusaurus site, on a clean branch, then work through the review above.

npx blume migrate docusaurus --claude
Read the migration reference

A step here not working for you? Report a broken step.

Keep going.More guides.

  • Migrate your docs from Fumadocs

    Hand your Fumadocs repository to a coding agent, check its meta.ts and component rewrites, keep your docs at /docs, and deploy with every old URL working.

  • Migrate your docs from Starlight

    Move a Starlight site to Blume with a coding agent, keep its pages and URLs, and rebuild the overrides and splash pages that stay Astro work.

  • Migrate your docs from Nextra

    Move a Nextra site's MDX and _meta navigation to Blume with a coding agent, check nested order and React components, and keep every URL working.

Upgrade your docs with Blume.

Install today and ship a production-grade docs site in minutes. Free and open source, forever.

npx blume init