Skip to content
Blume
Esc
↑↓navigate↵open⌘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 Codex or Claude Code.

Blume 2 changes configuration, not content: your Markdown and MDX pages need no edits (see Frontmatter for one field that now does something). 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 assistant’s model backend — are now adapters you import from a blume/* subpath and call. Ask AI is renamed the assistant, so ai.ask becomes ai.assistant. 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 --codex or --claude:

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

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 read :name segments and a trailing * as patterns, matched the same way on every host (see Pattern redirects). Blume 1 never supported patterns and handed them to each host as written, so check any from or to that holds one.
  • 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. A CI step that passed a different --base per deploy can read it from an environment variable in the config instead.

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.
  • Text typed in Notion renders as written now: a component, {, or Markdown character in a notion() page shows as typed instead of being read as MDX. If your Notion pages hold components or Markdown typed as text, set mdx: true on the source.

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({ … }).

Assistant

Ask AI is now called the assistant, and its config moves with the name: ai.ask becomes ai.assistant. Its 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: {
    assistant: {
      enabled: true,
      provider: openrouter({
        model: "anthropic/claude-sonnet-4-5",
        reasoning: "none",
      }),
    },
  },
});

A 1.x provider name maps onto gateway(), openrouter(), llmgateway(), or inkeep(), and openai-compatible onto openai({ baseUrl, name, model, apiKeyEnv }); the adapter list has the rest. model, apiKeyEnv, baseUrl, headers, and reasoning move into the adapter; enabled, instructions, retrieval, suggestions, cors, and endpoint move to ai.assistant unchanged. Leaving provider unset still uses the AI Gateway.

The rename reaches every name that said Ask AI:

  • i18n.ui overrides: the ask group becomes assistant, and search.askAi and search.askAiHint become search.assistant and search.assistantHint.
  • useAskAI from blume/hooks becomes useAssistant, with its UseAssistant and UseAssistantOptions types.
  • The askEnabled prop on PageLayout, RootLayout, Header, and a Search override becomes assistantEnabled.
  • In the blume:data module, config.ask becomes config.assistant and ui.ask becomes ui.assistant, and the UIStrings type from blume follows.
  • The adapter types from blume/ai swap their Ask prefix for Assistant (AskAdapter becomes AssistantAdapter, AskGatewayOptions becomes AssistantGatewayOptions), and askReasoningLevels and AskReasoning from blume/schema become assistantReasoningLevels and AssistantReasoning.
  • The blume:open-ask-ai window event becomes blume:open-assistant, and the data-blume-ask attribute on <body> becomes data-blume-assistant.

The generated /api/ask route and the ask, ask_answer, and ask_error analytics events keep their names, so callers and dashboards need no change. blume upgrade and blume doctor name each old config key they find, ai.ask and the i18n.ui keys alike, with its replacement.

Upgraded to Blume 2.0.0 already? It still read ai.ask, so update blume as usual and make the same rename.

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 assistant (formerly 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

Pages keep their frontmatter. One field changes what it does: search.boost. Blume 1 accepted it, but search never read it, so a page ranked the same without it. It now multiplies the page’s search relevance (see Ranking). Check any page that sets it: a boost you added and forgot now moves the page up.

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

Was this page helpful?