---
title: Upgrade to Blume 2
description: 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.
sidebar:
  label: Upgrade to Blume 2
  order: 2.5
---

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](#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`:

```package-install
npx 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`:

```package-install
npx 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

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

```ts title="Blume 1"
export default defineConfig({
  search: {
    provider: "algolia",
    algolia: { appId: "APP_ID", indexName: "docs", searchApiKey: "SEARCH_KEY" },
  },
});
```

```ts title="Blume 2"
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.

```ts title="Blume 1"
export default defineConfig({
  deployment: {
    adapter: "vercel",
    output: "server",
    site: "https://docs.example.com",
  },
});
```

```ts title="Blume 2"
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](/docs/deployment) for each adapter's options.

## Content sources

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

```ts title="Blume 1"
export default defineConfig({
  content: {
    sources: [
      { type: "filesystem", root: "content" },
      {
        type: "github-releases",
        owner: "acme",
        repo: "sdk",
        prefix: "changelog",
      },
    ],
  },
});
```

```ts title="Blume 2"
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](/docs/deployment#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`.

```ts title="Blume 1"
export default defineConfig({
  openapi: { enabled: true, spec: "./openapi.yaml" },
  graphql: {
    enabled: true,
    spec: "./schema.graphql",
    endpoint: "https://api.example.com/graphql",
  },
});
```

```ts title="Blume 2"
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`.

```ts title="Blume 1"
export default defineConfig({
  analytics: {
    posthog: { key: "phc_…" },
    vercel: true,
  },
});
```

```ts title="Blume 2"
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.

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

```ts title="Blume 2"
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.

```ts title="Blume 1"
export default defineConfig({
  ai: { mcp: { enabled: true }, skills: "./skills" },
  lastModified: true,
  markdown: {
    codeBlocks: { theme: { light: "github-light", dark: "github-dark" } },
  },
  theme: { layout: "sidebar" },
});
```

```ts title="Blume 2"
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.

```ts title="Blume 1"
import { defineComponents } from "blume";
import Counter from "./islands/Counter.tsx";

export default defineComponents({
  islands: { Counter },
});
```

```ts title="Blume 2"
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](/docs/configuration/customization) for the accepted forms.

## Ejected apps

An app you [ejected](/docs/configuration/customization#eject) 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:

1. **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.

2. **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.

3. **Eject a fresh copy**

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

4. **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:

```bash
npx blume doctor
npx blume build
```

The full list of changes, with the reasoning behind each, is in the [changelog](/changelog).
