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 fromaito a newagentskey, and planscomponents.tsstatically. Pages need no edits unless they set the removedsearch.boostfrontmatter field. To upgrade a Blume 1 project, runnpx blume@latest upgradefrom the folder withblume.config.ts: it bumpsblume, installs it, and lists every change still needed with its file, line, and replacement, and--claudeor--codexhands that list to a coding agent. The Upgrade to Blume 2 guide covers each change below with before-and-after examples. -
c984250: Replace the
analyticsobject with a list of adapters imported fromblume/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: true→vercel(),cloudflare: { token }→cloudflare({ token }), and eachscripts[]entry →script({ … }). Every adapter forwards the options Blume doesn’t name to the provider (intoposthog.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.providertakesgateway({ model }),openrouter({ model, reasoning }),llmgateway({ model }),inkeep({ model }), oropenaiCompatible({ baseUrl, name, model, apiKeyEnv }), and each owns its model, API key env var,headers,reasoningmapping, and aproviderOptionspassthrough tostreamText. The flatprovidername and themodel,apiKeyEnv,baseUrl,headers, andreasoningfields onai.askare gone, and a config still using them fails naming the adapter call that replaces them; leavingproviderunset still means the AI Gateway withopenai/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/converteris 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, sopnpm dlx blumefailed before Blume started. AsyncAPI 3.x specs need nothing. If anasyncapi()reference points at a 1.x or 2.x spec, install the converter (npm install @asyncapi/converter); without it,blume buildfails with that install command instead of rendering the reference. -
0bcb729: Every
blumecommand now rejects a flag it doesn’t take, instead of silently ignoring it:blume build --isolatdused to run a real, non-isolated build, andblume validate --strcitskipped strict mode. The error names the likely intended flag (did you mean --isolated?) and every flag the command takes.blume audit --onlyand--skiplikewise reject a term that names no check or category (--only linksuggestslinks), 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.tsare planned statically, with no runtime fallback. Everymdxandlayoutentry 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 aBLUME_COMPONENTS_INVALIDerror at its line, reported byblume dev,blume build, andblume doctor. Such overrides used to render without hydration or any warning.The
islandsgroup is gone: anmdxentry with aclientmode is an island, soislands: { Counter }becomesmdx: { Counter: { component: Counter, client: "visible" } }. Theislands/folder convention is unchanged, and acomponents.tsmdxentry replaces a folder island of the same name. Generated islands move fromsrc/generated/islands/tosrc/generated/component-slots/, inblume ejectoutput 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.layoutis removed. It only ever accepted"sidebar", and nothing read it; delete the field.markdown.codeBlocksis merged intomarkdown.code. The Shiki theme pair moves frommarkdown.codeBlocks.themetomarkdown.code.theme, besideiconsandwrap.lastModifiedis a flat value:false(default),"git", or"frontmatter".lastModified: truebecomes"git", and{ type: "git" }/{ type: "frontmatter" }become the bare string.- The
ainamespace now holds only the model-facing features,ai.askandai.openInChat. The machine-readable surface moves to a new top-levelagentskey:ai.api,ai.catalog,ai.llmsTxt,ai.markdownComponents,ai.mcp,ai.skills,ai.webBotAuth, andai.webmcpbecomeagents.api,agents.catalog,agents.llmsTxt,agents.markdownComponents,agents.mcp,agents.skills,agents.webBotAuth, andagents.webmcp, andseo.agentReadabilityandseo.contentSignalsbecomeagents.agentReadabilityandagents.contentSignals.
-
d1eaced: Replace
deployment.adapteranddeployment.outputwith adapters imported fromblume/deploy:vercel(),netlify(),cloudflare(), ornode(), each takingsite,base, andoutputplus 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 (passoutput: "static"to stay static with that host’s platform files), and leavingdeploymentunset 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
sitedetection on Vercel, Netlify, and Cloudflare Pages works as before. The--adapter,--output, and--baseflags onblume buildare gone, and passing one stops the build naming thedeploymentsetting 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’soutput: "static". -
3cbf91e:
redirectstake exact paths: afromortoholding a:paramsegment 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:slugfolder, 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 inredirects. -
274f7b4: Replace the top-level
openapi,asyncapi, andgraphqlconfig blocks with onereferencelist of adapters imported fromblume/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 ownscalar()adapter instead of arendereroption: a 1.xrenderer: "scalar"block becomes a separatescalar({ spec, theme, … })entry that forwards its other keys to the embed, whileopenapi()andasyncapi()always render Blume’s own pages. The old keys and a leftoverrendererfail validation with a hint naming the replacement. -
81c0a17: Remove the
search.boostfrontmatter 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.providerstring and its credential blocks with adapters imported fromblume/search:orama()(still the default),flexsearch(),pagefind(),algolia({ appId, apiKey, indexName }),oramaCloud({ endpoint, apiKey, indexId }),typesense({ host, collection, apiKey }),mixedbread({ storeId }), orfalseto turn search off. Pass the adapter directly, or assearch: { 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 } }becomessearch: algolia({ appId, indexName, apiKey: searchApiKey }), and the Orama Cloud, Typesense, and Mixedbread blocks map the same way, with the search-only key asapiKeyeverywhere;provider: "pagefind"becomespagefind(), andprovider: "none"becomessearch: false. Admin keys stay in their env vars. -
d56c124: Replace the
content.sources{ type: "…" }objects with adapters imported fromblume/sources:filesystem(),mdxRemote(),githubReleases(),sanity(),notion(),obsidian(), orcustom(source)for anyContentSource. Every other field moves into the call unchanged, each factory that takes options also acceptsprefixandpollInterval, and a leftovertypeobject 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/clientandNOTION_TOKEN,sanity()needs@sanity/clientandSANITY_TOKEN, andgithubReleases()needsGITHUB_TOKEN, as doesmdxRemote()when it reads from GitHub — so the generated project, the secrets check, andblume doctortake them from the adapter. The top-levelcontent.root,include, andexcluderemain the zero-config shorthand for onefilesystem()source but can’t sit besidesources: move them into thefilesystem()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 (
regionto Mixpanel’s ingestion host,hostto Plausible’s script origin,cdnto Segment’s custom domain, …), and forwards everything else verbatim — into the SDK’sinitoptions for the script-based providers and asdata-attributes for the tag-based ones. Client-router navigations count as pageviews on every adapter: Segment and Hightouch get the sameastro:page-loadhook PostHog has, Mixpanel is initialized with URL-change tracking on, Fathom’s tag defaults todata-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 customdataLayerwhengoogleTagManager()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(), andstrapi()content source adapters toblume/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 samefieldsoption 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. SettingserializersoncontentfulSourceorpayloadSourcewrites the body as MDX, so the components they return render; a Markdown text field passes through as written.contentful()readsCONTENTFUL_ACCESS_TOKEN, and under--previewreads drafts through the Preview API withCONTENTFUL_PREVIEW_TOKEN(failing clearly without one).payload()readsPAYLOAD_API_KEYandstrapi()readsSTRAPI_API_TOKEN; under--previewboth stage unpublished documents withdraft: true. Relative upload paths resolve against the CMS origin,paramsappends query parameters (where[...],filters[...]), requests time out after 30 seconds, andblume initoffers all three. -
16678ba: New
markdown.externalLinksoption: set it totrueto open external links written in Markdown ([Status](https://status.example.com), autolinks, and reference-style links) in a new tab, in both.mdand.mdx. Each getstarget="_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, andGithubInfo) now carries the same screen-reader note.Card,Tile, andTooltipalso decide what counts as external with the same rule as the header, so a protocol-relative//hostlink now opens in a new tab and a relative path that merely starts withhttpno 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, ornextra, detected from the project’s files when left out — then opens Claude Code (--claude) or Codex (--codex) on theblume-migrateskill bundled in the package, with that framework’s mapping reference. Without an agent flag it prints the skill’s path and thenpx skills add haydenbleasel/blume --skill blume-migrateline 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 upgradecommand 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 bumpsblumeinpackage.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 checksblume.config.ts, eachcomponents.tsentry, page frontmatter for removed fields, andpackage.jsonscripts that still pass removedblume buildflags.--claudeor--codexhands the list, with the new Upgrade to Blume 2 guide, to Claude Code or Codex;--no-installbumps without installing.
Patch Changes
-
8469262: When a
blume evalorblume translateagent 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_pagestool andresources/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 taggedfrordewith nothing marking it untranslated, so alocalefilter returned pages that aren’t in that language and acontentTypesorversionfilter returned duplicates. The copies are left out asllms.txtand search already leave them out;get_pagestill reads a fallback URL. -
0bcb729: Every built-in component now reaches agents as Markdown in the
.mdmirrors,llms-full.txt, MCPget_page, and search.Accordion,AccordionItem,Expandable,FileTree,Columns,CodeGroup,Frame,Panel,Tile,Update,Prompt,GithubInfo,CodeBlock,Diff,Math,Tree,Color,Badge,Icon, andTooltipused 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 (aStepwith sub-items, say) also keeps its indentation instead of flattening to one level. -
81c0a17: The MCP server’s
search_docstool and the/api/docs/searchendpoint 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 thePAGE_NOT_FOUNDproblem naming the route, instead of the genericAPI_ROUTE_NOT_FOUNDfor 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
createTextStreamResponseandtoTextStreamhelpers instead of the deprecatedresult.toTextStreamResponse(), soblume checkno longer reports it, and it answers503rather than500when 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
/changelogindex 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
/changelogindex now has a Markdown form:/changelog.md,Accept: text/markdownon/changelog, and MCPget_pagewith/changelogreturn 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
/changelogindex now lists every release as a single row — title,categorytag, 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 thechangelog.showReleasesUI string it used. -
77fc861: A
/changelogtab now opens the changelog timeline instead of the newest entry, with nohrefneeded, including under abasePath, where the generated index is still served at/changelog. -
0bcb729:
blume checkno longer fails on files Blume generates itself in a project without its owntsconfig.json, where Astro type-checks the whole generated.blumeproject under its strict settings. The sidebar fragment pages of apage- orgroup-display folder read a group-only property off a union, and the Mixedbread search endpoint readtextoff 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/nodeitself. A site with a Mermaid diagram no longer fails withts(2306), “File … mermaid-element.ts is not a module”. -
ecd01a0: Diagnostics from
blume validate,blume audit, andblume evallink 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 previewin a project where onlyblume devhas run says to runblume buildfirst, instead of printing Astro’s stack trace.blume check --strictandblume dev --strict, which are strict only on request, now say to drop--strictto continue past errors, not to pass--no-strict.blume translate --check --jsonlists every missing or stale translation as an error diagnostic, so itssummaryagrees with the non-zero exit.blume translate,blume audit, andblume evalreport 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 readsrun failed:, instead of afix:line naming a docs page that has nothing to fix. blume --helpcarries the current tagline.
-
3cbf91e: A
cloudflare()server build now deploys withnpx wrangler deployfrom 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 — itspackage.jsonname, else the site’s hostname, else the project folder — instead ofblume-runtime, which every Blume site shared, so two sites deployed to one account no longer overwrite each other. Anameset in awrangler.jsoncat the project root still wins. -
c57c472: Cloudflare server builds now answer a missing page with the prerendered Markdown 404 (
/404.md) when the client sendsAccept: text/markdown, and with the JSON problem document (/404.json) when it prefers JSON — keeping the404status, as Vercel builds already did. To let a request for a URL no page backs reach the Worker at all, the generatedassets.run_worker_firstrules now claim every path except the fingerprinted build assets, the raw.md,.mdx, and.txtfiles, the.well-knownfiles, 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’sPAGE_NOT_FOUNDproblem 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 / #0a0a0apair it displays. Its “Copied” confirmation and its accessible name are localized, the latter through a newcontent.copyColorstring 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-assetsroute in dev and server builds, from anode()server’s entry, and through the_headersandvercel.jsonrules 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 anEBADENGINEwarning: it depends onwrite-file-atomic7.x, whose code is identical to 8.0, andundici7.x, since their 8.x releases require Node 22.22 and 22.19. The optional@mixedbread/sdkpeer now accepts every 0.x release from 0.77, so a current SDK no longer draws an incorrect-peer warning. -
3cbf91e:
blume devno longer crashes with “Cannot read properties of null (reading ‘port’)” when Astro restarts during startup. The firstblume devafter ablume buildrewrites the generatedastro.config.mjsjust 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 anog:imagepointing at a generated card that was never rendered. Generated cards cover static custom pages only, so passogImagetoPageLayoutfor a dynamic page’s social card. -
0bcb729:
blume ejectno longer overwrites an app it already ejected: run again, it stops before touchingastro.config.mjsorsrc/, since a fresh copy would discard every edit made since, and--forceis the explicit way through. In an ejected app,blume devandblume buildstop too, pointing at the app’s ownnpm 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 configblume ejectcan’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 boxblume buildprints lines its “Server features” row up with the others. Only the headerblume ejectwrites (or the one Blume 1’s eject wrote) marks a project as ejected, so a project with its ownastro.config.mjsand a codegensrc/generated/folder still runs through Blume. -
81c0a17:
blume ejectnow writes an Astro app that builds from any checkout, not only the directory it ran in. The ejectedastro.config.mjsuses a relativeoutDirandpages/scan glob, hands a deploy adapter no absolute project root, and resolves itsblume:*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 topackage.jsonat the ranges Blume uses —astro,@astrojs/mdx,@tailwindcss/vite, the integrations and adapter SDKs the config wires in, andreactandreact-domwhen React renders an island, example, or Ask AI, plusaifor the Ask AI route andepub-gen-memorywhen EPUB export is on — and its closing message lists the install command first, soastro buildruns under pnpm, which resolves only a project’s own dependencies. -
3cbf91e:
blume eval --agent codexnow 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-referencefolder is “API Reference”, not “Api Reference”, andfaq,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 inllms.txt. A folder’smeta.tstitlestill wins. -
81c0a17: A page’s top-level
iconfrontmatter now sets its sidebar icon whensidebar.iconis unset, like the top-levelhiddenandnoindexshorthands. The key was accepted but never read, so a page migrated from Mintlify withicon: downloadshowed 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 alwaysnpm 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. Ajavascript:,data:, orvbscript: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.mdand.mdx, and on search, the.mdmirrors, andllms-full.txt. A line that opens<includebut isn’t a statement Blume can read now raises aBLUME_INCLUDE_MALFORMEDwarning instead of rendering the raw tag without a word. -
81c0a17:
BLUME_NAV_INDEX_TITLE_MISMATCHnow 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 initnow 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 runsdevstraight away. Pass--no-installto only write the files; a failed install keeps the scaffold, prints the command to retry, and exits non-zero, and--ejectnow ejects in the same run. For pnpm,initwrites apnpm-workspace.yamlapproving esbuild’s build script, and for Yarn 2 or later a.yarnrc.ymlwithnodeLinker: node-modules, which Blume needs; inside an existing workspace it leaves the workspace’s config alone and says what to add. -
81c0a17:
blume initfixes: the starter pages no longer repeat their frontmatter title as a body# Introductionheading, so each renders one<h1>, and the docs starter no longer tells you to run a bareblume dev, which isn’t onPATH. In a folder whosepackage.jsonalready exists — whichinitleaves alone — the next steps now addblumeand any source SDK it doesn’t list yet (npm install blume) and start the dev server through the package runner (npx blume dev) unless itsdevscript already runs Blume, instead of printingnpm run devfor a script that isn’t there. The Sanity source scaffolds@sanity/client^8.6.1, the major Blume is tested against. Whenblumeis listed but was never installed, the next steps run the install first. Every starter page’s description now falls within the lengthblume auditchecks, 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 settingdeployment.site. -
81c0a17:
search: pagefind()no longer fails everyblume buildin 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’sastro buildfrom 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 configuredlogo.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; theLogolayout slot receives the activelocaleas 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-existshows the Arabic message, right to left, with its home link pointing at/ar, where it used to show the default locale’s. Hosts serve one404.htmlfor 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-migrateskill 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 buildnow stops before generating anything when a configured adapter’s package isn’t installed —algoliasearchforalgolia(),@openrouter/ai-sdk-providerforopenrouter(),@astrojs/netlifyfornetlify(), a Vue or Svelte island’s Astro integration — with oneBLUME_DEPENDENCY_MISSINGerror 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 opaqueMISSING_EXPORTorERR_MODULE_NOT_FOUND.blume devstill warns and keeps serving.blume doctornow reports the same missing packages, plus any secret an enabled feature needs that isn’t set (reading.envand.env.localfirst, asdevandbuilddo), 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-expandedand names the drawer witharia-controls; Escape closes it; and while it is open the page behind it isinert, 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-knowndiscovery 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-catalogwent out asapplication/octet-streamand a registry reading the AI catalog, ARD manifest, or MCP discovery files from another origin was blocked.blume buildnow puts a small wrapper in front of the server entry (dist/server/entry.mjs, with Astro’s own entry beside it asastro-entry.mjs) that sets those headers before Astro handles the request; the entry’shandlerexport,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 withimportorexportno longer fails the page as an MDX import, and a line of only-or=or a typed©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.mdfiles. -
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 pagesReferenceLayoutrenders, now carry the samedescribedbylinks (llms.txt,agent-readability.json) andrel="ai-catalog"/rel="ard"manifest pair. All three layouts defaultdiscoveryfrom theblume:datasnapshot, 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. APageLayouthomepage also advertises its/index.mdMarkdown mirror as atext/markdownalternate, matching the homepage HTTPLinkheader. The block lives in one sharedDiscoveryLinkspartial. -
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 withdata-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 theopenapi()reference — instead of the removedopenapi.playground.proxykey. -
3cbf91e: The API playground’s built-in proxy (
/_api-proxy, foropenapi()andgraphql()references withplayground: { proxy: true }) now sends every response it relays withContent-Security-Policy: sandbox,X-Content-Type-Options: nosniff, andCross-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 a413before 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.comorv1) 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 itsplayground: { 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 newcontent.copyPromptstring that every shipped language pack translates. Tables copy as GFM tables, alignment and all. -
3cbf91e:
blume-redirects.jsonis written only for a static build with no host named, as documented. A static build forvercel(),netlify(), orcloudflare()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’sindexpage (/guides) went to/install, and a link to a file ([Setup](./setup.md)) opened its raw Markdown. Each relative link now resolves the wayblume validatechecks it — from the page’s own folder, with a.mdor.mdxlink landing on the route that file publishes at,slugincluded — andblume auditreads relativehrefs the way a browser does, so all three agree. A component’s stringhref(<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.mdmirrors,llms-full.txt, and MCPget_pageget 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, whichblume auditflags 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 buildfailed with “does not provide an export named ‘binaryTag’” once another package putjs-yaml@4at the root, and installing@astrojs/netlifyor@astrojs/cloudflarewas enough to do that. Yarn Classic failed the same way, and Yarn Berry failed on acookieconflict. Under pnpm, anetlify()build crashed in the adapter’s file trace, avercel()build failed its function-bundle check, and anode()server built but couldn’t start (“Cannot find package ‘zod’”). Each server-side build output now carries anode_modulesof links to exactly the packages its bundles import. A project installed with Yarn Plug’n’Play, which creates nonode_modules, now stops with aBLUME_YARN_PNPerror naming the fix (nodeLinker: node-modulesin.yarnrc.yml) instead of an error that read like a bug in Blume. -
81c0a17: Builds no longer print Rolldown’s nine-line
MODULE_LEVEL_DIRECTIVEwarning 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
sanitySourcewithserializersnow 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/astroto 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.jsonagain 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#hashlink 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
transparentHeaderprop toPageLayout. 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 validatenow checks a component’s stringhref(<Card href="/guides/setup">,<Tile href="./install">) the way it checks a Markdown link, including anhrefa formatter wraps onto its own line, so a broken card or tile link is reported at its line instead of shipping. An expression-valuedhref={…}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 aheadersblock indist/vercel.json:charset=utf-8on the raw.md,.mdx, and.txtfiles, the homepageLinkheader, and the media types and CORS header of the.well-knowndiscovery files. The file is written even when the site has no redirects. -
0bcb729: The first
blume version <id>now turns versioning on inblume.config.ts, adding aversionsblock with the id archived and the live docs labeled “Latest”. It used to only print a snippet — with a placeholdercurrent: { 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, andblume doctorwarns 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.