Migrate
Migrate your docs from GitBook
Hand your Git-synced GitBook repository to a coding agent, keep every page at the URL it has today, convert GitBook's blocks to components, and deploy docs you host yourself.
By Hayden Bleasel10 min read

By the end of this guide, your GitBook docs are a Blume project in the repository GitBook already syncs to: every page at the URL it has today, GitBook's blocks converted to Blume's components, reusable content as includes, and a redirect for every URL GitBook still answers. A coding agent does the conversion, and you review it.
It covers content that GitBook's Git Sync writes to GitHub or GitLab. A book built with the old open-source GitBook CLI or HonKit, with a book.json, migrates the same way, from the same skill.
What carries over
GitBook's content is already Markdown. The work is in three places: its URLs follow SUMMARY.md rather than where files sit, its blocks are {% … %} tags and HTML, and some of what readers see lives only in the GitBook app. On one real site with 134 pages, the run ended with every page built at its old URL and all 938 old URLs, translated pages included, still landing on a page.
| In GitBook | In Blume |
|---|---|
SUMMARY.md | Folders laid out like your URLs, with a meta.ts per folder for order and labels |
## Group headings | Top-level folders, shown as plain sidebar headings |
| A page with subpages | A folder whose index page is the parent |
{% hint %} | :::info, :::warning, and the other callouts |
{% tabs %} and {% stepper %} | <Tabs> and <Steps> |
{% content-ref %} | <Card>s, or a card listing Blume generates for a section's landing page |
<details> | <Accordion> or <Expandable> |
<figure> images | Markdown images, optimized at build, in a <Frame> when captioned |
Reusable content in .gitbook/includes/ | <include> partials in _includes/ |
Uploads in .gitbook/assets/ | An assets/ folder beside your pages |
| Variables and expressions | variables, or props on an include |
Redirects in .gitbook.yaml and in site settings | redirects in blume.config.ts |
Sections in gitbook-docs.yaml | Header tabs, one folder each |
| Font Awesome icons | Lucide icons |
Here's one page before and after the conversion:
---
description: Connect Acme to your identity provider.
---
# Single sign-on
{% hint style="warning" %}
SSO needs the **Business** plan.
{% endhint %}
{% stepper %}
{% step %}
### Create the app
In your identity provider, create a SAML app.
{% endstep %}
{% step %}
### Paste the metadata URL
{% code title="Metadata URL" %}
```
https://acme.example/saml/metadata
```
{% endcode %}
{% endstep %}
{% endstepper %}
<figure><img src="../.gitbook/assets/image (3).png" alt="The SAML settings"><figcaption><p>Turn on SAML.</p></figcaption></figure>
{% content-ref url="scim.md" %}
[scim.md](scim.md)
{% endcontent-ref %}---
title: Single sign-on
description: Connect Acme to your identity provider.
---
:::warning
SSO needs the **Business** plan.
:::
<Steps>
<Step>
### Create the app
In your identity provider, create a SAML app.
</Step>
<Step>
### Paste the metadata URL
```text title="Metadata URL"
https://acme.example/saml/metadata
```
</Step>
</Steps>
<Frame caption="Turn on SAML.">
.png)
</Frame>
<Card title="SCIM provisioning" href="/security/scim" />The # Title line becomes the title. Each step keeps its heading, so it stays in the page's outline and keeps its anchor. A code block with no language gets text, and the image path points at the moved assets/ folder.
Before you start
Work on a new branch, so the migration is one diff to review:
git switch -c migrate-to-blumeNext, make sure every space you publish is in the repository. Site-wide Git Sync maps each space to a directory in gitbook-docs.yaml; a space marked directory: null there, or synced only to another repository, has no files here. Translations GitBook made for you usually aren't synced either. Turn on Git Sync for each space you want to keep, or decide now to drop it. A dropped language can redirect to your default one, and you can bring it back later with blume translate.
Then save every URL your site serves. GitBook's sitemap.xml is an index with one sitemap per published space, so read each of them:
curl -s https://docs.acme.example/sitemap.xml \
| grep -o '<loc>[^<]*' | sed 's#<loc>##' \
| while read -r sitemap; do curl -s "$sitemap"; done \
| grep -o '<loc>[^<]*' \
| sed -e 's#<loc>https://docs.acme.example##' -e 's#^$#/#' \
> ../old-urls.txtReplace docs.acme.example with your docs domain in both places. Comparing the sitemaps' prefixes, like /de, with the spaces in your repository is also the quickest way to spot one that isn't synced.
Site redirects, set under Settings → Redirects, aren't in the repository, and the app can import them from CSV but not export them. With a GitBook API token, and the organization and site IDs from the site's address in the app (app.gitbook.com/o/ORG_ID/sites/SITE_ID), save them for the agent:
curl -s -H "Authorization: Bearer $GITBOOK_TOKEN" \
"https://api.gitbook.com/v1/orgs/$ORG_ID/sites/$SITE_ID/redirects?limit=1000" \
> ../gitbook-redirects.jsonIf the response ends with a next page, fetch again with &page= and its value. Each redirect's destination is a GitBook page, space, or section ID, or an external URL; the agent resolves the IDs to your old URLs with the same token. Without a token, copy them from the settings page. Also note what lives only in the app and you want to keep: your logo and favicon, primary color, fonts, header links, and footer. Finally, 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 synced repository, run:
npx blume migrate gitbook --claudeTo use Codex, swap --claude for --codex. Leave out gitbook and Blume detects the source from a .gitbook.yaml, .gitbook.yml, or gitbook-docs.yaml at the root, or a honkit or gitbook-cli dependency. A repository with only a SUMMARY.md isn't detected, since other tools use that file too, 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 GitBook reference. Following it, the agent:
- Works out each page's old URL from
SUMMARY.md, checks the list against the live sitemap, and moves every file to its URL, renaming eachREADME.mdtoindex. - Writes a
meta.tsper folder so the sidebar keeps GitBook's order, labels, and groups. - Converts every
{% … %}tag and the HTML GitBook writes, moves reusable content into_includes/, and renames pages that need components to.mdx. - Writes
blume.config.tsfrom what it can read off your published site, and adds the redirects GitBook answers today. - Builds, pins GitBook's heading ids with a bundled script, and runs
blume validate --strictandblume audit --only redirectsuntil 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 GitBook migration most often needs a second look.
- URLs follow
SUMMARY.md. In GitBook, a page's URL comes from its place in the table of contents, so a page you once dragged to another group kept its old file path. In Blume, the path is the URL. Spot-check a few files that moved: each should sit at the URL the page had, and the agent's summary should list every move. - The sidebar. Each group should be a plain heading and each page with subpages a single expandable row labeled with its title. Blume lists top-level pages above groups, so a page GitBook placed after a divider moves up, unless the agent put it in a group folder.
- No blocks left. A
{% … %}tag left in a.mdpage shows as literal text while the build passes, with only aBLUME_TEMPLATE_TAGwarning. This search should print only code samples that show the syntax on purpose:
grep -rnE '\{%|class="expression"|data-gb-custom|\.gitbook/' docs- Includes. Open a page that uses reusable content. Headings inside an include move down one level, since an include has no title of its own, and its images resolve from the include's folder.
- Accordions. An accordion's title renders inline Markdown, so code formatting and links from a
<summary>carry over when they're written as Markdown in itstitle. Raw HTML there shows as text. - Translations. If you dropped a language, open one of its old URLs. GitBook translates group names in URLs, like
/de/zahlungen/…, so each translated group needs its own redirect ahead of the language's catch-all:
redirects: [
// GitBook translated the group's slug, so map it before the catch-all.
{ from: "/de/zahlungen/:path*", to: "/payments/:path*", status: 307 },
{ from: "/de/:path*", to: "/:path*", status: 307 },
],- The repository. Look for CI and scripts that read
.mdfiles or.gitbook/assets/. A Markdown link checker won't understand Blume's routes or.mdxpages, so replace it withblume validate --strict. A script that removes unused uploads by searching*.mdwould now delete images still in use.
Heading anchors
GitBook and Blume build heading anchors differently. GitBook keeps dots and turns colons and slashes into hyphens, so a link to #api-keys-tokens or #id-2.4-may would land at the top of the page in Blume. The agent pins each old anchor on its heading with a script that ships in the package. After you edit headings, rerun it against your live GitBook site:
npx blume build
node node_modules/blume/skills/blume-migrate/scripts/pin-heading-ids.mjs \
--old https://docs.acme.example/api/auth "API keys/tokens" #api-keystokens → #api-keys-tokens (docs/api/auth.mdx:42)
/changelog "2.4 - May" #24---may → #id-2.4-may (docs/changelog.md:9)
412 heading(s) paired · 2 heading line(s) need a pin in 2 file(s) · 0 note(s)
Dry run: pass --write to apply.It reads the anchors GitBook published and the ones Blume built, and lists each heading whose anchor changed. Add --write to pin them, then rebuild and run it again until it reports none. Add --only-linked to pin only the anchors your pages link to. It fetches one page at a time and caches them in .pin-heading-ids-cache/, which you can delete when you're done.
Check every old URL
With npx blume dev running, walk the list you saved and print every old URL that doesn'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.txtRedirects count as passing, since -L follows them. The loop works against blume preview of a build too, which applies every redirect, pattern ones like the translations included. Add a redirect for each path the loop prints and run it again until it prints nothing.
GitBook also redirects the old URL of every page you moved or renamed in its editor, and keeps no list of them. The agent recovers the ones it can from the history of SUMMARY.md and checks each against your live site. If you know of other old links, such as from your analytics or search console, add them to the list and run the loop again. For status codes and patterns, 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 redirectsPick a host and follow Deployment. If you have pattern redirects, choose one that applies them: Netlify, Cloudflare, or Vercel with the vercel() adapter. A plain static host like GitHub Pages serves only exact redirects. On Netlify, set deployment: netlify({ output: "static" }) too, so a static build's exact redirects answer with an HTTP redirect rather than a redirect page. netlify() alone switches to a server build.
Turn off Git Sync before you merge. GitBook imports every push to the synced branch, so merging would publish the converted files on your GitBook site, and edits made in GitBook would keep landing in the repository. Turn it off for every synced space, then merge and deploy. Keep the GitBook site itself published until the switch is done.
Run the URL check against your preview deployment. Then add your docs domain to the new host and update the DNS record you created for GitBook so it points there instead. If your docs sit under a path on your main site, like example.com/docs, point that proxy at the new deployment and set deployment.base to the path. Rolling back is changing the record or proxy back, while GitBook still serves the old site. Once production passes the URL check, remove the custom domain from your GitBook site, and submit the new sitemap.xml in Google Search Console.
What doesn't carry over
- The hosted editor. GitBook's block editor and live edits stay with GitBook. In Blume, pages are files you edit in your own editor.
- Change requests. Reviews, comments, and merge rules become pull requests in your Git host, with a preview deployment for each.
- Site customization. Themes, tint colors, and sidebar styles don't map. Your logo, primary color, fonts, header links, and footer links move to
blume.config.ts, your favicon topublic/, and anything else can go in atheme.css. - AI search. Blume's search is built in. GitBook Assistant has no direct replacement, but Blume's assistant runs on a model provider you choose, with server output.
- Access control. Share links, authenticated access, and adaptive content have no Blume equivalent. Use your host's protection, which covers the whole site, so mixing public and private pages takes two sites.
- GitBook's extras. Page covers, color highlights, and button styles drop; embeds other than YouTube become link cards; and insights come from the analytics adapter you choose.
Troubleshooting
A page fails with "Could not parse expression"
A {% … %} tag or a bare brace is left in an .mdx page, and blume check reports it as BLUME_MDX_SYNTAX at its line. Convert the tag, or put text like {{DeviceId}} in inline code. GitBook's escaped form, \{{name\}}, still fails in MDX.
A page fails with BLUME_MDX_UNDEFINED_NAME
Braces inside raw HTML, like <code>CN={{DeviceId}}</code> in a table cell, parse as an expression and fail when the page renders. The error names the line. Write the cell's <code> as Markdown inline code.
A page fails with "Expected a closing tag"
GitBook writes <br> and <img> without closing them, which MDX rejects. Inside a <figure>, the error reads "Unexpected closing tag" instead. A BLUME_MDX_UNCLOSED_ELEMENT warning names each one's line. Write <br />, or turn the image into Markdown.
Next step
Migrate your docs
Run it at the root of the repository GitBook syncs to, on a clean branch, then work through the review above.
npx blume migrate gitbook --claudeA step here not working for you? Report a broken step.