Skip to content
Blume
Esc
navigateopen⌘Jpreview
On this page

blume@2.0.0

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

    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: truevercel(), 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.

    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.

    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:

    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:

    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:

    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 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 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 initnpm install, bunx blume initbun 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 hrefs 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.

Last updated on September 24, 2026

Was this page helpful?