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

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 10 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 GitBookIn Blume
SUMMARY.mdFolders laid out like your URLs, with a meta.ts per folder for order and labels
## Group headingsTop-level folders, shown as plain sidebar headings
A page with subpagesA 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> imagesMarkdown 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 expressionsvariables, or props on an include
Redirects in .gitbook.yaml and in site settingsredirects in blume.config.ts
Sections in gitbook-docs.yamlHeader tabs, one folder each
Font Awesome iconsLucide 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.">

![The SAML settings](../assets/image%20(3).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-blume

Next, 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.txt

Replace 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.json

If 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 --claude

To 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:

  1. 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 each README.md to index.
  2. Writes a meta.ts per folder so the sidebar keeps GitBook's order, labels, and groups.
  3. Converts every {% … %} tag and the HTML GitBook writes, moves reusable content into _includes/, and renames pages that need components to .mdx.
  4. Writes blume.config.ts from what it can read off your published site, and adds the redirects GitBook answers today.
  5. Builds, pins GitBook's heading ids with a bundled script, and runs 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 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 .md page shows as literal text while the build passes, with only a BLUME_TEMPLATE_TAG warning. 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 its title. 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 .md files or .gitbook/assets/. A Markdown link checker won't understand Blume's routes or .mdx pages, so replace it with blume validate --strict. A script that removes unused uploads by searching *.md would 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.txt

Redirects 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 redirects

Pick 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 to public/, and anything else can go in a theme.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 --claude
Read the migration reference

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

Keep going.More guides.

  • 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.

  • 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.

Upgrade your docs with Blume.

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

npx blume init