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

Migrate

Migrate your docs from ReadMe

Hand your Git-synced ReadMe repository to a coding agent, keep every page at its flat URL, generate the API reference from your specs, and deploy docs you host yourself.

By 8 min read

By the end of this guide, your ReadMe developer hub is a Blume project in the same repository: your guides and changelog converted from ReadMe's Markdown to Blume's, your categories rebuilt as folders, your API reference generated from the OpenAPI files you already sync, and every old URL either still serving its page or redirecting to it. A coding agent does the conversion with a codemod that ships with Blume, and you review it.

Which repository you have

ReadMe is hosted, so what's in Git depends on how your project talks to it:

  • Bi-directional Git sync. ReadMe commits every page to your repository: folders per category, _order.yaml files for the sidebar order, your OpenAPI files in reference/, and one branch per version. This is the shape the codemod reads, and the one this guide follows.
  • Uploads with rdme. A CI step pushes Markdown files to ReadMe, and each file's frontmatter places it in the sidebar. The repository holds only what CI uploads, so edits made in ReadMe's editor are missing. The agent rebuilds the folders from frontmatter first, then converts the pages.
  • No Git at all. Export first. On a current project, connect bi-directional sync to a new, empty repository (Settings → Git Connection): ReadMe writes the whole hub there, every version as a branch, and you're in the first case.

What carries over

Your pages are MDX already, so most of the work is ReadMe's own syntax and keeping its flat URLs. On one real hub with 75 guides and 10 API definitions, the build passed and all 247 old URLs either served a page or redirected to one.

In ReadMeIn Blume
Dashboard settings: logo, colors, fonts, navigation namesblume.config.ts, read from the settings your hub publishes
Categories and _order.yamlGroup folders, which add nothing to the URL, with a meta.ts for order
Pages nested under a parent pageFolders, with each page's flat URL pinned in its frontmatter
Emoji blockquotes and <Callout>:::info, :::warning, and the other callouts
Code blocks written back to back<CodeGroup> tabs
<Image>, <Embed>, <Table>, <Cards>, <Accordion>Markdown images, <YouTube>, Markdown tables, <CardGroup>, <Accordion> and <Expandable>
Glossary terms<Tooltip>, with each definition from your hub
Reusable content blocksPartials in _snippets/, pulled in with <include>
Custom componentsBlume components, or React islands when they're interactive
Endpoint pages, one per operationPages generated from your OpenAPI files, in ReadMe's order, with any prose you wrote on them
The changelog, from your main branchChangelog pages with the same URLs, an index, and an RSS feed
Font Awesome iconsLucide icons

Here's one page before and after the conversion:

---
title: Create an order
excerpt: Place your first order with the Orders API.
metadata:
  title: Create an order | Acme
icon: fa-cart-shopping
---
# Create an order

> 📘 Sandbox keys
>
> Test keys never charge a card. See [API keys](doc:api-keys).

<Image align="center" src="https://files.readme.io/1a2b3c4-order-flow.png" />

```curl
curl https://api.acme.example/v1/orders
```
```javascript Node SDK
await acme.orders.create({ sku: "tee" });
```
---
title: Create an order
description: Place your first order with the Orders API.
icon: shopping-cart
seo:
  title: Create an order | Acme
---

:::info[Sandbox keys]
Test keys never charge a card. See [API keys](/docs/api-keys).
:::

![order flow](https://files.readme.io/1a2b3c4-order-flow.png)

<CodeGroup>

```bash cURL
curl https://api.acme.example/v1/orders
```

```javascript title="Node SDK"
await acme.orders.create({ sku: "tee" });
```

</CodeGroup>

The page still serves /docs/create-an-order. Every page becomes .mdx, since ReadMe's .md files are MDX.

Before you start

Check out the branch named after your hub's default version (not main, which holds the changelog) and branch from it. Use a name that matches none of your ReadMe versions: with sync on, ReadMe reads and writes every branch named after a version, so restructuring one changes your live hub.

git switch -c migrate-to-blume

ReadMe keeps the changelog only on main. Bring it into your working tree:

git archive origin/main changelogs | tar -x

Then save every URL your hub serves today, outside the repository:

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

And save one page of the hub as hub.html at the root of the repository. Every page embeds your project's settings (logo, colors, fonts, navigation names, glossary, and ReadMe's own redirects), so the agent reads them from that file instead of asking you, and deletes it when it's done:

curl -s https://docs.acme.example/docs/getting-started -o hub.html

Replace docs.acme.example with your hub's domain. ReadMe can start refusing requests (429) after as few as eight in a few minutes, so save these files rather than fetching pages one by one. Check that you have Node.js 22.19 or later, and that Claude Code or Codex is installed and signed in.

Run the migration

From the root of your repository, run:

npx blume migrate readme --claude

To use Codex, swap --claude for --codex. Leave out readme and Blume detects a synced repository from its _order.yaml files. An rdme upload repository leaves no such sign, so name the source there.

blume migrate converts nothing itself. It opens the agent on the blume-migrate skill inside the package, pointed at its ReadMe reference. Following it, the agent:

  1. Runs the ReadMe codemod, which moves every page into place, converts ReadMe's syntax and frontmatter, turns _order.yaml into meta.ts, and writes the redirects it can already compute. It lists everything that needs a person.
  2. Writes blume.config.ts from your hub's settings, with each OpenAPI file as a source of the openapi() reference.
  3. Works through the codemod's list: custom components, interactive widgets, page styles, links that were already broken, and images.
  4. Builds once, reads the new operation routes from the build, and runs the codemod's second pass, which redirects every old endpoint URL and puts each tag's endpoints back in ReadMe's order.
  5. Adds a package.json, removes any rdme upload steps from CI, and runs blume build, blume validate --strict, and blume audit --only redirects until they pass.

It ends with a summary of what it migrated, dropped, and approximated. Keep it for the review.

Review the changes

Start with git diff --stat, run npx blume dev, and check these, which are where a ReadMe migration most often needs a second look.

  • Interactive widgets. A button with an onClick pasted into a page does nothing in Blume, which warns about it as BLUME_MDX_EVENT_HANDLER. Hubs often paste the same one, such as a "contact support" box, into many pages. Each should now be one React island. The search below should print nothing outside code samples.
  • Page styles. Rules in a page's <style> block, like ul { … }, restyle the whole site, sidebar included, once they move to theme.css. Pages that leaned on them should be rebuilt with components instead.
  • Custom components. Blume compiles Tailwind classes, but ReadMe's own CSS variables don't exist here, so check each block the agent ported or replaced, in light and dark mode.
  • Callouts and code tabs. A blockquote that starts with an emoji was a callout on ReadMe; one the conversion missed shows as a plain quote. Fenced blocks written back to back were tabs; check the tab labels.
  • Headings. Where a page used # headings for its sections, every heading moved down one level, since Blume renders the title as the page's only H1. Anchors keep their ids.
  • Images. Images move off ReadMe's CDN into public/. Where an image had no usable alt text, the agent derived one from the file name; read those.
  • Variables. Each {{name}} needs a default in variables. ReadMe filled them per reader; Blume shows the default to everyone, so make sure no default is a real key.
  • Links that were already broken. Hubs collect links to pages from older versions, which 404 on ReadMe too. The agent points each at the closest page or unlinks it, and lists them.
  • The API reference. Each OpenAPI file is its own group in the API tab, in ReadMe's category order, with endpoints in ReadMe's order inside each tag. Prose you wrote on an endpoint page, and callouts inside your spec's descriptions, now show on the operation page. A ReadMe category that held several files is split into one group per file, each with its own label.
  • Hidden and custom pages. Pages hidden on ReadMe stay reachable by URL and out of the sidebar and search. Custom pages have no sidebar, as before.
grep -rnE 'on[A-Z][a-zA-Z]*=|<style|className="fa' \
  docs reference recipes page changelog --include='*.mdx'

Keep every old URL working

ReadMe serves every page at a flat URL however deeply it's nested, and Blume keeps those. API reference URLs change, because Blume builds them from your OpenAPI files:

Old ReadMe URLIn Blume
/docs/create-an-order, at any depthThe same page
/changelog/12-05-2022The same entry, with the date whole in its URL
/reference/getorderIts operation page, by redirect: /reference/orders-api/orders/get-order, or /reference/orders/get-order with a single API file
A tag page, like /reference/ordersA redirect to its first endpoint, as ReadMe did
A parent page with no content of its own, and /docsA redirect to the first page under it, as ReadMe did
/v3.0/docs/create-an-orderA redirect to /docs/create-an-order

Your sitemap leaves out hidden pages. The codemod's readme-migration.json lists every URL your repository served, hidden ones included, so add them to your list while the file is there:

node -p "require('./readme-migration.json').urls.map((u) => u.url).join('\n')" \
  >> ../old-urls.txt
sort -u -o ../old-urls.txt ../old-urls.txt

Then, with npx blume dev running, check every URL and print the ones that don't land on a page:

while read -r route; do
  code=$(curl -sL -o /dev/null -w "%{http_code}" "http://localhost:4321$route")
  [ "$code" = "200" ] || echo "$code $route"
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 preview works too, after a build: both answer pattern redirects like the version prefixes. For status codes and how each host serves redirects, see Move documentation URLs while preserving old links.

Deploy and switch over

Build and run the checks the agent ran, so you see them pass yourself:

npx blume build
npx blume validate --strict
npx blume audit --only redirects

Pick a host and follow Deployment. ReadMe sent Try it requests through its own proxy; Blume sends them from the reader's browser, so your API has to allow your docs origin. Send a preflight and look for an access-control-allow-origin header:

curl -si -X OPTIONS https://api.acme.example/v1/orders \
  -H 'Origin: https://docs.acme.example' \
  -H 'Access-Control-Request-Method: GET' \
  | grep -i '^access-control-allow-origin'

If it's missing, choose a host adapter with server output, such as vercel(), and set playground: { proxy: true } in the openapi() entry.

Disconnect Git sync before you merge (Settings → Git Connection), or ReadMe syncs the restructured branch into your live hub. Then push the branch, check a preview deployment, and rerun the URL check against it. Point your custom domain's DNS at the new host, and keep the ReadMe project until production passes the URL check, so rolling back is a DNS change. Submit the new sitemap.xml in Google Search Console, since search engines recrawl moved URLs on their own schedule.

What doesn't carry over

  • The hosted editor. ReadMe's editor, branches and reviews, and suggested edits stay with ReadMe. In Blume, pages are files reviewed in pull requests, and each page can link to its source on GitHub.
  • API metrics and logs. My Requests, API Metrics, and the developer dashboard read your API's traffic through ReadMe. Analytics in Blume come from the adapter you choose and cover the docs, not the API.
  • Personalized docs. ReadMe filled in each reader's API key and variables after they logged in. Blume shows every reader the same page, and Try it takes the credentials they type.
  • Discussions. The community forum doesn't move; link to wherever your community lives now from the header.
  • Logins. Blume has no reader accounts. Use your host's protection, such as Vercel Deployment Protection or Cloudflare Access, which covers the whole site; mixing public and private pages takes two sites.
  • The landing page and recipe walkthroughs. ReadMe's landing page builder doesn't sync to Git, so rebuild your home page as a Blume page. Recipes become ordinary pages; their step-by-step code modal doesn't carry over.

Next step

Migrate your docs

Run it at the root of the repository ReadMe syncs to, on a branch named after none of your versions, then work through the review above.

npx blume migrate readme --claude
Read the migration reference

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

Keep going.More guides.

  • Migrate your docs from VitePress

    Hand your VitePress site to a coding agent, convert its Markdown extensions with a codemod, rebuild sidebar groups without moving URLs, and keep every old .html address and heading anchor working.

  • Migrate your docs from Fern

    Hand your Fern repository to a coding agent, keep every page URL and redirect every endpoint, export a Fern Definition to OpenAPI, and deploy docs you host yourself while Fern keeps generating your SDKs.

  • Migrate your docs from Redocly

    Hand your Redocly project to a coding agent, convert Markdoc to MDX, keep every API reference URL with generated redirects, and deploy docs you host yourself.

Upgrade your docs with Blume.

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

npx blume init