---
changelog:
  category: Release
  version: blume@2.0.0
date: '2026-09-24T07:32:13Z'
seo:
  description: >-
    Blume 2 turns search, deployment, content sources, API references,
    analytics, and the Ask AI backend into adapters imported from blume/*
    subpaths, moves the…
title: blume@2.0.0
type: changelog
---
## Major Changes

- 0bcb729: Blume 2 turns search, deployment, content sources, API references, analytics, and the Ask AI backend into adapters imported from `blume/*` subpaths, moves the machine-readable settings from `ai` to a new `agents` key, and plans `components.ts` statically. Pages need no edits unless they set the removed `search.boost` frontmatter field. To upgrade a Blume 1 project, run `npx blume@latest upgrade` from the folder with `blume.config.ts`: it bumps `blume`, installs it, and lists every change still needed with its file, line, and replacement, and `--claude` or `--codex` hands that list to a coding agent. The [Upgrade to Blume 2](https://useblume.dev/docs/upgrading) guide covers each change below with before-and-after examples.
- c984250: Replace the `analytics` object with a list of adapters imported from `blume/analytics`, emitted in order:
  
  ```ts
  import { posthog, script, vercel } from "blume/analytics";
  
  export default defineConfig({
    analytics: [
      posthog({ key: "phc_…" }),
      vercel(),
      script({ src: "https://plausible.io/js/script.js", strategy: "defer" }),
    ],
  });
  ```
  
  Move each key to its adapter: `posthog: { key, host }` → `posthog({ key, host })`, `vercel: true` → `vercel()`, `cloudflare: { token }` → `cloudflare({ token })`, and each `scripts[]` entry → `script({ … })`. Every adapter forwards the options Blume doesn't name to the provider (into `posthog.init`, the Cloudflare beacon's JSON, or the Vercel component's props), and the object form fails validation with a hint pointing at the list.
- 7e270dd: Configure the Ask AI backend with an adapter from `blume/ai`: `ai.ask.provider` takes `gateway({ model })`, `openrouter({ model, reasoning })`, `llmgateway({ model })`, `inkeep({ model })`, or `openaiCompatible({ baseUrl, name, model, apiKeyEnv })`, and each owns its model, API key env var, `headers`, `reasoning` mapping, and a `providerOptions` passthrough to `streamText`. The flat `provider` name and the `model`, `apiKeyEnv`, `baseUrl`, `headers`, and `reasoning` fields on `ai.ask` are gone, and a config still using them fails naming the adapter call that replaces them; leaving `provider` unset still means the AI Gateway with `openai/gpt-5.5`.
  
  ```ts
  import { openrouter } from "blume/ai";
  
  export default defineConfig({
    ai: {
      ask: {
        enabled: true,
        // was: provider: "openrouter", model: "anthropic/claude-sonnet-4-5"
        provider: openrouter({ model: "anthropic/claude-sonnet-4-5" }),
      },
    },
  });
  ```
- 0bcb729: `@asyncapi/converter` is now an optional peer dependency instead of a dependency. Blume only uses it to lift an AsyncAPI 1.x or 2.x spec to 3.0, and it pulled `@asyncapi/parser`, Spectral, and Scarf's telemetry postinstall into every install — a build script pnpm 12 refuses to run, so `pnpm dlx blume` failed before Blume started. AsyncAPI 3.x specs need nothing. If an `asyncapi()` reference points at a 1.x or 2.x spec, install the converter (`npm install @asyncapi/converter`); without it, `blume build` fails with that install command instead of rendering the reference.
- 0bcb729: Every `blume` command now rejects a flag it doesn't take, instead of silently ignoring it: `blume build --isolatd` used to run a real, non-isolated build, and `blume validate --strcit` skipped strict mode. The error names the likely intended flag (`did you mean --isolated?`) and every flag the command takes. `blume audit --only` and `--skip` likewise reject a term that names no check or category (`--only link` suggests `links`), where a typo used to filter out every finding and pass the gate. A config that fails validation while a command runs is now reported as its diagnostic, with its line, instead of a raw stack trace (`blume version`, `blume eject`). A mistyped negation is suggested as a negation (`--no-strcit` → `--no-strict`), and a switch that's on by default is listed by the form that changes it (`--no-strict`).
- d6b4eaf: Component overrides in `components.ts` are planned statically, with no runtime fallback. Every `mdx` and `layout` entry must be an imported identifier, a path string, or a `{ component, client, media }` object literal; anything else — an inline function, a component declared in the file, a spread, a computed key — or an import of a file that doesn't exist is a `BLUME_COMPONENTS_INVALID` error at its line, reported by `blume dev`, `blume build`, and `blume doctor`. Such overrides used to render without hydration or any warning.
  
  The `islands` group is gone: an `mdx` entry with a `client` mode is an island, so `islands: { Counter }` becomes `mdx: { Counter: { component: Counter, client: "visible" } }`. The `islands/` folder convention is unchanged, and a `components.ts` `mdx` entry replaces a folder island of the same name. Generated islands move from `src/generated/islands/` to `src/generated/component-slots/`, in `blume eject` output too, where every import is relative so the ejected app builds from any checkout.
- 1199f92: Simplify four corners of `blume.config.ts`. Each removed or renamed field now fails validation with a hint that names its replacement.
  
  - `theme.layout` is removed. It only ever accepted `"sidebar"`, and nothing read it; delete the field.
  - `markdown.codeBlocks` is merged into `markdown.code`. The Shiki theme pair moves from `markdown.codeBlocks.theme` to `markdown.code.theme`, beside `icons` and `wrap`.
  - `lastModified` is a flat value: `false` (default), `"git"`, or `"frontmatter"`. `lastModified: true` becomes `"git"`, and `{ type: "git" }` / `{ type: "frontmatter" }` become the bare string.
  - The `ai` namespace now holds only the model-facing features, `ai.ask` and `ai.openInChat`. The machine-readable surface moves to a new top-level `agents` key: `ai.api`, `ai.catalog`, `ai.llmsTxt`, `ai.markdownComponents`, `ai.mcp`, `ai.skills`, `ai.webBotAuth`, and `ai.webmcp` become `agents.api`, `agents.catalog`, `agents.llmsTxt`, `agents.markdownComponents`, `agents.mcp`, `agents.skills`, `agents.webBotAuth`, and `agents.webmcp`, and `seo.agentReadability` and `seo.contentSignals` become `agents.agentReadability` and `agents.contentSignals`.
- d1eaced: Replace `deployment.adapter` and `deployment.output` with adapters imported from `blume/deploy`: `vercel()`, `netlify()`, `cloudflare()`, or `node()`, each taking `site`, `base`, and `output` plus any option of the underlying `@astrojs/*` adapter, or the plain `{ site, base }` form for a static build on any host. Naming a host adapter builds for the server there (pass `output: "static"` to stay static with that host's platform files), and leaving `deployment` unset is still a static build, so zero-config sites are unchanged.
  
  ```ts
  import { vercel } from "blume/deploy";
  
  export default defineConfig({
    // was: deployment: { output: "server", adapter: "vercel" }
    deployment: vercel({ isr: { expiration: 60 } }),
  });
  ```
  
  Server output is no longer inferred from the platform's environment — name the host adapter — while `site` detection on Vercel, Netlify, and Cloudflare Pages works as before. The `--adapter`, `--output`, and `--base` flags on `blume build` are gone, and passing one stops the build naming the `deployment` setting that replaces it. The old object form fails validation with a hint, and a build that uses a server feature on static output fails naming the host adapter to set, or telling you to drop a host adapter's `output: "static"`.
- 3cbf91e: `redirects` take exact paths: a `from` or `to` holding a `:param` segment or a `*` wildcard now fails config validation, naming the redirect. Patterns were never supported, and hosts disagreed on them — a static build wrote a literal `:slug` folder, Vercel and Netlify passed the pattern through, and a Cloudflare server build served the page instead of redirecting — so move pattern rules into your host's own config (`vercel.json`, `_redirects`) and list exact paths in `redirects`.
- 274f7b4: Replace the top-level `openapi`, `asyncapi`, and `graphql` config blocks with one `reference` list of adapters imported from `blume/reference`, rendered in order:
  
  ```ts
  import { graphql, openapi, scalar } from "blume/reference";
  
  export default defineConfig({
    reference: [
      openapi({ spec: "./openapi.yaml" }),
      graphql({
        spec: "./schema.graphql",
        endpoint: "https://api.example.com/graphql",
      }),
      scalar({ spec: "./legacy.yaml", route: "/legacy" }),
    ],
  });
  ```
  
  Each block's options move onto its factory unchanged, without `enabled` (leave an adapter out of the list to disable it), and the same kind can appear more than once with its own route. The Scalar embed is its own `scalar()` adapter instead of a `renderer` option: a 1.x `renderer: "scalar"` block becomes a separate `scalar({ spec, theme, … })` entry that forwards its other keys to the embed, while `openapi()` and `asyncapi()` always render Blume's own pages. The old keys and a leftover `renderer` fail validation with a hint naming the replacement.
- 81c0a17: Remove the `search.boost` frontmatter field. Search never read it, so a page that set it ranked exactly the same without it. A page that still sets it now fails frontmatter validation with a hint to delete the field.
- 61c390e: Replace the `search.provider` string and its credential blocks with adapters imported from `blume/search`: `orama()` (still the default), `flexsearch()`, `pagefind()`, `algolia({ appId, apiKey, indexName })`, `oramaCloud({ endpoint, apiKey, indexId })`, `typesense({ host, collection, apiKey })`, `mixedbread({ storeId })`, or `false` to turn search off. Pass the adapter directly, or as `search: { provider, popular, indexing }` beside curated links and indexing options. The hosted adapters forward any option Blume doesn't name to their SDK's search client (options must be JSON values), and the keyless ones take no options.
  
  Migration: `search: { provider: "algolia", algolia: { appId, indexName, searchApiKey } }` becomes `search: algolia({ appId, indexName, apiKey: searchApiKey })`, and the Orama Cloud, Typesense, and Mixedbread blocks map the same way, with the search-only key as `apiKey` everywhere; `provider: "pagefind"` becomes `pagefind()`, and `provider: "none"` becomes `search: false`. Admin keys stay in their env vars.
- d56c124: Replace the `content.sources` `{ type: "…" }` objects with adapters imported from `blume/sources`: `filesystem()`, `mdxRemote()`, `githubReleases()`, `sanity()`, `notion()`, `obsidian()`, or `custom(source)` for any `ContentSource`. Every other field moves into the call unchanged, each factory that takes options also accepts `prefix` and `pollInterval`, and a leftover `type` object fails validation naming its factory:
  
  ```ts
  import { filesystem, githubReleases } from "blume/sources";
  
  export default defineConfig({
    content: {
      sources: [
        filesystem({ root: "content" }),
        githubReleases({ owner: "acme", repo: "sdk", prefix: "changelog" }),
      ],
    },
  });
  ```
  
  Each adapter declares the SDK and env vars it needs — `notion()` needs `@notionhq/client` and `NOTION_TOKEN`, `sanity()` needs `@sanity/client` and `SANITY_TOKEN`, and `githubReleases()` needs `GITHUB_TOKEN`, as does `mdxRemote()` when it reads from GitHub — so the generated project, the secrets check, and `blume doctor` take them from the adapter. The top-level `content.root`, `include`, and `exclude` remain the zero-config shorthand for one `filesystem()` source but can't sit beside `sources`: move them into the `filesystem()` entry.

## Minor Changes

- 733405a: Add first-class analytics adapters for Adobe Analytics, Amplitude, Microsoft Clarity, Clearbit, Fathom, Google Analytics 4, Google Tag Manager, Heap, Hightouch, Hotjar, LogRocket, Mixpanel, Pirsch, Plausible, and Segment, alongside PostHog, Vercel, and Cloudflare:
  
  ```ts
  import { googleAnalytics, mixpanel, plausible } from "blume/analytics";
  
  export default defineConfig({
    analytics: [
      googleAnalytics({ id: "G-…" }),
      plausible({ domain: "docs.example.com" }),
      mixpanel({ token: "…", region: "eu" }),
    ],
  });
  ```
  
  Each adapter renders the provider's own install snippet from its public identifier, maps the options it names (`region` to Mixpanel's ingestion host, `host` to Plausible's script origin, `cdn` to Segment's custom domain, …), and forwards everything else verbatim — into the SDK's `init` options for the script-based providers and as `data-` attributes for the tag-based ones. Client-router navigations count as pageviews on every adapter: Segment and Hightouch get the same `astro:page-load` hook PostHog has, Mixpanel is initialized with URL-change tracking on, Fathom's tag defaults to `data-spa="auto"`, and the rest follow history changes on their own. Page feedback's custom event now reaches each of these providers through its client API as well (into a custom `dataLayer` when `googleTagManager()` names one), and Plausible gets its custom-event queue stub so an event fired before the deferred script loads isn't lost.
- 5e7c238: Add `contentful()`, `payload()`, and `strapi()` content source adapters to `blume/sources`. Each reads one content type or collection through the CMS's REST API, with no SDK to install, maps its fields to frontmatter through the same `fields` option Sanity uses, and lowers its rich text to Markdown (Contentful rich text, Payload's Lexical state, and Strapi's Blocks field), escaping it so what an author typed stays prose. Setting `serializers` on `contentfulSource` or `payloadSource` writes the body as MDX, so the components they return render; a Markdown text field passes through as written.
  
  `contentful()` reads `CONTENTFUL_ACCESS_TOKEN`, and under `--preview` reads drafts through the Preview API with `CONTENTFUL_PREVIEW_TOKEN` (failing clearly without one). `payload()` reads `PAYLOAD_API_KEY` and `strapi()` reads `STRAPI_API_TOKEN`; under `--preview` both stage unpublished documents with `draft: true`. Relative upload paths resolve against the CMS origin, `params` appends query parameters (`where[...]`, `filters[...]`), requests time out after 30 seconds, and `blume init` offers all three.
- 16678ba: New `markdown.externalLinks` option: set it to `true` to open external links written in Markdown (`[Status](https://status.example.com)`, autolinks, and reference-style links) in a new tab, in both `.md` and `.mdx`. Each gets `target="_blank"`, `rel="noreferrer"`, the arrow icon featured sidebar links already use, and a localized screen-reader note that it opens in a new tab. It is off by default, and site routes, `#fragments`, `mailto:`/`tel:` links, and raw `<a>` tags are never changed.
  
  Every link Blume itself opens in a new tab (header actions and CTA, the GitHub link, featured sidebar links, page actions, `Card`, `Tile`, `Tooltip`, and `GithubInfo`) now carries the same screen-reader note. `Card`, `Tile`, and `Tooltip` also decide what counts as external with the same rule as the header, so a protocol-relative `//host` link now opens in a new tab and a relative path that merely starts with `http` no longer does.
- 59b9bca: New `blume migrate [source]` command for moving a docs site from another framework to Blume. Run it in the site being migrated (`npx blume migrate fumadocs --claude`): it names the source — `mintlify`, `fumadocs`, `docusaurus`, `starlight`, or `nextra`, detected from the project's files when left out — then opens Claude Code (`--claude`) or Codex (`--codex`) on the `blume-migrate` skill bundled in the package, with that framework's mapping reference. Without an agent flag it prints the skill's path and the `npx skills add haydenbleasel/blume --skill blume-migrate` line for any other agent, and exits 0. A named source runs even when the project looks like another framework, with a warning naming the one detected. The new [Migrate to Blume](https://useblume.dev/docs/migrating) guide covers what the agent changes.
- 0bb92d9: New `blume upgrade` command for moving a project to a new major. Run it through the package runner (`npx blume@latest upgrade`), since a project on Blume 1 doesn't have it yet: it bumps `blume` in `package.json` (editing only that range), installs with the project's package manager, and lists every change still needed with its file, line, and replacement, exiting non-zero until none are left. It checks `blume.config.ts`, each `components.ts` entry, page frontmatter for removed fields, and `package.json` scripts that still pass removed `blume build` flags. `--claude` or `--codex` hands the list, with the new [Upgrade to Blume 2](https://useblume.dev/docs/upgrading) guide, to Claude Code or Codex; `--no-install` bumps without installing.

## Patch Changes

- 8469262: When a `blume eval` or `blume translate` agent run times out and exits on the SIGTERM, Blume now cancels the SIGKILL follow-up instead of sending it five seconds later to a process id the system may have reused.
- 0bcb729: The MCP server's `list_pages` tool and `resources/list`, and the JSON API's `/api/docs/pages.json`, no longer list i18n fallback copies. An untranslated page came back once per locale, English text tagged `fr` or `de` with nothing marking it untranslated, so a `locale` filter returned pages that aren't in that language and a `contentTypes` or `version` filter returned duplicates. The copies are left out as `llms.txt` and search already leave them out; `get_page` still reads a fallback URL.
- 0bcb729: Every built-in component now reaches agents as Markdown in the `.md` mirrors, `llms-full.txt`, MCP `get_page`, and search. `Accordion`, `AccordionItem`, `Expandable`, `FileTree`, `Columns`, `CodeGroup`, `Frame`, `Panel`, `Tile`, `Update`, `Prompt`, `GithubInfo`, `CodeBlock`, `Diff`, `Math`, `Tree`, `Color`, `Badge`, `Icon`, and `Tooltip` used to be left as raw JSX; each now renders what the component shows — an accordion item as its title over its answer, a file tree as its list, a tooltip's tip after its trigger, a code block or diff as a fence. `AutoTypeTable`, whose table comes from type-checking a source file, stays as JSX. A nested list inside a component's body (a `Step` with sub-items, say) also keeps its indentation instead of flattening to one level.
- 81c0a17: The MCP server's `search_docs` tool and the `/api/docs/search` endpoint no longer return i18n fallback copies. A page not yet translated came back once per locale, the same English text at every localized URL, while the site's own search already left those copies out.
- 0bcb729: The AI catalog's descriptions and sample queries read naturally for a site whose title already ends in "Docs": "get a page of the Acme Docs as Markdown" instead of "get a Acme Docs docs page as Markdown", and "Acme Docs API" instead of "Acme Docs docs API". An API reference's queries no longer say "API" twice ("what operations does the API Reference expose").
- 0bcb729: On a server build, a request for the JSON of a page that doesn't exist (`/api/docs/pages/nope.json`) now gets the `PAGE_NOT_FOUND` problem naming the route, instead of the generic `API_ROUTE_NOT_FOUND` for an unknown endpoint. Per-page JSON is prerendered, so a miss fell through to the API catch-all, which now recognizes the path.
- 3cbf91e: The Ask AI route reads a request body only up to 64 KB and answers anything larger with a `413`, declared length or not. It used to parse the whole body before checking the conversation's size, so on a self-hosted Node server one oversized request could take hundreds of megabytes of memory. The Mixedbread search endpoint reads its body under a 16 KB cap the same way.
- 3cbf91e: When Ask AI's API key isn't set, the chat panel now shows the route's own notice — "Ask AI is not configured: set AI_GATEWAY_API_KEY." — instead of "Sorry, something went wrong." The notice names only the environment variable, never a value; any other error still shows the generic message.
- 0bcb729: The generated Ask AI route streams its answer through the AI SDK's `createTextStreamResponse` and `toTextStream` helpers instead of the deprecated `result.toTextStreamResponse()`, so `blume check` no longer reports it, and it answers `503` rather than `500` when its API key isn't set: the route exists, but can't answer until the deployment provides the key.
- 0bcb729: The theme toggle and the dismissible banner work where the browser blocks storage (Safari's "Block All Cookies", sandboxed iframes). Reading the saved theme threw before the page registered its navigation listeners, so the theme dropped on every client-router navigation; toggling the theme threw after switching off CSS transitions, leaving them off for the rest of the page; and dismissing the banner did nothing. Now the page changes first and the choice is saved when storage allows, and without storage a toggled theme or a dismissed banner still holds across navigations for the rest of the visit.
- 7968d01: The generated `/changelog` index links each release to its default-locale page. Under i18n every locale serves a page for an entry, so the index could link a release to whichever locale's route came last in the manifest — the Portuguese permalink on the English index.
- 0bcb729: The generated `/changelog` index now has a Markdown form: `/changelog.md`, `Accept: text/markdown` on `/changelog`, and MCP `get_page` with `/changelog` return the release list the page shows — newest first, grouped by year, each release with its date, category, and link. The index is short enough for an agent to read in one go, but only the rendered HTML existed.
- 8639601: The generated `/changelog` index now lists every release as a single row — title, `category` tag, and date — grouped by year, instead of rendering each release's full notes inline. A long history stays a short page an agent can read in one context window, and each row links to the entry's own page, where the notes render in full. The major-version "Show N.x releases" reveal is gone with it, along with the `changelog.showReleases` UI string it used.
- 77fc861: A `/changelog` tab now opens the changelog timeline instead of the newest entry, with no `href` needed, including under a `basePath`, where the generated index is still served at `/changelog`.
- 0bcb729: `blume check` no longer fails on files Blume generates itself in a project without its own `tsconfig.json`, where Astro type-checks the whole generated `.blume` project under its strict settings. The sidebar fragment pages of a `page`- or `group`-display folder read a group-only property off a union, and the Mixedbread search endpoint read `text` off chunks that can be images, audio, or video; both now narrow first.
  
  In a fresh project the image asset route Blume generates no longer fails with `Cannot find name 'node:fs'`: its Node module types lived only in Blume's development dependencies, so Blume now depends on `@types/node` itself. A site with a Mermaid diagram no longer fails with `ts(2306)`, "File … mermaid-element.ts is not a module".
- ecd01a0: Diagnostics from `blume validate`, `blume audit`, and `blume eval` link to the commands' new pages in the CLI section of the docs, and frontmatter validation errors link to the page's new home under Content.
- 0bcb729: Command output fixes:
  
  - `blume preview` in a project where only `blume dev` has run says to run `blume build` first, instead of printing Astro's stack trace.
  - `blume check --strict` and `blume dev --strict`, which are strict only on request, now say to drop `--strict` to continue past errors, not to pass `--no-strict`.
  - `blume translate --check --json` lists every missing or stale translation as an error diagnostic, so its `summary` agrees with the non-zero exit.
  - `blume translate`, `blume audit`, and `blume eval` report an invalid config with its code, file, and line, like the other commands, instead of the bare message.
  - When the agent run itself fails in `blume eval`, the finding points at the question in the evals file and reads `run failed:`, instead of a `fix:` line naming a docs page that has nothing to fix.
  - `blume --help` carries the current tagline.
- 3cbf91e: A `cloudflare()` server build now deploys with `npx wrangler deploy` from the project root. The Cloudflare Vite plugin writes its pointer to the built Worker config inside Blume's hidden runtime, where wrangler never looks, so a deploy from the project root failed with "Could not detect a directory containing static files"; Blume now writes that pointer at the project root as well. The Worker is also named after the project — its `package.json` name, else the site's hostname, else the project folder — instead of `blume-runtime`, which every Blume site shared, so two sites deployed to one account no longer overwrite each other. A `name` set in a `wrangler.jsonc` at the project root still wins.
- c57c472: Cloudflare server builds now answer a missing page with the prerendered Markdown 404 (`/404.md`) when the client sends `Accept: text/markdown`, and with the JSON problem document (`/404.json`) when it prefers JSON — keeping the `404` status, as Vercel builds already did. To let a request for a URL no page backs reach the Worker at all, the generated `assets.run_worker_first` rules now claim every path except the fingerprinted build assets, the raw `.md`, `.mdx`, and `.txt` files, the `.well-known` files, and the JSON Blume writes at fixed paths, instead of only the content routes. JSON under `/api/` reaches the Worker, so a request for a page's JSON that doesn't exist answers with the API's `PAGE_NOT_FOUND` problem document instead of an empty 404.
- 0bcb729: A theme-aware `<Color.Item>` now copies the value for the active light or dark theme, instead of the `#ffffff / #0a0a0a` pair it displays. Its "Copied" confirmation and its accessible name are localized, the latter through a new `content.copyColor` string that every shipped language pack translates.
- 81c0a17: A config error for an unknown key now points at the line that sets that key, instead of at its parent object's line (or at no line at all for a top-level key), and when a config has several problems they're listed in the order they appear in the file.
- 3cbf91e: SVG images a content source downloads are served with `Content-Security-Policy: sandbox`, from the `/blume-assets` route in dev and server builds, from a `node()` server's entry, and through the `_headers` and `vercel.json` rules a static build writes. An SVG opened directly is a document, so one uploaded to a CMS with a `<script>` inside could otherwise run as the docs site; an `<img>` that embeds it renders as before.
- 81c0a17: Blume's own dependencies now install on every Node version Blume supports (`>=22.12.0`) without an `EBADENGINE` warning: it depends on `write-file-atomic` 7.x, whose code is identical to 8.0, and `undici` 7.x, since their 8.x releases require Node 22.22 and 22.19. The optional `@mixedbread/sdk` peer now accepts every 0.x release from 0.77, so a current SDK no longer draws an incorrect-peer warning.
- 3cbf91e: `blume dev` no longer crashes with "Cannot read properties of null (reading 'port')" when Astro restarts during startup. The first `blume dev` after a `blume build` rewrites the generated `astro.config.mjs` just before the server starts, and when Astro's watcher picked that write up mid-startup it restarted before listening, leaving no address to read; Blume now takes the port from the restarted server's own URL.
- d8fe3bc: A custom page on a dynamic route (`pages/compare/[tool].astro`) no longer sets an `og:image` pointing at a generated card that was never rendered. Generated cards cover static custom pages only, so pass `ogImage` to `PageLayout` for a dynamic page's social card.
- 0bcb729: `blume eject` no longer overwrites an app it already ejected: run again, it stops before touching `astro.config.mjs` or `src/`, since a fresh copy would discard every edit made since, and `--force` is the explicit way through. In an ejected app, `blume dev` and `blume build` stop too, pointing at the app's own `npm run dev`/`npm run build` (in the project's package manager), instead of regenerating `.blume/` and running that copy while the app's edits are ignored. An app ejected by Blume 1 is recognized as well. A config `blume eject` can't load, such as one still using Blume 1 fields, is now reported as the same diagnostics other commands print rather than a stack trace, and the summary box `blume build` prints lines its "Server features" row up with the others. Only the header `blume eject` writes (or the one Blume 1's eject wrote) marks a project as ejected, so a project with its own `astro.config.mjs` and a codegen `src/generated/` folder still runs through Blume.
- 81c0a17: `blume eject` now writes an Astro app that builds from any checkout, not only the directory it ran in. The ejected `astro.config.mjs` uses a relative `outDir` and `pages/` scan glob, hands a deploy adapter no absolute project root, and resolves its `blume:*` aliases against the config file when it loads, so Vite no longer prints a "not an absolute path" warning per alias on every build; island and example wrappers import their components by relative path; and the config's header no longer says it is recreated on each run. Eject also adds the packages the ejected app imports by name to `package.json` at the ranges Blume uses — `astro`, `@astrojs/mdx`, `@tailwindcss/vite`, the integrations and adapter SDKs the config wires in, and `react` and `react-dom` when React renders an island, example, or Ask AI, plus `ai` for the Ask AI route and `epub-gen-memory` when EPUB export is on — and its closing message lists the install command first, so `astro build` runs under pnpm, which resolves only a project's own dependencies.
- 3cbf91e: `blume eval --agent codex` now runs both the reader and the judge with Codex's shell, exec, and local-image tools turned off and no inherited environment, leaving them only the prompt and the docs MCP tools. Codex's read-only sandbox still let its shell tool read any file on the machine, so an instruction planted in remote docs content could have pulled a local secret into the transcript sent to the model provider.
- 3cbf91e: An untranslated page served under another locale marks its article with the language it's written in (`lang="en"` on English text at `/ar/…`), beside the text direction it already set, so screen readers read it with the right voice instead of the page locale's.
- 0bcb729: An i18n fallback page — an untranslated page rendered at another locale's URL — now points its canonical link at the page it copies, as an archived version points at the latest docs, so search engines never rank the copy against the original. It used to name itself as canonical while repeating the fallback locale's text. When the copied page is itself archived, the canonical names the latest docs page directly, never a chain.
- 0bcb729: Answering "Was this page helpful?" moves focus to the thank-you line and announces it, instead of dropping focus to the top of the page when the clicked button disappears. The language switcher's accessible name now includes the language it shows ("Language: English"), so a voice-control user can open it by what they see.
- 0bcb729: A sidebar group label inferred from its folder name now spells common acronyms and brand names the way they're written: an `api-reference` folder is "API Reference", not "Api Reference", and `faq`, `cli`, `sdk`, `mcp`, `json`, `graphql`, `oauth`, and similar read "FAQ", "CLI", "SDK", "MCP", "JSON", "GraphQL", and "OAuth". The same label heads the folder's section in `llms.txt`. A folder's `meta.ts` `title` still wins.
- 81c0a17: A page's top-level `icon` frontmatter now sets its sidebar icon when `sidebar.icon` is unset, like the top-level `hidden` and `noindex` shorthands. The key was accepted but never read, so a page migrated from Mintlify with `icon: download` showed no icon.
- 0bcb729: When a Vercel server build's function bundle is missing packages, the fix it prints now adds them with the project's own package manager (`pnpm add -D …`, `bun add -D …`, `yarn add -D …`) instead of always `npm install -D`. The missing packages are an isolated-linker problem, so the project running into it is usually not on npm.
- 8c26c89: A sidebar group whose folder index is also listed as one of its rows no longer highlights both the header and the row on that page. The header reads as current only when the index row is hidden and it is the section's sole link.
- 3cbf91e: A page titled like the site itself — often the home page — no longer repeats it in the browser tab ("Acme Docs - Acme Docs"); it takes the bare site title.
- 0bcb729: Click-to-zoom images (`markdown.imageZoom`) no longer leak across client-router navigations. Every page view created a new zoom instance, each adding keyboard, scroll, and resize listeners to the document that were never removed and keeping the previous pages' images in memory; one instance is now re-pointed at each page's images.
- 3cbf91e: Links in content Blume imports from someone else — an OpenAPI or AsyncAPI spec's descriptions, a `githubReleases()` repository's release notes, and rich text from Sanity, Contentful, Payload, and Strapi — now render only when they point at a web, mail, phone, or relative address. A `javascript:`, `data:`, or `vbscript:` link keeps its label as plain text instead of becoming a link that would run as the docs site when a reader clicked it. Pages you write yourself are unchanged.
- 0bcb729: An `<include>` statement wrapped across lines — the path on its own line between the opening and closing tags, as a formatter wraps a long one — now splices like a one-line statement in `.md` and `.mdx`, and on search, the `.md` mirrors, and `llms-full.txt`. A line that opens `<include` but isn't a statement Blume can read now raises a `BLUME_INCLUDE_MALFORMED` warning instead of rendering the raw tag without a word.
- 81c0a17: `BLUME_NAV_INDEX_TITLE_MISMATCH` now fires only when a folder's index page hides its own sidebar row, so the linked group header is the only sidebar label the page has. A visible index row already shows the page's own title beneath the header ("CLI" over "Overview"), so a site pairing the two on purpose no longer gets a warning per section and locale on every build.
- ce42d61: `blume init` now installs dependencies after scaffolding, with the package manager that ran it (`npx blume init` → `npm install`, `bunx blume init` → `bun install`) or the one given with `--package-manager`, so the new project runs `dev` straight away. Pass `--no-install` to only write the files; a failed install keeps the scaffold, prints the command to retry, and exits non-zero, and `--eject` now ejects in the same run. For pnpm, `init` writes a `pnpm-workspace.yaml` approving esbuild's build script, and for Yarn 2 or later a `.yarnrc.yml` with `nodeLinker: node-modules`, which Blume needs; inside an existing workspace it leaves the workspace's config alone and says what to add.
- 81c0a17: `blume init` fixes: the starter pages no longer repeat their frontmatter title as a body `# Introduction` heading, so each renders one `<h1>`, and the docs starter no longer tells you to run a bare `blume dev`, which isn't on `PATH`. In a folder whose `package.json` already exists — which `init` leaves alone — the next steps now add `blume` and any source SDK it doesn't list yet (`npm install blume`) and start the dev server through the package runner (`npx blume dev`) unless its `dev` script already runs Blume, instead of printing `npm run dev` for a script that isn't there. The Sanity source scaffolds `@sanity/client` `^8.6.1`, the major Blume is tested against. When `blume` is listed but was never installed, the next steps run the install first. Every starter page's description now falls within the length `blume audit` checks, the changelog starter links its intro to the changelog index, and the docs starter uses the current tagline, so an untouched scaffold audits clean apart from setting `deployment.site`.
- 81c0a17: `search: pagefind()` no longer fails every `blume build` in a project that installed Blume from npm with "Vite module runner has been closed". The same fault skipped the Algolia, Orama Cloud, and Typesense index sync with a "Search sync skipped" warning, left inline `` `code{:lang}` `` snippets unhighlighted, and stopped an ejected app's `astro build` from loading the Notion and Sanity SDKs or routing remote OpenAPI fetches through a configured proxy. Blume now loads these libraries in a way that works however Astro evaluated its config.
- d8fe3bc: On a multi-locale site, the header logo now links into the reader's locale, and the logo, header tabs, header links, and featured links move into that locale only when it serves the route. The brand link was always `/`, the default locale's start page, so a reader on `/en/...` who clicked it left the language they were reading. The configured `logo.href` (default `/`) is now treated as a default-locale path and moved into the reader's locale (`/` → `/en`, `/docs` → `/en/docs`) when that locale serves it, while absolute and protocol-relative hrefs pass through untouched; the `Logo` layout slot receives the active `locale` as a new prop. A tab or link to a custom page or the generated changelog index, which exist only at their own path, stays there instead of pointing at a localized URL that 404s.
- 3cbf91e: On a multi-locale site, the 404 page speaks the locale in the missing URL: `/ar/does-not-exist` shows the Arabic message, right to left, with its home link pointing at `/ar`, where it used to show the default locale's. Hosts serve one `404.html` for every missing URL, so the page carries each locale's message and switches to the one the URL names when it loads.
- 3cbf91e: Sites with Mermaid diagrams no longer print Vite's "Some chunks are larger than 500 kB" warning on every build. Mermaid's layout engine and core load in their own chunks, only on pages with a diagram, so the warning had nothing to fix; the threshold on those sites is now 2 MB.
- 0bcb729: Mermaid diagrams stop re-rendering after every client-side navigation: the theme script rewrites the page's theme attribute on each swap, usually to the same value, and each diagram rendered again in response. A real light/dark switch still re-renders them.
- 81c0a17: The `blume-migrate` skill now ships the oxfmt 0.67.0 directive patch in place of the 0.55.0 one, so a migrated project that formats with oxfmt also keeps titled fences like `:::warning[Heads up]` intact.
- 81c0a17: `blume build` now stops before generating anything when a configured adapter's package isn't installed — `algoliasearch` for `algolia()`, `@openrouter/ai-sdk-provider` for `openrouter()`, `@astrojs/netlify` for `netlify()`, a Vue or Svelte island's Astro integration — with one `BLUME_DEPENDENCY_MISSING` error that names every missing package and the command that installs them with the project's package manager (`pnpm add algoliasearch`). It used to warn and then fail inside Vite with an opaque `MISSING_EXPORT` or `ERR_MODULE_NOT_FOUND`. `blume dev` still warns and keeps serving. `blume doctor` now reports the same missing packages, plus any secret an enabled feature needs that isn't set (reading `.env` and `.env.local` first, as `dev` and `build` do), where it used to print "No problems found" for a project whose build would fail.
- 0bcb729: The mobile navigation drawer now behaves like the modal panel it is. Its toggle reports `aria-expanded` and names the drawer with `aria-controls`; Escape closes it; and while it is open the page behind it is `inert`, so Tab moves through the header and the drawer instead of into content the overlay covers. Closing it with focus inside, from Escape or the overlay's close button, returns focus to the toggle.
- 0bcb729: A sidebar section whose deferred contents fail to load (offline, or a deploy that dropped the fragment) no longer leaves the click dead. A drill-in row follows the section's own page link instead, and a collapsed group closes again so the next open retries, where both used to do nothing and raise an unhandled promise rejection.
- 3517912: A sidebar group heading that links to its folder's index page now takes the same hover and active pill as the page rows beneath it, instead of a narrower, offset one.
- 0bcb729: A `node()` server build now serves the `.well-known` discovery files with their registered media type and CORS header, as the Vercel, Netlify, and Cloudflare builds already did. The standalone server's static handler types a file by its extension alone and sends no CORS header, so `/.well-known/api-catalog` went out as `application/octet-stream` and a registry reading the AI catalog, ARD manifest, or MCP discovery files from another origin was blocked. `blume build` now puts a small wrapper in front of the server entry (`dist/server/entry.mjs`, with Astro's own entry beside it as `astro-entry.mjs`) that sets those headers before Astro handles the request; the entry's `handler` export, `astro preview`, and middleware mode keep working as before.
- 81c0a17: Pages from a `notion()` source now keep the text written in Notion as text. Notion pages are written as MDX, and the rich text went in unescaped, so a literal `{` in a paragraph opened a JSX expression that failed the whole page and a `<b>` became a real tag; each run is now escaped like the other CMS sources, and a run whose edge is a space keeps it outside its bold or italic markers. Code blocks keep their text verbatim in a fence longer than any backticks inside, and a page title, description, or slug, a toggle's title, and an image's alt text take the plain text instead of Markdown marks. A paragraph that opens with `import ` or `export ` no longer fails the page as an MDX import, and a line of only `-` or `=` or a typed `&copy;` stays text.
- 0bcb729: A generated API reference page's meta and social description no longer runs its summary into the sentence after it. OpenAPI summaries are usually title-like ("Get a flag"), so the description read "Get a flag Reference for the GET /flags/{id} endpoint in the Acme API."; the summary now ends with a period when it has no closing punctuation of its own, and so does each operation's line in the agent-facing tag listings ("— Get a flag. Deprecated.").
- 0bcb729: API reference pages rebuild byte-for-byte: the "Try it" playground and the AsyncAPI message composer derive their error-message ids from the operation they render instead of drawing a random one on every build.
- 7878932: Add consumer-facing agent guidance to the published package, directing coding agents to the bundled skills' `SKILL.md` files.
- 13582e9: Advertise the agent-discovery head links from every page shell, not just the docs pages: a landing page, the generated 404, or any other custom page built on `PageLayout`, and the API-reference pages `ReferenceLayout` renders, now carry the same `describedby` links (`llms.txt`, `agent-readability.json`) and `rel="ai-catalog"` / `rel="ard"` manifest pair. All three layouts default `discovery` from the `blume:data` snapshot, so a custom page that never passes the prop is covered instead of silently dropping out of the "every page's head" promise; `discovery={null}` still drops the links for one page. A `PageLayout` homepage also advertises its `/index.md` Markdown mirror as a `text/markdown` alternate, matching the homepage HTTP `Link` header. The block lives in one shared `DiscoveryLinks` partial.
- 3cbf91e: `search: pagefind()` indexes each page's article instead of its whole `<body>`. Excerpts used to open with the skip link, header, and language switcher, and a word from the chrome ("search", "skip", a locale name) matched every page, including the 404 and custom landing pages. Content pages now mark their article with `data-pagefind-body`, so Pagefind reads only the page's own text and leaves out every page without it — the 404, custom pages, and the generated changelog index, none of which the other search adapters index either.
- d8fe3bc: When the API playground's request is blocked by CORS, its message now names the Blume 2 setting to fix it — `playground: { proxy: true }` on the `openapi()` reference — instead of the removed `openapi.playground.proxy` key.
- 3cbf91e: The API playground's built-in proxy (`/_api-proxy`, for `openapi()` and `graphql()` references with `playground: { proxy: true }`) now sends every response it relays with `Content-Security-Policy: sandbox`, `X-Content-Type-Options: nosniff`, and `Cross-Origin-Resource-Policy: same-origin`, and marks an HTML or SVG response as a download. The proxy serves the documented API's bytes from the docs origin, so an API error page that echoed its input could run script as the docs site for anyone who opened a crafted proxy link. It also refuses a request body over 4 MB with a `413` before buffering it, where a self-hosted Node server used to read the whole body into memory first. The playground's own requests are unaffected.
- 3cbf91e: The API playground tells a mistyped server from a CORS problem. A bare relative server (`api.example.com` or `v1`) is refused before sending, with a note to enter an absolute URL or a path on the docs site, instead of sending the request to the docs site and showing its 404 page. When a send fails before any response, the CORS explanation (and its `playground: { proxy: true }` advice) appears only when the API's origin answers a follow-up probe; a host that doesn't answer, and any failure through the proxy, reads "Couldn't reach the API" instead.
- 0bcb729: `<Prompt>`'s copy button and "Open in Cursor" link now hand over the prompt as Markdown — links with their URLs, list markers, emphasis, and fenced code — instead of its bare text, which dropped every link target and flattened lists. The button and link labels are localized with the rest of the UI, using a new `content.copyPrompt` string that every shipped language pack translates. Tables copy as GFM tables, alignment and all.
- 3cbf91e: `blume-redirects.json` is written only for a static build with no host named, as documented. A static build for `vercel()`, `netlify()`, or `cloudflare()` gets that host's own redirect file alone.
- 0bcb729: Relative links in Markdown and MDX now render as the root-relative route they point at. Left as written, a browser resolved them against the page's slashless URL, so `[Install](./install)` on a folder's `index` page (`/guides`) went to `/install`, and a link to a file (`[Setup](./setup.md)`) opened its raw Markdown. Each relative link now resolves the way `blume validate` checks it — from the page's own folder, with a `.md` or `.mdx` link landing on the route that file publishes at, `slug` included — and `blume audit` reads relative `href`s the way a browser does, so all three agree. A component's string `href` (`<Card href="./install">`) and a dotted page name (`./v1.2`) resolve the same way, a link to a sibling that isn't translated yet lands on the default locale's page in the reader's locale, and the `.md` mirrors, `llms-full.txt`, and MCP `get_page` get the same rewrite.
- 81c0a17: Release pages from a `githubReleases()` source now open their notes at h2. Changesets starts every section at `### Patch Changes`, so each page jumped from its h1 title straight to an h3, which `blume audit` flags on every release; the notes' headings are now lifted together until the shallowest is an h2, and a `#` inside a code block is left alone.
- 2b948ca: Changelog pages from a `githubReleases()` source are no longer copied to every other locale's URL, and no longer show the language switcher. Release notes publish in one language, so the copies repeated the same text at URLs missing from the sitemap and llms.txt.
- 3cbf91e: A built site now loads each package Blume, Astro, and the deploy adapter import from the copy the importing package declared, instead of whichever copy the package manager hoisted to the project root. Under npm, `blume build` failed with "does not provide an export named 'binaryTag'" once another package put `js-yaml@4` at the root, and installing `@astrojs/netlify` or `@astrojs/cloudflare` was enough to do that. Yarn Classic failed the same way, and Yarn Berry failed on a `cookie` conflict. Under pnpm, a `netlify()` build crashed in the adapter's file trace, a `vercel()` build failed its function-bundle check, and a `node()` server built but couldn't start ("Cannot find package 'zod'"). Each server-side build output now carries a `node_modules` of links to exactly the packages its bundles import. A project installed with Yarn Plug'n'Play, which creates no `node_modules`, now stops with a `BLUME_YARN_PNP` error naming the fix (`nodeLinker: node-modules` in `.yarnrc.yml`) instead of an error that read like a bug in Blume.
- 81c0a17: Builds no longer print Rolldown's nine-line `MODULE_LEVEL_DIRECTIVE` warning about `"use astro:head-inject"` for every Markdown page. Astro opens each page's asset module with that directive and nothing reads it after bundling, but Rolldown 1.2.10 and later, which a fresh install resolves, warns about it on every build. The generated Astro config, and an ejected one, now drops that one warning and passes every other build log through.
- 3cbf91e: In a right-to-left locale, the banner keeps its message's punctuation where it was written (an English sentence's closing period no longer jumps to its start), and the search button's shortcut reads "⌘K", not "K⌘", as does the search dialog's "⌘J" hint.
- 81c0a17: A `sanitySource` with `serializers` now writes its entries as MDX, so a serializer that returns a Blume component (`<Callout>…</Callout>`) renders it instead of leaving the raw tag in a Markdown page. The rest of the lowered text stays valid MDX: `{` and `}` in a document's text are escaped, a block with no serializer is noted in an MDX comment, and an image's alt text is escaped like the surrounding prose.
- 4a8e9f8: Bump `@scalar/astro` to 0.4.21, which accepts Astro 7 as a peer, so a fresh project no longer installs with an "incorrect peer dependency" warning (or, under npm, a second nested copy of Astro).
- 0bcb729: Search loads its client and index once per visit instead of once per page. Each client-router navigation rebuilds the header, and the first search after it fetched `blume-search.json` again and rebuilt the index — on a large site, a megabyte-plus download and a visible pause before results on every page. A failed load still retries on the next open.
- 0bcb729: The browser's Back button works again after opening a tab or an accordion. Clicking a `<Tabs>` tab, or opening an `<AccordionItem>` or `<Expandable>` (including one opened from a `#hash` link on load), replaced the page's history entry with an empty state, which Astro's client router ignores — so going Back to that page changed the URL but left the next page on screen. The URL is still updated to the open tab or item, now keeping the router's state.
- 0bcb729: A `<Tooltip>` stays open while focus is anywhere inside it, so a keyboard reader can Tab from the underlined term to its call-to-action link instead of watching the panel vanish, and Escape now dismisses it until the pointer or focus leaves. Its element id is derived from the tooltip's content rather than drawn at random, so rebuilding an unchanged page produces the same HTML.
- 4b9ce3e: Add a `transparentHeader` prop to `PageLayout`. The header starts with no background and its chrome in white, so it can sit over a dark hero at the top of a landing page, and returns to the frosted bar as soon as the page scrolls; the search dialog keeps the page's own colors throughout.
- 3cbf91e: `blume validate` now checks a component's string `href` (`<Card href="/guides/setup">`, `<Tile href="./install">`) the way it checks a Markdown link, including an `href` a formatter wraps onto its own line, so a broken card or tile link is reported at its line instead of shipping. An expression-valued `href={…}` and a raw HTML `<a>` tag are left alone.
- 3cbf91e: A static build deployed to Vercel — `vercel({ output: "static" })`, or a static build with no host named — now carries the response headers the other hosts get from `_headers`, as a `headers` block in `dist/vercel.json`: `charset=utf-8` on the raw `.md`, `.mdx`, and `.txt` files, the homepage `Link` header, and the media types and CORS header of the `.well-known` discovery files. The file is written even when the site has no redirects.
- 0bcb729: The first `blume version <id>` now turns versioning on in `blume.config.ts`, adding a `versions` block with the id archived and the live docs labeled "Latest". It used to only print a snippet — with a placeholder `current: { label: "…" }` to fill in — so until it was pasted, the snapshot built as ordinary content and every page shipped twice. When the config can't be edited safely, the snippet is now a warning saying so, and `blume doctor` warns about a version-shaped folder (`v1.0/`) on a site with no versioning configured.
- 3cbf91e: The header's version menu stays on screen on a phone. Anchored to its trigger's edge, it opened partly off the leading edge at narrow widths (off the trailing edge in a right-to-left locale), cutting off the version names; an opened selector panel is now shifted back inside the viewport. The trigger also shows a short current label, such as the version, on a phone, where it used to show only a chevron.
- 0fdffe9: Copy through a surfaced adapter bundle's symlinks when Windows refuses to recreate them, share one download between pages that reference the same asset, and bound `blume eject`'s package-manager lookup at the repository root so a workspace package still finds its lockfile.
