Skip to content
Blume
English
Esc
navigateopen⌘Jpreview
On this page

Upgrade to Blume 2

Move a Blume 1 site to Blume 2 with one command, then use this guide for each config change — or hand the whole upgrade to Claude Code or Codex.

Blume 2 changes configuration, not content: your Markdown and MDX pages need no edits, unless one sets the removed search.boost frontmatter field (see Frontmatter). Settings that used to be a named string or a keyed block — the search provider, the deployment target, content sources, API references, analytics, and the Ask AI backend — are now adapters you import from a blume/* subpath and call. The machine-readable settings move from ai to a new agents key, and components.ts overrides are checked before the build. A zero-config site, or one that sets none of these, only needs the version bump.

Upgrade with one command

Run the upgrade from your project, the folder with blume.config.ts:

npx blume@latest upgrade
pnpm dlx blume@latest upgrade
yarn dlx blume@latest upgrade
bunx blume@latest upgrade
nubx blume@latest upgrade
aube dlx blume@latest upgrade

It bumps blume in your package.json to 2, installs it with the package manager your project uses, then checks your config and components.ts against Blume 2. Every change that’s still needed is listed with its file, line, and replacement — including package.json scripts that still pass the removed blume build flags — and the command exits non-zero until none are left. Run from a folder with neither a config nor a blume dependency, it stops with an error instead. Run it through npx blume@latest rather than blume: the command ships in Blume 2, so a project still on 1 doesn’t have it yet. On pnpm 12, add --allow-build=esbuild after pnpm dlx, since pnpm 12 won’t run esbuild’s install script unapproved.

To hand the changes to a coding agent instead, add --claude or --codex:

npx blume@latest upgrade --claude
pnpm dlx blume@latest upgrade --claude
yarn dlx blume@latest upgrade --claude
bunx blume@latest upgrade --claude
nubx blume@latest upgrade --claude
aube dlx blume@latest upgrade --claude

The agent opens interactively with the findings and this guide, applies each change, and runs blume doctor and blume build until both pass, so you review every edit through its own permission flow. Pass --no-install to bump package.json without installing.

The sections below cover each change, for upgrading by hand or checking what the agent did.

search takes an adapter from blume/search instead of a provider string with a credentials block. The default local search needs no change.

export default defineConfig({
  search: {
    provider: "algolia",
    algolia: { appId: "APP_ID", indexName: "docs", searchApiKey: "SEARCH_KEY" },
  },
});
import { defineConfig } from "blume";
import { algolia } from "blume/search";

export default defineConfig({
  search: algolia({ appId: "APP_ID", indexName: "docs", apiKey: "SEARCH_KEY" }),
});
  • Orama Cloud, Typesense, and Mixedbread map the same way to oramaCloud(), typesense(), and mixedbread(). The search-only key is apiKey in every adapter that takes one; mixedbread() takes storeId rather than a key, since its queries run on the docs server; any other option it’s given is forwarded to the store search call, where top_k defaults to 8. Admin keys stay in their env vars (ALGOLIA_ADMIN_API_KEY, ORAMA_PRIVATE_API_KEY, TYPESENSE_ADMIN_API_KEY, MIXEDBREAD_API_KEY).
  • provider: "pagefind" becomes pagefind(), and provider: "none" becomes search: false.
  • To keep popular links or indexing options, wrap the adapter: search: { provider: algolia({ … }), popular: […] }.

Deployment

deployment takes a host adapter from blume/deploy instead of adapter and output fields. site and base move into the adapter’s options.

export default defineConfig({
  deployment: {
    adapter: "vercel",
    output: "server",
    site: "https://docs.example.com",
  },
});
import { defineConfig } from "blume";
import { vercel } from "blume/deploy";

export default defineConfig({
  deployment: vercel({ site: "https://docs.example.com" }),
});
  • netlify(), cloudflare(), and node() work the same way. Naming a host adapter switches to server output; pass output: "static" to keep a static build with that host’s platform files.
  • A config that only sets site or base stays as it is: deployment: { site, base } is still the static form.
  • redirects take exact paths. A from or to with a :param segment or a * wildcard now fails validation; Blume 1 never supported patterns, and hosts treated them differently. Move pattern rules into your host’s own config (vercel.json, _redirects).
  • The --adapter, --output, and --base flags on blume build are gone, and passing one stops the build with an error naming the deployment setting that replaces it. Set the adapter in blume.config.ts, and name it explicitly: server output is no longer inferred from the platform’s environment.

See Deployment for each adapter’s options.

Content sources

Each content.sources entry is an adapter from blume/sources instead of a { type } object.

export default defineConfig({
  content: {
    sources: [
      { type: "filesystem", root: "content" },
      {
        type: "github-releases",
        owner: "acme",
        repo: "sdk",
        prefix: "changelog",
      },
    ],
  },
});
import { defineConfig } from "blume";
import { filesystem, githubReleases } from "blume/sources";

export default defineConfig({
  content: {
    sources: [
      filesystem({ root: "content" }),
      githubReleases({ owner: "acme", repo: "sdk", prefix: "changelog" }),
    ],
  },
});
  • mdx-remote, sanity, notion, and obsidian become mdxRemote(), sanity(), notion(), and obsidian(), with every other field moving into the call unchanged. { type: "custom", source } becomes custom(source).
  • content.root, content.include, and content.exclude are still the shorthand for a single folder, but they can’t sit beside sources any more. Move them into the filesystem() entry.
  • Release pages from githubReleases() publish in one language now, so a multi-locale site no longer copies them to every other locale’s URL (/de/changelog/…). If other sites link to those copies, add redirects to the default-locale pages.

API references

The top-level openapi, asyncapi, and graphql blocks become one reference list of adapters from blume/reference. Drop enabled.

export default defineConfig({
  openapi: { enabled: true, spec: "./openapi.yaml" },
  graphql: {
    enabled: true,
    spec: "./schema.graphql",
    endpoint: "https://api.example.com/graphql",
  },
});
import { defineConfig } from "blume";
import { graphql, openapi } from "blume/reference";

export default defineConfig({
  reference: [
    openapi({ spec: "./openapi.yaml" }),
    graphql({
      spec: "./schema.graphql",
      endpoint: "https://api.example.com/graphql",
    }),
  ],
});
  • asyncapi: { … } becomes asyncapi({ … }) with the same options.
  • An AsyncAPI 1.x or 2.x spec is still converted to 3.0 for you, but the converter is now an optional peer: install @asyncapi/converter in your project, or the build fails with the install command. A 3.x spec needs nothing.
  • renderer: "scalar" becomes its own scalar({ spec, theme, … }) entry in the list, keeping the block’s route and sources.
  • A block with enabled: false is simply left out of the list.

Analytics

The analytics object becomes a list of adapters from blume/analytics.

export default defineConfig({
  analytics: {
    posthog: { key: "phc_…" },
    vercel: true,
  },
});
import { defineConfig } from "blume";
import { posthog, vercel } from "blume/analytics";

export default defineConfig({
  analytics: [posthog({ key: "phc_…" }), vercel()],
});

cloudflare: { token } becomes cloudflare({ token }), and each scripts[] entry becomes script({ … }).

Ask AI

ai.ask.provider takes an adapter from blume/ai, which owns the model and the fields that went with it.

export default defineConfig({
  ai: {
    ask: {
      enabled: true,
      provider: "openrouter",
      model: "anthropic/claude-sonnet-4-5",
      reasoning: "none",
    },
  },
});
import { defineConfig } from "blume";
import { openrouter } from "blume/ai";

export default defineConfig({
  ai: {
    ask: {
      enabled: true,
      provider: openrouter({
        model: "anthropic/claude-sonnet-4-5",
        reasoning: "none",
      }),
    },
  },
});

The adapters are gateway(), openrouter(), llmgateway(), inkeep(), and openaiCompatible({ baseUrl, name, model, apiKeyEnv }). model, apiKeyEnv, baseUrl, headers, and reasoning move into the adapter; enabled, instructions, retrieval, suggestions, cors, and endpoint stay on ai.ask. Leaving provider unset still uses the AI Gateway.

Agents and other config moves

The machine-readable settings move from ai to a new agents key, and three smaller fields change shape.

export default defineConfig({
  ai: { mcp: { enabled: true }, skills: "./skills" },
  lastModified: true,
  markdown: {
    codeBlocks: { theme: { light: "github-light", dark: "github-dark" } },
  },
  theme: { layout: "sidebar" },
});
export default defineConfig({
  agents: { mcp: { enabled: true }, skills: "./skills" },
  lastModified: "git",
  markdown: {
    code: { theme: { light: "github-light", dark: "github-dark" } },
  },
});
  • ai.api, ai.catalog, ai.llmsTxt, ai.markdownComponents, ai.mcp, ai.skills, ai.webBotAuth, and ai.webmcp become agents.*, and so do seo.agentReadability and seo.contentSignals. ai keeps only ask and openInChat.
  • lastModified is a flat value: true becomes "git", and { type: "git" } or { type: "frontmatter" } becomes the bare string.
  • markdown.codeBlocks merges into markdown.code.
  • theme.layout is gone. Nothing read it, so delete it.

Frontmatter

One frontmatter field is gone: search.boost. Blume 1 accepted it, but search never read it, so a page ranked the same without it. Delete it wherever it appears; a page that still sets it fails validation with a hint, and blume upgrade lists each one with its file and line.

Component overrides

Blume 2 checks every components.ts entry before the build instead of falling back at runtime. Each mdx and layout entry must be an imported component, a path string, or a { component, client, media } object, and the islands group is gone: an mdx entry with a client mode is an island.

import { defineComponents } from "blume";
import Counter from "./islands/Counter.tsx";

export default defineComponents({
  islands: { Counter },
});
import { defineComponents } from "blume";
import Counter from "./islands/Counter.tsx";

export default defineComponents({
  mdx: { Counter: { component: Counter, client: "visible" } },
});

An inline function, a component declared in components.ts itself, a spread, or a computed key now fails with BLUME_COMPONENTS_INVALID, naming the entry. Move the component into its own file and import it. The islands/ folder convention works as before. See Customization for the accepted forms.

Ejected apps

An app you ejected on Blume 1 no longer runs through the Blume CLI, but it still depends on the blume package. Its pages import Blume’s components, src/generated/ holds a snapshot of your site written by the Blume 1 generator, and astro build loads blume.config.ts again to write the search index, llms.txt, and the sitemap. Bumping blume to 2 under it pairs that Blume 1 snapshot with Blume 2 components that expect the new shapes, so eject again instead:

Copy the project out

Copy everything except astro.config.mjs, src/, .blume/, dist/, and node_modules/ into an empty folder: your content, blume.config.ts, components.ts, islands/, public/, any spec files your references read, and package.json. Leave the ejected app as it is.

Upgrade the copy

In the copy, run npx blume@latest upgrade and apply what it lists, so blume.config.ts and components.ts are valid Blume 2. Then run npx blume build to confirm the site builds before you eject it.

Eject a fresh copy

Run npx blume eject --yes in the copy, install the packages it adds, and build it with npm run build.

Carry your edits across

Diff the fresh astro.config.mjs and src/ against your ejected app, and move your own changes onto the new files.

Until the fresh copy is ready, keep the ejected app on Blume 1 ("blume": "^1") and don’t run blume upgrade in it: nothing changes until you bump it.

Command-line flags

Every blume command now rejects a flag it doesn’t take, where Blume 1 ignored it. A script or CI step that passes a stray or misspelled flag fails, naming the flag it didn’t recognize and the ones the command accepts. blume upgrade reports only the three removed blume build flags (--adapter, --output, --base), so check your other blume scripts too.

Check your work

Once blume upgrade reports nothing left to change, run the site’s own checks:

npx blume doctor
npx blume build

The full list of changes, with the reasoning behind each, is in the changelog.

Last updated on September 24, 2026

Was this page helpful?