Changelog
August 20, 2026
Patch Changes
- b6770b3: Fix fully unstyled frames during client-router navigations. Astro hoists the CSS of components rendered after the head has streamed (a page’s MDX content, for one) into the body as stylesheet links, and the client router neither preloads nor persists body stylesheets — so swapping in a page painted a frame or two with no CSS applied before its sheet loaded. The layouts now load an incoming page’s body stylesheets into the head and wait for them before the swap, and keep the loaded copy across it.
August 20, 2026
Patch Changes
- 7650327: Add
ai.ask.retrieval—maxResults,excerptChars, andcontextBudget— so a site can size how much documentation each Ask AI question injects into the model’s prompt. Injected characters are the dominant term in time-to-first-token, and on a self-hosted backend the fixed 10,000-character grounding could push a single question past 40 seconds. The three knobs stay separate because they aren’t interchangeable:contextBudgetcaps the total,excerptCharsdecides how deep into one long page an excerpt reaches, andmaxResultscaps how many pages retrieval adds (the page the reader is viewing is injected on top of them). Defaults are unchanged (6 / 2000 / 10000), so existing sites behave exactly as before. - 0c2c985: Try It message composer on native AsyncAPI operation pages. Each
send/receiveoperation ships an interactive Try it panel: a payload editor prefilled from the message’sexamples(or a value sampled from the payload schema when the message declares none) and validated against the payload schema as you type, an input per channel parameter, and a server picker fed by the channel’sserverswith a free-text URL override. The protocol-aware code samples (wscat, browserWebSocket,kcat,mosquitto_pub/mosquitto_sub) stay in lockstep with the form, filling the channel address template with the parameter values you type. Onws/wssbindings the panel also connects live to the resolved channel URL, shows connection state, sends composed messages, and logs received frames with timestamps — there’s no reconnect logic; other protocols get the composer and copyable CLI samples, since Blume doesn’t fake broker connectivity from a browser tab. The panel is server-rendered collapsed and loads its JavaScript only when a reader first opens it;asyncapi.playgroundmirrorsopenapi.playground, so it’s on by default with the native renderer andfalseturns it off.playground.proxystays OpenAPI-only, and no broker credentials are collected or persisted for event operations. - e2dbf74: Adopt Astro’s client router (
<ClientRouter />) for page navigation. Same-origin link clicks now swap the new page into the live document instead of tearing it down for a full load, so navigation is flicker-free in every browser — including Firefox, which has no cross-document paint holding and briefly flashed a blank frame between pages. Navigations animate with native view transitions where supported and Astro’s fade fallback elsewhere, both honoringprefers-reduced-motion; the sidebar keeps its scroll position across pages; search results navigate through the router too; and PostHog analytics captures a pageview per client-side navigation. The Scalar API reference intentionally keeps full-page loads (it is a SPA that mounts once per document), and the cross-document@view-transitionrule from the previous release is gone — the router supersedes it. - 97a9b30: Support a dark-mode favicon. A dark mark is invisible against dark browser chrome, so Blume now auto-detects a
-darksibling of your icon file — the same name and directory with-darkbefore the extension, likeicon.svg→icon-dark.svg— and emits both icons behindmedia="(prefers-color-scheme: …)", preceded by a plain light link so crawlers and browsers that ignore media queries on icons still get a sensible mark. Only the sibling of the icon Blume resolved is picked up, so an unrelated-darkfile can’t pair with your mark by accident. Ship one icon and nothing changes: a single<link rel="icon">, as before. The bundled Blume fallback now ships a light variant too, so a site with no icon of its own keeps a visible favicon in dark mode in browsers that honor media queries on icon links. - 2597b38: Ignore repo-locating
GIT_*environment variables (GIT_DIR,GIT_WORK_TREE,GIT_INDEX_FILEand friends) when resolving git last-modified dates and shallow-clone status. A parent git process exports an absoluteGIT_DIRto its hooks, so a build run from inside one — a husky hook, a post-merge script, a CI wrapper — silently read the wrong repository and dropped every page’s date. - 0c2c985: Try It playground on native OpenAPI operation pages. Each operation ships an interactive Try it panel: a form generated from the operation’s parameters and request-body schema, prefilled from the spec’s examples, with a server picker fed by the spec’s
serversand auth inputs matching the operation’s security schemes (bearer, API key, basic, and a token paste field for OAuth2). Values typed into the form update the generated code samples live, and Send fires the request directly from the browser — with an optional proxy for APIs that don’t allow cross-origin requests from the docs site (openapi.playground.proxy: a URL of your own, ortruefor the built-in/_api-proxyserver route). The panel is server-rendered collapsed and loads its JavaScript only when a reader first opens it; it’s on by default with the native renderer, andplayground: falseturns it off. - e31ff0a: Add
ai.openInChatto configure the “Open in chat” page action:falsehides it, and an array of provider keys ("v0","chatgpt","claude","t3","scira","cursor") shows just that subset, in order. - 5f41cbb: Keep the Ask AI panel alive across page navigations. The island now rides the client router with
transition:persist, so the conversation, a draft question, and the open panel all survive moving between pages instead of resetting on every click. The island re-anchors its portaled panel and the desktop content-push attribute after each swap, and the mobile overlay’s focus containment re-applies to the new page’s content. - 6eceaea: Segment search text for every non-Latin default locale, not just Japanese, Chinese, Korean, and Thai. Orama’s default tokenizer keeps only basic Latin letters and digits, so when
i18n.defaultLocalewas a Cyrillic, Greek, Hebrew, or Devanagari language the index collapsed to zero tokens and every query silently returned no hits. The script now comes fromIntl.Locale.maximize()—sr-Latnkeeps Orama’s tokenizer whileaz-Cyrlis segmented — with legacy tags (ja_JP.UTF-8,zh-cmn-Hans) resolved by their language subtag, and Latin terms on a segmented index fold diacritics so café still matches cafe. The tokenizer follows the default locale for the whole index, so non-Latin translations on a Latin-default site are unchanged. - 0c2c985: Harden the Try It playground: empty auth inputs no longer send their redaction placeholder as a real credential (the live request goes out anonymous instead), pre-network URL/header errors are reported as themselves rather than as a CORS failure, spec-derived names are escaped before selector interpolation, external proxy URLs that already carry a query string join with
&, blank required parameters stay visible in samples and sends, and mid-edit invalid JSON renders as a string literal in the JS/Python samples instead of broken syntax. The AsyncAPI composer now connects with an empty payload editor, prefills channel parameters from declared defaults/examples/enums instead of the sampler literalstring, and degrades a no-example, no-schema payload to{}rather thannull. The built-in CORS proxy allowlist skips templated server URLs, and a proxy with no allowable origin warns at build time. - c289eff: Stop
blume validatereporting colocated images as broken assets. A relative image embed () — the form the docs recommend for local images, since those are optimized at build time — was resolved into a site route and then looked for underpublic/, where it never lands: the image pipeline emits it to_astro/from beside the content. Every such reference came back asBLUME_BROKEN_ASSET, so--strictfailed on a site whose pages render the image correctly. Validation now accepts an image embed whose file exists next to its page source, passing the raw target through the same resolver that decides which colocated images the/blume-assets/contentendpoint serves, so the two can’t disagree about what one is. Everything that resolver would skip is still reported: a plain link to a relative path (only image nodes go through the pipeline), a?query/#fragmentsuffix, a case-mismatched or directory-shaped name (which breaks on the case-sensitive production build), and a reference that resolves nowhere — the last with a diagnostic that points at the page’s folder rather thanpublic/, and even when nopublic/directory exists. - 1517f9c: Fix the sidebar rendering empty on archived version pages when header tabs are configured. A version tree’s navigation root is versionized while tab paths stay in current-docs space, so the root tab was misread as a section tab that owns no group in the snapshot; it is now recognized as the root tab — via a single shared containment check used by sidebar scoping, tab-section pruning, and hoisting alike — and the archived sidebar renders its full tree. Header tabs also no longer claim
aria-currenton archived pages: they link back to the current docs, so none of them is the current page there.
August 16, 2026
Patch Changes
- ca28fb8: Generate an Open Graph card for the changelog index. The
/changelogpage previously shipped noog:image(and fell back to X’s compact summary card); it now gets the same generated card every other page has, served at/og/changelog.png. - ca28fb8: Stop doubling the site title in the changelog index’s document title. The page passed “{site title} {Changelog}” to a layout that suffixes “- {site title}” itself, producing titles like “Acme Changelog - Acme”; it now reads “Changelog - Acme”.
- 8db1ecb: The default display font is now Inter, matching the body font — default sites download one text family instead of two (Inter Tight’s files alone were ~190 KB per page). To keep the tightened display look, headings now get
letter-spacing: -0.05emfrom the theme itself, which also means any font you configure fordisplayreads correctly at heading sizes instead of depending on tracking built into the font. Inter Tight remains available as theinter-tightslug. - ca28fb8: Let a user-supplied
ogImageonPageLayoutdeclare its alt text and pixel size via the newogImageAltandogImageSizeprops, emitted asog:image:alt/og:image:width/og:image:height— the metadata Blume’s generated cards already declare automatically. - ca28fb8: Emit schema.org JSON-LD from
PageLayout, so custom pages — most importantly a custom home page — carry the sameWebSitestructured-data graph the docs pages do. On by default; passstructuredDataEnabled={false}to opt a page out. - ca28fb8: Keep the trailing slash on the home page’s canonical,
og:url, and hreflang URLs (https://site/instead ofhttps://site), so they byte-match the sitemap’s<loc>for the root route. - ca28fb8: Warn when
lastModifiedruns in a shallow git clone. CI platforms usually check out limited history, which silently dropped most git-derived dates — sitemap<lastmod>and “Last updated” stamps vanished in production while working locally. The build now emits aBLUME_SHALLOW_GIT_HISTORYwarning pointing at the fix (VERCEL_DEEP_CLONE=trueon Vercel,fetch-depth: 0for actions/checkout). - bad025f: Smooth out page navigation: every link now prefetches on hover/viewport, and same-origin navigations opt into cross-document view transitions in supporting browsers, replacing the hard flash between pages with a crossfade. Respects
prefers-reduced-motion. - 8db1ecb: Preload only the font weights above-the-fold text actually renders in (body 400/500, display 500/600, mono 400) instead of every configured face. On a default site this cuts the per-page font preloads from ten files (260 KB) to a handful (50 KB), bandwidth that was competing with the critical CSS and pushing out mobile LCP. All other faces still load on demand through their
@font-facerules withfont-display: swap. - ca28fb8: Redirect trailing-slash URLs to their slashless twins on Vercel with a 308.
/docs/and/docspreviously both served 200 as duplicate URLs; the routing config now collapses the slashed form onto the canonical slashless one (the root/is untouched).
August 16, 2026
Minor Changes
- 94b0678: Add docs versioning. The latest documentation lives at the content root with unprefixed URLs; each past version is a frozen snapshot directory cut with the new
blume version <id>command, which copies the tree, rewrites root-absolute links to stay inside the snapshot, and registers the id inblume.config.ts. With aversionsblock configured, the header grows an automatic version switcher (same-page navigation, falling back to the version root), every archived page shows a localized “you’re viewing an old version” notice with a go-to-latest link, and archived pages default their canonical to the same page in the live docs — the SEO treatment guides recommend and no docs framework automates — while version-only pages stay self-canonical and per-versioncanonical: "self"andnoindexremain available. Search scopes results to the version being viewed with a remembered “All versions” toggle across Orama, FlexSearch, Algolia, and Typesense, and the agent surface is version-aware: the MCPsearch_docs/list_pagestools default to the current docs and accept an archived id or"all"(plus a newlocalefilter),get_navigationserves snapshot and per-locale trees,llms.txtsections archived versions with an “(archived)” label,llms-full.txtstays current-only, and the sitemap drops archived pages whose canonical points at a live equivalent. Versioning composes with i18n (snapshot outermost on disk, locale outermost in URLs), snapshots keep their own folder meta and translations, andblume translatenever retranslates them.
Patch Changes
-
b954287: Tighten types across the package:
unknown-typed parameters, returns, and open dictionaries are replaced with precise named types, and every remaining type assertion carries a documented invariant (adopting ultracite’s anti-slop lint preset). -
c8d1624: Native AsyncAPI renderer. The
asyncapiblock now defaults torenderer: "blume"— one real page persend/receiveoperation, grouped by tag (or channel address) in a tab-scoped sidebar, with message payload and header schema tables, channel parameters, protocol bindings, an Authorization section derived fromsecuritySchemes, protocol-aware code samples (wscat/WebSocketfor ws,kcatfor Kafka,mosquitto_pub/mosquitto_subfor MQTT), and full participation in site search,llms.txt, and OG images. AsyncAPI 2.x specs are normalized to 3.x automatically with the official AsyncAPI converter, sopublish/subscribemap onto stable operation URLs. Theasyncapiblock gainsrenderer,codeSamples, andexpandSchemasfor full parity withopenapi; per-sourceincludeInSearch/includeInLlms/noindexnow apply to AsyncAPI sources too.Behavior change: AsyncAPI references were previously always rendered by the embedded Scalar SPA. Set
asyncapi: { renderer: "scalar" }to keep the old behavior. -
84055a7: Measure the audit’s title and meta-description limits in display columns rather than characters. What a search engine truncates is the space the text takes up, and a character count only stands in for that where every character is one column wide — true of Latin text and of nothing else. Counted in characters, one range cannot serve both scripts: the same 110–160 was at once too strict for a Japanese description (which says in ~60 characters what English needs ~120 for, so every page of a Japanese site reported
BLUME_AUDIT_DESCRIPTION_LENGTH) and too loose for a Japanese title (60 characters render as wide as 120 Latin ones and truncate, and nothing was reported). Widths come fromstring-width, so a fullwidth or wide character counts 2 and ASCII or precomposed (NFC) Latin text scores exactly as it did before — an English site’s findings are unchanged. (Two knowable divergences: an emoji now counts 2 columns instead of its UTF-16 code units, and a decomposed (NFD) combining mark counts 0 instead of 1.) The changelog and OpenAPI generators, which trim their derived meta descriptions to the same snippet range, now budget in display columns too, so generated pages keep passing the audit in every script. -
a749fd7:
blume ejectno longer dropsai.ask.instructions: the ejected Ask AI endpoint now carries the custom system-prompt text, matching the hidden runtime. -
eab66c5: Per-group sidebar display modes on the generated sidebar. A folder can opt its group into
flat,group, orpagewithout an explicitnavigation.sidebarconfig: setdisplayin the folder’smeta.ts, or — sugar for folders with anindexpage —sidebar.displayin the index page’s frontmatter. A generated group’s effective mode resolves index frontmatter first, then folder meta, then the globalnavigation.sidebar.display, then theflatdefault; a group’s value applies to that group only, and nested subgroups resolve their own chain.page-mode drill-in panels stay route-aware and list the index page first, loose pages hoist above sibling groups only where a sibling rendersflat(self-delimitinggroup/pagerows keep authored interleaving), and under i18n a fallback-filled index page never overrides a locale’s ownmeta.tsdisplay.sidebar.displayanywhere it is dead — a non-index page, the content root’s own index page, or any page under an explicit config sidebar — reports a newBLUME_SIDEBAR_DISPLAY_IGNOREDwarning instead of being silently dropped. Explicit config sidebars render exactly as before.
August 11, 2026
Patch Changes
- c19bfeb: Render Ask AI answers through a dedicated
Markedinstance instead of mutating the sharedmarkedsingleton, so other components importingmarkedno longer inherit the panel’sbreaksoption and citation link rewriting. - d16f0a5: Keep keyboard focus inside the Ask AI panel while it is a small-screen overlay: everything behind it turns inert until it closes, so Tab no longer escapes into the covered page. The desktop docked panel stays non-modal and the page remains interactive.
- e59cbd2: Add
ai.ask.instructionsfor custom system-prompt text (identity, language, tone). It is appended to the built-in instructions on every backend, so the grounding and citation contract stays intact. - 421fa82: Locate Ask AI grounding excerpts for queries in languages written without word spaces. Query terms are now segmented with
Intl.Segmenter(with a Unicode-aware regex fallback) instead of a Latin-only pattern, so Japanese, Chinese, Korean, and Thai questions center the injected excerpt on the matching section instead of always sending the head of the page. Content and query are NFC-normalized so decomposed text still matches. - e59cbd2: Warn at build time, naming the exact package to install, when the configured Ask AI backend’s provider SDK isn’t installed — instead of failing with an opaque unresolved-import error.
- 9a1b71c: Bound concurrent file reads during audit crawling, example/island discovery, and math detection with
p-map. These previously fanned out one unboundedfscall per discovered file, which on large sites riskedEMFILEand held every page’s HTML in memory at once. - 9156701: Keep punctuation out of the index terms the segmenting search tokenizer produces.
Intl.Segmenterfollows UAX #29, which holds connector punctuation, combining marks, format characters and mid-number punctuation inside a word, soスネーク_ケースandrobots.txteach arrived as a single word-like segment and were indexed as they stood — reachable only by retyping the punctuation, and unreachable fromケースortxt. Word-like segments made of nothing but a symbol became index terms of their own. Segments are now split into runs of letters, combining marks and digits — marks are spelling, so Thai vowel and tone signs and the voicing in decomposed kana survive, as do a word-internal apostrophe (don't) and punctuation flanked by digits (1.0.3,1,000) — and each boundary ends a bigram run the same way a space or an interpunct does, so no window spans it. Text is also NFC-normalized before segmenting, so decomposed input indexes the same terms as a composed query. - 051bceb: Count audit prose words with
Intl.Segmenterseeded by the page’slang. The whitespace split it replaces counted a fully written Japanese or Chinese page as a handful of “words”, trippingBLUME_AUDIT_LOW_WORD_COUNTon every page of a CJK site. - 156d5ff: Preserve the request query string when the Cloudflare wrapper Worker answers a configured redirect. The baked-in redirect table now matches the static layer’s
_redirectssemantics: the incoming query string is forwarded to the destination unless the destination specifies its own, so inbound links carrying UTM or ref parameters keep them across a retired URL. - 2cb73cc: Slug Accordion, Tab, and Update ids through
github-sluggervia one shared helper instead of three identical per-component copies. Component ids now slug exactly like heading anchors: unicode letters are kept (Español→españolinstead ofespaol) and underscores survive. Titles that relied on the old ASCII-only stripping get new ids, so hash deep-links to such components change once. - c37b42a: Consolidate four divergent route normalizers (OpenAPI references, Ask AI retrieval, the MCP server, and the Scalar page generator) onto one shared
normalizeRoutein core, and retire the last quadratic edge-trimming regexes in favor of the lineartrimhelpers. - 92e1d2e: Spawn agent CLIs (claude/codex handoffs, eval and translate runners) through
cross-spawninstead ofshell: trueon Windows. cmd.exe no longer parses the argument list — codex’s-cflags carry JSON that shell quoting could mangle, and the interactive handoff no longer hand-builds a quoted command line — and a missing executable now rejects withENOENTon every platform instead of surfacing as cmd.exe’s exit code 9009. - d56e235: Declare
p-retryandnanotaras dependencies. Both were imported byopenapi/parse.tsandai/tar.tsbut only declared in the monorepo root, so installs of the published package could fail to resolve them. - be0d582: Deduplicate the
escapeRawHtml/unwrapParagraphhelpers that were copied verbatim into<Prompt>,<Frame>, and<Tooltip>into one shared module. No behavior change. - e5a374a: Hand the CLI’s
.env/.env.localcascade todotenv.config({ path })instead of a hand-rolled apply loop. The walk up to the repo root stays; the first-wins ordering, never-clobber-process.envsemantics, and best-effort file handling now come from dotenv itself. - d67135f: Cut GitHub Releases meta descriptions on grapheme boundaries. The UTF-16 slice could split a surrogate pair at the 160-character cap, emitting invalid Unicode (a lone surrogate) into
seo.description. - 5056321: Harden the search excerpt sanitizer: any
<that does not begin a bare<mark>tag is now entity-escaped instead of passed through. A remote excerpt containing<!--could previously open an HTML comment inside the results list and swallow the rest of the excerpt, highlights included. Rendering of legitimate text is unchanged. - 7c37792: Keep the Ask AI overlay’s inert sweep current while it is open. The sweep snapshotted
<body>’s children at open time, so anything portaled in afterwards (an image-zoom backdrop, a mermaid render, another island) stayed tabbable behind the overlay; a MutationObserver now folds late arrivals into the sweep. - 614ecb2: Measure header logo SVG dimensions with
image-sizeinstead of a hand-rolled attribute regex. This is the same parser the OG card already uses for the brand mark, so the two can no longer disagree about one logo, and spellings the regex missed (unquoted attributes,em/ptlengths, a>inside another attribute value) now measure correctly. - 0db1528: Define each MCP tool’s input once in zod and derive both the runtime argument parsing and the JSON Schema advertised by
tools/listfrom that single definition, so the two can no longer drift. The lenient agent-friendly coercions are preserved: a bare string is accepted for an array field,[]/{}mean “no filter”, and out-of-range limits clamp instead of rejecting. - 9229cf2: Extract search-index plain text by parsing Markdown (GFM included) and walking the tree instead of regex-stripping the source. Reference-style links, autolinks, setext headings, and table cells now index their text; literal
*/~/>in prose are no longer blanked; and the inner prose of block-level JSX components stays indexed. Applies to the client index and every hosted-provider sync, so rankings may shift slightly on re-index. - fec3400: Build agent-skill
.tar.gzarchives withnanotarinstead of a hand-rolled ustar writer, keeping the path validation, deterministic attributes, and sync gzip layer. The header byte layout changes once with this release, so each published skill’s archive digest changes on the next build; digests remain stable from then on and are now pinned by a golden test. - b6fe173: Pace Notion source API requests through a bounded request pool (default 3 concurrent, configurable via
concurrency) and jitter the 429 backoff, so large databases import within Notion’s rate limits instead of failing the build withBLUME_SOURCE_FETCH_FAILED. - 9560ed2: Measure the OG-card brand mark’s aspect ratio with
image-size(already a dependency) instead of a viewBox regex. Legitimate SVG spellings the regex missed —viewBox = "…"with spaces, newline-separated values, explicit width/height attributes without a viewBox — no longer silently render a squashed square mark. - fc92c24: Flatten OpenAPI descriptions into meta descriptions with a real markdown parse (
mdast-util-to-string) instead of regex stripping. Literal punctuation in spec prose survives —snake_caseno longer becomessnakecase,C#no longer becomesC— in theseo.descriptiontags of generated reference pages. - 7f1b355: Leave tilde-fenced code blocks in OpenAPI spec descriptions verbatim when escaping MDX-special characters, matching how backtick fences are already handled. Braces inside a
~~~fence no longer ship as literal{entities on rendered reference pages. - ea04c90: Replace three hand-rolled concurrency pools with
p-limit/p-map: the Notion source’s request semaphore, the link-audit probe pool, and the translate runner’s worker lanes and ledger-flush mutex. Behavior is unchanged — bounds, FIFO ordering, and the flush-before-next-item guarantee all carry over — with ~100 fewer lines to maintain. - 40cb5d1: Drive remote OpenAPI spec fetch retries through
p-retryinstead of a hand-rolled loop. The behavior is unchanged — three attempts, exponential backoff capped at 10s, non-retryable statuses abort immediately, and a saneRetry-Afterreplaces the backoff rather than stacking on it (now covered by a test). - b15a87a: Render
package-installcommand tabs throughpackage-manager-detector(the engine behindni), with the yarn tab pinned to Berry. Previously the yarn tab mixed Berry-only commands (yarn dlx,--immutable) with Classic-only ones (yarn global add), so no single yarn version could run every rendered command; global installs on the yarn tab now render npm’s form, since Berry removedyarn global.blume ejectalso detects the project’s package manager from its lockfile instead of only the invoking user agent, which was absent (silently defaulting to npm) whenever the CLI was run directly. - 39367e2: Strip all markup except bare
<mark>highlights from Pagefind excerpts before rendering them in the search dialog and preview pane, so a compromised or tampered search index can’t inject HTML into reader pages. The other providers already escape their excerpts. - 0900dc7: Parse
llms.txtlink targets with a real Markdown parse instead of a](url)regex in the audit crawler. Reference-style links and angle-bracket destinations now resolve, link titles no longer leak into URLs, and link-shaped strings inside fenced code blocks are no longer probed as claims. - 4b04a52: Locate code regions in OpenAPI descriptions with a CommonMark parse instead of fence-emulating regexes when escaping MDX-special characters. The parser is the authority on backtick pairing, tilde closers, unclosed fences, and fences nested in blockquotes (which the regexes mis-handled by escaping entities into the quoted code). Indented blocks keep escaping as prose — MDX has no indented code form.
- abd6be5: Collapse the dev server’s regeneration debounce and single-flight coalescer into
perfect-debounce, whose contract covers both: watch bursts debounce at 80ms and a trigger during a running scan marks exactly one trailing rerun instead of starting an overlapping scan. The heap-exhaustion guarantee the old coalescer carried is pinned by a dedicated test against the library. - f2ca3f6: Escape Markdown syntax in Portable Text prose spans. Span text is plain text by the Portable Text contract, but a literal
*,_,[, backtick,~, or<typed in Sanity was interpreted as Markdown or raw HTML in the rendered page; those characters now render as themselves. Code-marked spans stay verbatim. - 61a1146: Coalesce the page-actions menu and
<Component>preview-pane resize handlers to one layout pass per animation frame. Both previously re-read layout on every resize event, causing needless main-thread churn during a live resize drag. - 2094270: Honor the HTTP-date form of
Retry-After(RFC 9110’sWed, 21 Oct 2015 07:28:00 GMTspelling) when retrying remote OpenAPI spec fetches. Previously only delta-seconds were parsed and date values were silently ignored. - 3d6789b: Strip
:and control characters from filename-derived route segments, so files likeGuide: Architecture.mdbuild instead of crashing route generation with “The URL must be of scheme file”. - a7ad63a: Give the search dialog proper combobox semantics: the input announces as
role="combobox"witharia-expanded/aria-controls/aria-autocomplete, results render in a labeledrole="listbox"with groupedrole="option"rows, and the highlighted result is surfaced througharia-activedescendantandaria-selected. Screen readers previously heard nothing while arrowing through results. - 1a83df2: Guard the search dialog’s localStorage reads and writes so blocked storage (Safari “Block All Cookies”, sandboxed webviews) degrades to session-default preferences instead of throwing during setup and leaving search unable to open.
- 3b59b8f: Hash staged source content and asset filenames with SHA-256 (64-bit prefix) instead of a 31-bit DJB2 hash. The old hash named downloaded CMS assets, where a collision — plausible from ~46k items — silently served the wrong file. The first build after upgrading re-downloads remote source assets once under the new names.
- f14ac3f: Consolidate the six hand-rolled clipboard + “Copied” flashes (code blocks, page actions, color swatches, prompts, API panels, Ask AI) into one shared helper. Every copy affordance now announces success to a screen-reader live region (previously only code blocks did), never confirms a failed write, and holds its confirmation through rapid repeat clicks instead of reverting early.
- e511d02: Join
deployment.sitewith emitted paths through one shared helper built onufo’sjoinURL. Eight emitters (sitemap, RSS, robots, llms.txt, the MCP server and discovery documents, the API catalog, agent-readability) had drifted across three different trailing-slash treatments; they now agree, and a site configured with extra trailing slashes can no longer produce double-slash URLs. - 19f1f7d: Split the sitemap into a sitemap index over numbered
sitemap-N.xmlchunks when a site exceeds 50,000 URLs, the per-file cap search engines enforce. Smaller sites keep the single classicsitemap.xml. - c3f058d: Keep Unicode letters in OpenAPI tag slugs, and label tag sidebar groups with the spec’s own tag names. Slugs derived from OpenAPI tag names and reference-source labels now keep Unicode letters and numbers (with NFC normalization) instead of stripping them to hyphens, and tag sidebar groups take their label directly from the spec’s
tags[].name(overridable with ameta.tstitle), so authored casing likeOAuth2orGrößerenders verbatim. Note that operation-page URLs change for specs whose tag names, operation ids, or source labels contain non-ASCII characters — if such URLs are already deployed, add entries underredirectsinblume.configto forward the old routes. - e81ab33: Harden the search excerpt sanitizer against tag splicing: every
<is now consumed by a single scan, so dropping a disallowed tag can no longer join the text around it into a fresh one. - 6c747bc: Treat a document-leading
---followed by a blank line, or one with no closing fence, as a thematic break instead of front matter, so bodies that open with a divider (e.g. a Notion page whose first block is one) build instead of crashing with a YAML parse error. - 032df5f: Keep Unicode letters in CMS/frontmatter slugs. The slugifier’s ASCII-only keep-class deleted every non-ASCII character, so a Japanese/Chinese/Cyrillic
slugcollapsed to empty (forcing Sanity/Notion routes onto opaque document-id fallbacks) and accented slugs were mangled (café→caf). ASCII slugs are byte-identical; sites with non-ASCII CMS slugs get readable routes where the id fallback previously applied — set up redirects if those fallback URLs were shared. - 0ade7e6: Resolve OG card accent presets from the theme’s own OKLCH palette instead of a separate hand-synced hex table. The card’s “blue” was Tailwind’s blue, not the site’s — social cards now render exactly the accent the site shows. Custom (non-preset) accents are unaffected.
- edf5b78: Fix the
/blume-assetsendpoint’s traversal guard on Windows: it compared a forward-slash directory againstpath.resolveoutput (backslashes there), which 404’d every staged remote-source asset. The guard now usespath.relative, which also closes the sibling-directory-name edge a bare prefix test admits.
August 9, 2026
Patch Changes
-
fdfd1fe: Advertise agent discovery in every page’s head, not just the homepage header
An agent that enters the site on a deep page — a search result, a shared link — never sees the homepage-only
Linkresponse header, so it had no path toagent-readability.json. Every rendered page now carries the discovery links in its HTML head:describedbylinks toagent-readability.jsonandllms.txt, plus atext/markdownalternatepointing at the page’s own raw-Markdown mirror. Because the links travel with the prerendered HTML, they also work on hosts where Blume can’t set response headers at all (GitHub Pages, S3). -
5f735ae: Raise minimum versions of astro, mermaid, dompurify, undici, js-yaml, and @astrojs/vercel to pull in upstream security fixes
August 9, 2026
Patch Changes
- 8865982: The built-in Ask AI panel now streams through the public
useAskAIhook instead of carrying its own near-identical copy of the client — one implementation now owns request shaping, the optimistic assistant bubble, stale-stream and abort guards, and the error-body-is-not-an-answer rule.useAskAIgains an optionalerrorMessageoption so custom UIs (and the built-in panel, which passes its localized dictionary string) can control the failure notice shown in the transcript. - dc5c97a: Measure WebP and AVIF images in the audit’s Open Graph checks. The hand-rolled header parser only understood PNG, JPEG, and GIF, so builds whose image pipeline emits modern formats — including Blume’s own sharp-based optimization — silently skipped every OG image dimension check. Dimensions now come from the image-size package, which covers 25+ formats; unknown or truncated files still yield no finding.
- 57e1525: Watch project inputs in
blume devwith chokidar. Rawfs.watchrequired two documented platform workarounds — watching single files through their parent directory so rename-replace saves (vim and most atomic-save editors) don’t orphan the watcher, andrecursivehandling for directories. chokidar (the watcher Vite itself uses) owns both, and one watcher now covers the pages directory, config, theme, and component override files. - 9dfabd6: Respect
NO_COLOR,FORCE_COLOR, and terminal detection in all CLI output. Diagnostics, the audit/eval/translate reports, and internal error reports previously emitted raw ANSI escape codes unconditionally, so piping a command to a file (blume validate > report.txt) or reading CI logs outside a color-capable terminal showed literal[31msequences. All five hand-rolled palettes now go through consola’s color utilities, which disable styling when the output is not a color-capable terminal and honor the standardNO_COLOR/FORCE_COLORoverrides. - 28f2a9d: Escape HTML through one complete entity table. Four call sites each hand-rolled their own escape map with different coverage: the search popular-links icon markup escaped only
&and"while building ansrcattribute, the fallback code-block renderer escaped only&,<, and>, and the search dialog and XML feeds carried their own full tables. All four now use html-escaper’s five-entity escape (the same one Astro uses internally), so every site covers&,<,>,", and'consistently. - 7af2f4a: Check the Node version in
blume doctoragainst the package’s fullengines.noderange with semver. The previous check stripped the range down to a bare version triple and compared segments numerically, so any real range expression (^22.12.0 || >=24, prerelease tags) degraded into comparisons againstNaNand the check silently stopped working. - 6d7abb4: Parse
.envfiles with dotenv — the same parser Vite applies to these files at build time. The previous line-based parser silently truncated multi-line double-quoted values at the first newline, corrupting PEM-style credentials (-----BEGIN PRIVATE KEY-----blocks) before the content scan could use them, and its escape handling diverged from what the rest of the toolchain sees in the same file. The.env.local/.envcascade from the working directory up to the repository root is unchanged, and shell/CI values still win. - 016e5cf: Use the unified ecosystem’s own utilities for two hand-rolled markdown helpers: directive label text extraction now goes through
mdast-util-to-string, and the<TypeTable>Markdown downlevel builds its GFM table withmarkdown-table, which owns delimiter-row and cell padding rules instead of string concatenation. - 738c071: Replace the hand-rolled image lightbox with medium-zoom. The
markdown.imageZoombehavior is unchanged — click to zoom, dismiss on click/scroll/Escape, opt out per image withdata-no-zoom, images inside links stay plain — but the FLIP transform math, natural-size capping, and transition-teardown races now belong to a 2 kB library built for exactly this. The library is lazy-loaded only on pages that contain a zoomable image, and reduced-motion preferences disable the transitions as before. - 863ad44: Generate OpenAPI request examples with openapi-sampler, the generator behind Redoc. Two visible improvements over the hand-rolled sampler:
readOnlyfields no longer appear in request-body samples (they are server-generated and were previously included even though the schema declared them read-only), and format-aware placeholders replace generic ones —email,uuid,uri, and friends produce realistic values instead of"<format>". Declaredexample/const/default/enumvalues keep their precedence, and circular$refchains still terminate safely. - 19de528: Derive changelog meta descriptions by parsing release notes as GitHub-flavored markdown (mdast) instead of chaining strip regexes. The regex chain mis-handled real release-note shapes: tilde fences and fences of more than three backticks leaked their code into the description, an image followed by a link containing
)truncated wrong, and prose likea * borx > ylost characters to a blanket punctuation strip. The parsed tree drops headings, code, and raw HTML; keeps link text and inline-code content; and the word-boundary truncation is unchanged. - 4d0590b: Match remote source
includeglobs with picomatch — the same engine the filesystem source already uses through tinyglobby. The remote MDX source previously compiled globs with a minimal hand-rolled translator, so the sameincludearray meant different things depending on source type: negation patterns (!drafts/**), character classes ([0-9]), nested braces, and extglobs silently failed to match on remote sources. The matcher is now also compiled once per enumeration instead of once per file × pattern, which matters on large GitHub trees. - 842ed9c: Remove single-call delegation wrappers left over from the library migrations: the remote source’s glob-matcher factory, the search dialog’s
escapeHtml, the XML escaper module, the directive label’s text collector, and the env-fileparseEnvexport now call picomatch, html-escaper, mdast-util-to-string, and dotenv directly. Wrappers that carry real behavior (the atomic-write helper’s mkdir/fsync policy, the audit image sizer’s null-on-unknown contract, the OpenAPI sampler’s error guard) are unchanged. If you deep-imported the undocumentedescapeHtmlfromblume/components/layout/search/types, importescapefrom html-escaper instead. - 909c2fd: Fix two false-positive sources in the audit’s robots.txt check by matching rules with robots-parser. The hand-rolled matcher never read
Allow:directives, so the common lockdown pattern (Disallow: /plusAllow: /docs/) flagged every sitemap URL as blocked; and it treated eachUser-agent:line independently, so consecutive agent lines heading one rule group (as the spec defines) dropped rules that do apply to*. robots-parser resolves Allow/Disallow by longest match and handles agent groups correctly; wildcard and$-anchor behavior is unchanged. - 7120f63: Parse sitemaps in the audit with fast-xml-parser instead of regex scanning. Sitemaps the audit reads can come from other generators (including remote ones during network audits), and three legal constructs were invisible to the regex scan: CDATA-wrapped
<loc>values, numeric character references like&, and namespace-prefixed elements (<sm:loc>). All three now parse; the shallow contract — the loc list, per-loc lastmod, and urlset-vs-index detection — is unchanged. - 57673ee: Parse the project’s tsconfig with get-tsconfig when deriving
@/-style Vite aliases. The hand-rolled JSONC parser could corrupt a config whose string values contained", }"(its trailing-comma strip ran over string contents), and itsextendsresolution accepted two forms real tsc rejects (relative paths naming a directory, bare specifiers resolved through a packagemain). get-tsconfig follows tsc’s own semantics — JSONC, the fullextendschain including TS 5.0 arrays, inherited-path rebasing, and${configDir}substitution, which now works in alias targets. - 8e9f3d0: Make atomic file writes safe under concurrency. The translation runner, its ledger, and the runtime generator each wrote through a temp file named
<path>.<pid>.tmp— a name that is not unique within a process, so two concurrent writers to the same target (translate lanes run up to 16-wide; staged content writes fan out in parallel) could interleave through a shared temp file. All three now write through npm’s write-file-atomic, whose temp names are unique per call and which preserves file modes on overwrite.
August 8, 2026
Minor Changes
-
6afe539: Add declared facets:
content.types.<type>.facetsnames custom frontmatter keys whose values become filterable metadata. Faceted values ride along on search documents (blume-search.jsonand the MCP snapshot), and the MCPsearch_docsandlist_pagestools accept afiltersobject matching against them ({"domain": "architecture", "status": "enforced"}, every entry must match) — so a knowledge base holding RFCs, runbooks, or policies can drive progressive-disclosure agent workflows straight off its static content. Results carry their facet values,list_pagesshows each page’s, and the shared Orama index gains afacetTermsenum-array field so one static schema serves every project’s facet keys. Each facet name must be a declared custom key (per-type orfrontmatter.extend, validated at config load), and string, number, and boolean values facet — numbers and booleans stringified. -
7e9f7dd: Add
blume translate: agent-driven i18n translation with a committed freshness ledger and a CI drift gate.blume translate --claude(or--codex) finds every default-locale page that is missing or outdated in each configured locale and translates it headlessly with your local agent CLI — Blume builds the prompts, disables the agent’s tools, validates each reply’s structure (frontmatter reconstructed from the source, code-fence counts preserved), and writes the files itself. A committedblume.translations.jsonledger records the source hash behind every translation, so reruns are incremental, and hand-authored translations are adopted rather than overwritten (only--forceretranslates them). Under thedirparser, folder-navmeta.tstitles are translated too, copying every other key verbatim so per-locale sidebars keep their ordering.blume translate --checkis the read-only CI gate: it exits non-zero when any translation is missing or stale, with--jsonemitting the shared diagnostics report shape. -
6afe539: Let MCP clients filter by content type.
search_docsandlist_pagesaccept an optionalcontentTypesarray that narrows results to pages of the given frontmattertypes (["rfc"],["blog", "changelog"]), so an agent working against a site that mixes docs with RFCs, runbooks, or policies can scope retrieval to the kind of page it needs. Search hits now name their content type alongside the title, route, and excerpt, search documents carry the resolved type end to end (blume-search.jsonincluded), and the shared Orama index gains acontentTypeenum field filtered with an exactwherematch — the same mechanism the locale filter uses. -
6afe539: Add per-type frontmatter schemas via
content.types.<type>.frontmatter. Wherefrontmatter.extenddeclares custom keys site-wide, a per-type declaration scopes them to pages whose frontmattertypematches — so a project can require an RFC’sstatusor a runbook’sservicewithout loosening every other page. Keys follow the same rules asextend: any Standard Schema library validates them (Zod at whatever version the project installs, Valibot, ArkType), every declared key is checked on every page of the type so required schemas enforce type-wide, and validated values land on the page record’scustomfield. A declaration forcontent.defaultTypeapplies to pages that set notype, a key declared only for another type stays unknown elsewhere (typo-catching is unchanged), and a key can’t be declared both site-wide and per-type.
Patch Changes
-
f88d256: Honor
Accept: text/markdownon Cloudflare server builds. Deployed Workers sites ignored the header — the.mdmirrors were reachable only at their explicit URLs — because the ASSETS binding serves the prerendered content pages before the Worker script runs, and even a request that does reach the Worker is answered by@astrojs/cloudflare’s handler straight from that binding, ahead of the only place Astro middleware runs. No middleware could ever see a content-page request, so the negotiation now lives in the deploy bundle itself: afterastro build, Blume writes a small wrapper Worker in front of the adapter’s entry and scopesassets.run_worker_firstindist/server/wrangler.jsonto the content routes (grouped into a handful of route globs, merged with any user-configured rules, and staying within Wrangler’s rule limits).When the client prefers Markdown, the wrapper serves the page’s prerendered
.mdmirror from the assets binding withVary: Accept; every other request is delegated to the Astro Worker untouched, so only content routes pay the extra Worker hop. Cloudflare does not apply_headersto worker-first routes, so the wrapper re-stamps what the static layer would otherwise add on the routes it takes over — the homepage agent-discoveryLinkheader, the homepagex-markdown-tokensestimate, and the Markdowncharset=utf-8— while negative rules keep the raw.md/.mdxURLs on the static fast path with their_headerstreatment intact. The agent readability manifest now advertisescontentNegotiationon Cloudflare server builds too. -
bd939ba: Keep configured
redirectsoff the Cloudflare worker-first routes, so they ship the status you configured instead of a permanent 301. On a server build Blume routesredirectsthrough Astro’s own config, and@astrojs/cloudflareturns those intodist/client/_redirectsentries carrying the exact status — a file only Cloudflare’s static layer reads. Butassets.run_worker_firstis derived from the first path segment of the content routes, so a single page at/docs/referenceemits/docs/*, which swallows every redirect under/docs—/docs/api,/docs/api/run-query, and so on. Inside a worker-first route the static layer never runs,_redirectsis never consulted, and Astro’s SSR redirect handler answers instead. That handler honors the configured status only when the destination resolves to a discrete route, which under Blume it never does (every page is served from[...slug]), socomputeRedirectStatusdefaults a GET to 301. A configured 302 was therefore served as a permanent redirect that browsers cache more or less forever — the opposite of what a temporary redirect is for, and unfixable from the config.The generated Worker wiring now emits a negative
run_worker_firstrule for every configured redirect a positive rule would otherwise claim, so the static layer keeps serving it. The exemptions are derived from the merged rule set, so redirects claimed by your ownrun_worker_firstrules are exempted too (a configuredtrueis rewritten to its array spelling/*so the negatives can ride along), and they compare full served URLs, so subpath deploys (deployment.base) guard their content routes correctly. A redirect that no rule would have claimed gets no rule, and a path that is also a content route is never exempted — the page owns it, and taking it off the Worker would silently disable its Markdown negotiation; a docs-only site redirecting/is exempted like any other path. A retired section collapses to a{path}+{path}/*pair, which is two rules however many URLs it held, but only when no content route lives underneath, and only the request spellings a positive actually claims are emitted, conserving Wrangler’s cap of 100 rules. A rule over Wrangler’s 100-character limit collapses to its nearest safe ancestor glob instead of costing the whole site its negotiation. The exemptions ride along into the coarse fallback, where/*claims everything and they matter more rather than less; if even that cannot fit the limits the negotiation is skipped entirely, since a redirect cached as permanent is a worse outcome than raw Markdown staying at its explicit.mdURL. -
2e231bc: Serve configured
redirectsfrom the generated Cloudflare Worker itself instead of carving them out ofassets.run_worker_first. The wrapper Worker now bakes in a redirect table and answers any redirect a worker-first rule claims with its exact configured status, before delegating to the Astro Worker; redirects outside every worker-first rule still never invoke the Worker and are served by the static layer from_redirects, as before.Unlike the negative-rule exemptions this replaces, a baked-in table costs nothing against Wrangler’s caps of 100 rules and 100 characters — so a large or deeply nested redirect set can no longer push the rule set into the coarse fallback or cost the site its
Accept: text/markdownnegotiation, and redirects claimed by your ownrun_worker_firstrules (including a baretrue) are answered correctly without rewriting your configuration. A redirect at a content route’s own path is still never honored — the page owns it — and non-ASCII destinations are percent-encoded into theLocationheader. -
50a9ea7: Keep
blume devfrom tearing down React islands built from project components. The generated runtime is the Vite root, so user pages, islands, and alias-reachable components were invisible to the dep optimizer’s startup scan — and the Babel-injectedreact/compiler-runtimeimport can never be scanned — so their dependencies were only discovered mid-session. That re-optimization served a second React copy to islands hydrating at that moment, crashing them with “Invalid hook call”. The generated config now points the optimizer’s startup scan at user pages, theislands/directory, and tsconfig-alias directories, and force-includes the compiler runtime, so every dependency hydration can reach is part of the first optimization run. -
7e9f7dd: Backfill every built-in UI language pack with the chrome strings added since launch — the Export menu, “Copy code”, “Copy Codex command”, the Ask AI panel, navigation and theme-toggle labels, the 404 page, and the newer search dialog strings — so localized sites no longer show English for those surfaces. The “Open in v0/ChatGPT/Claude/…” provider labels, previously hardcoded, now localize through a new
actions.openIntemplate ("Open in {name}"). -
b62278b: Emit
_headerson a Cloudflare server build, so the agent-discovery surface it already generates is actually advertised.buildHomeLinkHeader()had three consumers and every one excluded this deployment: the_headerswriter returned early unlessoutput === "static", the Vercel routing-config injection only runs for that adapter, and the middleware that callsres.setHeader("Link", …)is mounted onastro:server:setup, so it is dev-only. A Cloudflare server build therefore served no homepageLinkheader at all, and noContent-Typeon the extensionless well-known files — an API catalog went out with no media type rather thanapplication/linkset+json.The gate is now
readsHeaderFiles(), which is true for any static build and additionally for a Cloudflare server build: the Worker servesdist/clientthrough its ASSETS binding, and Workers static assets honor_headersfrom that directory exactly as Pages does. Node server builds stay excluded, because the standalone server’s static handler ignores the file and writing it there would be inert. Vercel server builds stay excluded because their headers arrive through the routing config, which this would duplicate.Two related corrections fell out of testing it against a real Cloudflare server build, and both apply to static builds as well:
- The user opt-out is now checked at
public/_headersrather than indist.@astrojs/cloudflarewrites its own_headersduring the build (an immutableCache-Controlfor/_astro/*), so testingdistread an adapter-generated file as a user opt-out and skipped silently — the fix above would not have fired without this. - When a
_headersalready exists in the output, its rules are preserved and the generated ones are appended, so the adapter’s caching rule and Blume’s discovery rules coexist. On a static build this changes behavior for a_headersthat reacheddistsome way other thanpublic/(an integration writing it directly, say): that file previously suppressed generation entirely and is now appended to. Shippingpublic/_headersremains the opt-out.
The charset rules in this file remain redundant on a server build, where the runtime endpoint sets
Content-Typeon the Response itself; they are harmless, because a static-asset rule only applies to a file served from that directory. TheLinkand well-known media-type rules are the part that was missing, and the previous comment’s reasoning — that server adapters set Content-Type themselves — was true of the charset rules only and had been applied to the whole file. - The user opt-out is now checked at
-
f9f045c: Fix
blume checkfailing on the generated MCP endpoint whenai.mcpis enabled. JSON imports widen literal types, somcp-data.jsoncould never satisfyMcpData’s discriminated navigation nodes; the generated endpoint now asserts the snapshot back toMcpDataat the JSON boundary. -
7e9f7dd: The header’s search field and language switcher now collapse to compact icon buttons below the
lgbreakpoint (previouslysm), and the logo mark no longer shrinks when the header runs out of room — fixing the mid-width squeeze where the inline tab bar pressed the two controls into each other. -
eb8e1dc: Restore the content inset on
Tabpanels that mix a code fence with prose. The panel dropped its padding via:has(>pre), a rule meant for code-only panels — a fence owns its own frame, so insetting it again double-pads it. But the bare selector also matched a panel holding a fence and prose, stripping the padding from the prose as well; combined with[&>:last-child]:mb-0!a trailing paragraph ended up flush against the panel border with no space on any side, and lost its left inset so text started hard against the frame. The rule is now[&:has(>pre):not(:has(>:not(pre,template)))], sop-0applies only when the panel holds nothing but fences and mixed panels keep the inset like every other code-block host.templateis excluded because aTabwith aniconrenders a<template data-blume-tab-icon>that stays a permanent child — the tabs script clones its content into the trigger rather than moving the node. That rules out the two naive “is the fence alone?” tests, each for a different reason::only-childsees two children on every icon tab and so silently reintroduces the double inset, while:only-of-typeignores thetemplate(it only counts siblingpreelements) and therefore matches a single fence but stops matching a code-only panel holding two or more of them. -
aa33ae7: Write a Cloudflare server build’s artifacts where the Worker can actually serve them.
@astrojs/cloudflaredeclarespreserveBuildClientDir: true, so it keeps Astro’sdist/client+dist/serversplit and points the ASSETS binding in thedist/server/wrangler.jsonit generates at../client— the deployed Worker servesdist/clientand nothing above it. Blume still treateddist/as the served root, sorobots.txt,sitemap.xml,llms.txt,llms-full.txt,agent-readability.json,.well-known/api-catalog, and the Pagefind index all landed one directory too high and 404’d in production, andblume auditcrawleddist/and readclient/as a route segment, reporting ~118 phantom broken links.deployStaticDirand its isolated-build mirrorisolatedStaticDirnow resolve todist/clientfor a Cloudflare server build, matching the existing Node standalone handling. Cloudflare static builds are unaffected — they have no client/server split, so theoutDirroot is still what ships. -
8d4fd17: Allow
search.popularicons to use image paths/URLs and inline SVG, matching nav icons. Markup is resolved on the server before the Cmd+K island runs. -
7e9f7dd: Header tab labels (and tab dropdown item labels) accept a per-locale map alongside the plain-string form:
label: { en: "Docs", fr: "Documentation" }. Each locale’s navigation resolves its own entry, falling back to the default locale’s and then the map’s first entry, so an i18n site can translate its header without forking the config. -
7e9f7dd: Locales accept an optional
style— freeform guidance like “Brazilian Portuguese, informal você” that rides along in everyblume translateprompt, pinning register and dialect from a locale’s first translation and winning over an existing translation’s style on reruns. -
7e9f7dd:
blume translatenow shows the agent the existing translation when retranslating a stale page and instructs it to match its register, dialect, and terminology, so a small source edit yields a minimal retranslation instead of a from-scratch rewrite that can flip style.
August 3, 2026
Patch Changes
- 4d7dc87: Negotiate
Accept: text/markdownon the homepage even when it’s a landing page. A user-authored home page has no Markdown source, so agent requests for a markdown homepage previously fell through to HTML; the homepage’s mirror now falls back to thellms.txtindex — the machine-readable map of the site — served at/index.mdand wired into the dev server, the Vercel routing config, and the homepageLinkheader’srel="alternate"entry. - ff31ab5: Remove a polynomial-backtracking regex from the font-name slugifier’s dash trim
- 02eb6c7: Fix the homepage
Linkheader andVary: Acceptnever being sent on Vercel deploys. The injected header routes sat afterhandle: "filesystem"in the Build Output config — the miss phase, which prerendered static responses never reach — so agent-readiness checkers saw noLinkheader onGET /. Both header routes now ride in the main phase, ahead of static-file matching. - de62812: Stamp an
x-markdown-tokensheader (estimated token count, ~4 characters per token) on Markdown responses, following the Cloudflare Markdown for Agents convention: the raw-Markdown endpoints send it on dev and server-rendered responses, and the Vercel routing config carries it on the negotiated homepage.
August 3, 2026
Minor Changes
- a089dbc: Upgrade the AI SDK to v7. The generated Ask AI endpoint now passes
instructionsinstead of the deprecatedsystemoption, and the optional provider peer dependencies moved to their AI SDK 7-compatible majors:@openrouter/ai-sdk-provider@^3and@ai-sdk/openai-compatible@^3. If your Ask AI backend uses one of those providers, upgrade the provider package when you update Blume; gateway-backed setups need no changes. - 56979c0: Built-in image optimization for local images. A relative image reference (
) next to your content is now optimized at build time end-to-end — compressed, converted to WebP, and stamped with intrinsicwidth/heightso the layout doesn’t shift while it loads. sharp now ships as a Blume dependency, so the Astro image service works out of the box under every installer (it was previously unresolvable from the generated runtime under isolated linkers, failing the build with “Could not find Sharp”). Agent-facing surfaces (/<route>.md,/<route>.mdx, llms-full.txt, MCP) rewrite relative references to a new/blume-assets/content/…endpoint serving the original files, so image links in raw Markdown resolve instead of 404ing; the same endpoint serves remote-source assets materialized under.blume/public/blume-assets, which builds previously never shipped. A newimageconfig (domains,remotePatterns) authorizes remote hosts for optimization, mapping directly onto Astro’simageconfig. - bd2b8cb: Custom fonts:
theme.fontsroles now accept any Google/Fontsource/Bunny/Fontshare family by name ({ name, provider?, weights?, fallback? }) and local font files ({ name, variants: [{ src, weight?, style? }] }) alongside the curated slugs — all self-hosted and optimized through Astro’s Fonts API.seo.og.fontsgains a matching local form ({ name, src, weight?, style? }), and when a config setstheme.fontsexplicitly, the generated Open Graph cards now render in the theme’s display and body fonts automatically (explicitog.fontsstill wins;og.fonts: []opts out). Sites that never touchedtheme.fontsare unaffected. - 6e293cf: Publish a Web Bot Auth signature directory from
ai.webBotAuth.keys. The configured public JWKs are served at/.well-known/http-message-signatures-directorywith the registered media type on every build surface (static hosts via_headers, Vercel server builds via a Build Output content-type override), advertised inagent-readability.json, and validated to be public-key-only — a JWK containing private material is rejected at config time. - 64bac00: Publish Agent Skills for discovery from
ai.skills. Point the new config field at a directory of skills (each subdirectory holding aSKILL.md) and the build publishes them per the Agent Skills Discovery RFC v0.2.0: single-file skills verbatim at/.well-known/agent-skills/<name>/SKILL.md, skills with supporting resources as deterministic.tar.gzarchives (execute bits preserved), and a discovery index at/.well-known/agent-skills/index.jsonwith the v0.2.0$schemaand per-skill SHA-256 digests. Artifacts get explicit media types on static hosts, the index is advertised inagent-readability.json, and spec-invalid skills are skipped with a build warning. - 4e7a824: Generate an RFC 9727 API catalog at
/.well-known/api-catalog. The linkset is derived from the site’s configured APIs — each OpenAPI/AsyncAPI reference (anchored at its docs route, withservice-docand, for remote specs,service-descrelations) and the hosted MCP server (with its discovery document as the service description). Served asapplication/linkset+jsonon every build surface, advertised via arel="api-catalog"homepage Link header and inagent-readability.json. Sites with no APIs emit no catalog. - f4e6ce7: Serve Markdown to agents through
Accept: text/markdowncontent negotiation on Vercel server builds. The build splices header-conditional rewrite rules into the Vercel routing config, so a content-page request that preferstext/markdowngets the page’s raw-Markdown mirror at the same URL — withVary: Accepton both variants — andagent-readability.jsonnow advertisescontentNegotiationonly on deployments that honor the header. - 7d426b2: Add
seo.og.siteandseo.og.descriptionoverrides for the generated OG card’s footer URL and subtitle, and acceptfalseon them (and onseo.og.logo) to hide that layer of the card entirely - a730bef: Check DNS-AID agent discovery in
blume audit. Whendeployment.siteis set, the network tier (--url) queries_index._agents.<host>for ServiceMode SVCB/HTTPS records over DNS-over-HTTPS and reports the exact record to publish when none exist, plus whether the answers are DNSSEC-authenticated. SetBLUME_DOH_URLto use your own resolver. - cacab69: Register WebMCP tools on every page. Agentic browsers with a model context (
navigator.modelContextordocument.modelContext,provideContextorregisterTool) get the docs’ read-only surface as in-page tools:search_docs(lazy-loads the configured search client on first call),get_page(a page’s raw-Markdown mirror), andlist_pages(the llms.txt index). The script is tiny and no-ops in browsers without the API. On by default; setai.webmcp: falseto opt out. - 8567927: Advertise the agent-facing surface with an RFC 8288
Linkheader on the homepage. The header points agents atagent-readability.jsonandllms.txt(rel="describedby") and the homepage’s raw-Markdown mirror (rel="alternate"; type="text/markdown"), and is emitted on every surface Blume controls: the dev server, the_headersfile on static builds (Netlify/Cloudflare), and the routing config on Vercel server builds. - 8c46244: Align the MCP server card with the SEP-2127 Server Card extension schema.
/.well-known/mcp/server-card.jsonnow declares the published$schema, a reverse-DNSnamederived from the site host,title,websiteUrl, andremotestransport endpoints (absolute, so present once the site URL is known), alongside initialize-shaped compat fields (serverInfo,capabilities,transports) for scanners built against the proposal’s earlier revision. The advertised tool set and existingtransport/urlfields are unchanged.
Patch Changes
-
a92b52f: Align the table of contents, search results, search preview, and Ask AI scrollbars with the sidebar’s thin scrollbar styling
-
1794284: Update katex to ^0.18.1
-
a4f4a62: Update tailwindcss to ^4.3.3
-
4202f80: Rank Japanese and Chinese search results by whole compound terms rather than their parts. Dictionary segmentation cuts a term like 資金決済法 into 資金 / 決済 / 法, and because Orama scores a bag of words, a page mentioning each fragment somewhere could outrank the page the term is about — on one 65-page Japanese site every law-name query returned its index page first. Han, Hiragana and Katakana runs are now indexed as overlapping character bigrams, and queries on those indexes look for documents carrying all of a term’s bigrams before falling back to the any-token default, so sentence-like queries still return their closest pages. Korean and Thai keep their segmented words, and Latin terms are still indexed whole — though on a Japanese or Chinese index a query of several Latin words now also prefers pages carrying all of them, with the same any-token fallback.
-
04d37f1: Give the code-block copy button a visible surface and cap code block height
The copy button previously rendered transparent over the code, making it hard to see against syntax-highlighted lines. It is now an opaque chip with hover states and a check icon that swaps in after copying. Code blocks taller than 24rem now scroll vertically in place (on the inner code scroller, so the header bar and copy button stay put), with thin theme-colored scrollbars matching the sidebar treatment and a brighter thumb in dark mode. The scroller is keyboard-focusable (the tab stop moves from the pre to the element that actually scrolls), print output renders capped blocks in full, and the Component source pane keeps its own measured height. Copy success is now announced to screen readers via a polite live region, using the existing localized “Copied!” string.
-
76ce58f: Let Mermaid diagrams take the full content width instead of shrink-wrapping to the SVG’s 300px fallback size; small diagrams stay centered and diagrams that set
useMaxWidth: falsestill scroll horizontally -
cbec130: Include
deployment.basein the generated OG card’s footer site text, so a subpath deploy (like a GitHub Pages project site) showsuser.github.io/repoinstead of the bare shared host -
87a3e9a: Drop empty table header rows. GFM requires a header row, so a table that doesn’t want one is authored with blank header cells (
| | |) — that used to render as a dead band above the body; the empty<thead>is now removed. A header cell containing any non-text content (an image, an icon) still counts as non-empty. -
5951ee0: Support @sanity/client v7. The optional peer dependency range is now
^6.21.0 || ^7.0.0, andblume initscaffolds new Sanity projects with v7. -
f3334cd: Harden two code-scanning findings: the WebMCP search tool now strips search-hit markup so no
<fragment (such as a dangling<script) can survive mangled highlighting, and the API catalog trims the configured site origin with the lineartrimEndhelper instead of a quadratic trailing-slash regex. -
67bdef6: Define the
<skill>placeholder in the blume-migrate skill so the codemod and oxfmt-patch commands resolve to the skill’s own directory instead of failing on a literal<skill>path -
1500d32: Resolve the bundled docs path from the installed
blumepackage instead of a barenode_modules/blume/docs. Theblumeandblume-migrateskills now tell agents to locate the package from the workspace that depends on it (viarequire.resolve('blume/package.json')), so the docs lookup works in pnpm workspace monorepos where the package is not installed at the repository root. -
ea3c5fd:
blume audit --verbosenow prints each finding’s full message under the affected page, so link checks name the broken target instead of only the page containing it. -
3afcf7a: Move Blume’s own
zoddependency from v3 to v4 (^4.3.6), the major Astro 7, Scalar, and the MCP SDK already use, so an install resolves one Zod major instead of a v3 copy hoisted beside nested v4 copies. That mixed tree is what made partially updatednode_modules(a restored CI cache that a dependency bump left half-reconciled) resolve Scalar’s schemas against a Zod withoutz.function().optional, failing builds withTypeError: z.function(...).optional is not a function. Resolved configs and frontmatter parse exactly as before: shorthand defaults that Zod 4’s.default()would return unparsed — collapsing blocks likethemeorseoto a bare{}instead of their fully-defaulted shape — now use.prefault(), which keeps Zod 3’s parse-the-default semantics.frontmatter.extendschemas still go through the Standard Schema contract, so any Zod version (or Valibot, or ArkType) works there unchanged.
July 30, 2026
Patch Changes
- a015b0a: Match CJK and Thai content in the default Orama search provider. With
i18n.defaultLocaleset to a language written without spaces (Japanese, Chinese, Korean, Thai), the search index now uses a word-segmenting tokenizer built onIntl.Segmenter— previously every query in those scripts silently returned zero results because the default tokenizer collapsed the text to no tokens. The fix covers the search dialog, the MCP server’ssearch_docstool, and Ask AI grounding, and keeps Latin terms matching case-insensitively on mixed-language sites. - 454e67f: Match every hoverable sidebar navigation row — the back rows, routed panel header, and flat group header — to the shared 0.65rem navigation radius.
- 17d520b: Style the desktop sidebar scrollbar with a thin, theme-colored thumb and a transparent track. The mobile drawer and the page scrollbar keep their platform defaults. Requires Tailwind CSS 4.3 or later, which is now the declared minimum.
July 27, 2026
Minor Changes
- 217975c: Add a top-level
integrationsarray toblume.config.tsfor registering Astro integrations. Entries are schema-validated as an array (each element is left for Astro to validate) and appended after Blume’s built-in integrations in declaration order, with no sorting or deduplication. The generated Astro config loads them through a portable bridge back toblume.config.tsrather than serializing the instances, so function-bearing hooks survive across build,blume dev, config regeneration, and eject. Install and version each integration in the site itself — Blume neither adds it to the runtime’s dependencies nor manages its Astro compatibility. - 592af35: Add
blume eval, a test suite for your docs. An AI agent — Claude Code by default, Codex with--agent codex, spawned from your own installation with no API keys held by Blume — answers the questions inevals.yamlusing ONLY the documentation, served over a private MCP stdio bridge to an agent locked out of its file, shell, and web tools; a judge pass then grades each answer against the expected facts you listed. Any question the docs can’t answer fails CI (relax with--threshold), each failure is anchored to the source page that should answer it,--jsonemits the validate/audit-compatible machine report,--fixhands the failing report to the agent to edit the docs interactively, andblume eval initdrafts a starter evals file from your existing docs. No build or deployment is needed — the docs snapshot is computed from the content tree. - 2063196: Let a header tab declare its link target with
href. A tab’spathscopes its sidebar section and doubles as the link, so a section whosepathisn’t a page of its own falls back to the section’s first page rather than linking to a 404. That fallback only sees the content tree, so a tab pointing at a route generated outside it — the automatic/changelogindex, or a custom page underpages/— lands on the section’s first entry instead of the page the reader expected. Settinghrefkeeps the tab on the declared route; the field is optional and tabs that omit it resolve exactly as before. Declared hrefs are localized and rebased like any other route, so they work under i18n and adeployment.base. - 55e176a: Support external Ask AI endpoints on static sites and per-source OpenAPI search, llms.txt, and crawler indexing controls.
Patch Changes
- 18e1d8d: Stop Cloudflare server builds from declaring unused
SESSIONKV andIMAGESbindings in the generated wrangler config. Without a configured session driver,@astrojs/cloudflareforce-enables KV-backed sessions — makingwrangler deploydemand a real KV namespace nothing reads — and defaults images to the runtime Cloudflare Images binding. Blume never readsAstro.sessionand every HTML route prerenders, so the generated Astro config now sets an inert in-memory session driver andimageService: "compile", which pre-optimizes images at build time with sharp. - 38475cb:
blume initnow addsnode_modules/to the generated.gitignorealongside Blume’s runtime and build output directories. - 0c8f78d: Make the entire sidebar navigation back row clickable. When a drilled-in section has no index page, the chevron and title now form a single full-width back button; when it does have one, the title link and back button each fill their side of the row so there are no dead zones, and both get row hover states.
- 812b09e: Fix nested
<Tree>folder chevrons, nested<Accordion>chevrons, and a nested object schema’s “Show properties” toggle reflecting an ancestor’s open state instead of their own. All three rotated or flipped on Tailwind’sgroup-open:variant, which matches any open ancestor.group— the same leak as the nested sidebar chevron — so a collapsed disclosure inside an expanded one showed an open indicator. Each indicator is now scoped to its owndetails. - 812b09e: Fix a nested sidebar group’s chevron pointing down while the group is collapsed. The chevron rotated on Tailwind’s
group-open:variant, which matches any descendant of an open.group— and since every collapsible group in the tree is a.group, expanding a parent rotated the chevrons of its collapsed children too, so the arrow disagreed with the items it was hiding. The rotation is now scoped to the group’s owndetails, leaving each chevron to reflect only its own open state. - d5d6b7a: Fix the EPUB page action failing in dev with
epub is not a function.epub-gen-memory’s browser bundle is a browserified UMD, and its dynamic import lives insidenode_modules/blume, which Vite’s optimizer scan doesn’t crawl — so in dev it was served as raw ESM, where the UMD finds noexports/define, exposes nodefault, and strands its callable onwindow.epubGen. It now joins mermaid inoptimizeDeps.include, naming the/bundlesubpath that is actually imported, since optimizing the package root leaves that entry unoptimized. Production builds already bundled it correctly and are unchanged. - 55e176a: Pagefind now honors search exclusions: pages with
search.excludefrontmatter (and hidden pages, unlesssearch.indexing.includeHiddenPagesopts them in) no longer appear in local search results.
July 23, 2026
Patch Changes
- e4506a0: Keep the Ask AI panel open when Escape dismisses the search dialog stacked on top of it. The panel’s window-level Escape listener fired alongside the dialog’s native cancel, closing both surfaces when the user only meant to close search.
- e4506a0: Stop reporting every
blume audit --claude/--codexlaunch failure as “not found on PATH”. Only a missing executable (ENOENT) gets the install hint now; any other spawn failure (EACCES,EMFILE, …) surfaces as itself instead of being masked by an irrelevant install suggestion. - e4506a0: Strip
deployment.basebefore comparing canonical and sitemap URLs inblume audit. Canonicals and<loc>s are emitted assite + base + routewhile page URLs come from the base-less file tree, so on a subpath deployment every page false-firedCANONICAL_BAD_TARGET,NON_CANONICAL_IN_SITEMAP, andINDEXABLE_PAGE_NOT_IN_SITEMAP— and the duplicate-content checks silently skipped every page (each one looked like it canonicalized elsewhere). - e4506a0: Percent-decode pathnames before comparing them against the built file tree in
blume audit. Sitemap<loc>s areencodeURI’d andURL#pathnamere-encodes non-ASCII, while page URLs and file-index keys are raw on-disk names — so a non-ASCII route (e.g. a Japanese slug) false-firedSITEMAP_BAD_URL, and a percent-encoded href false-firedLINK_TO_BROKEN. - e4506a0: Probe live URLs under
deployment.baseinblume audit --url. Page URLs come from the base-less build tree, but the deployed site serves everything (pages, robots.txt, sitemap.xml) under the base — so auditing a healthy subpath deployment produced a wall ofHTTP_4XXfindings from probing the wrong URLs. - e4506a0: Strip query strings and fragments from redirect destinations before
blume auditchecks them against the build. A working redirect to/guide#setupor/search?q=xwas reportedREDIRECT_BROKENbecause the suffixed path is not a file-tree member. - e4506a0: Apply
basePathto configured redirects beforeblume auditresolves them. Redirects are authored as if mounted at root and gain the base at build time, but the audit compared them raw against built page URLs that carry the base — so every redirect on abasePathsite was reportedREDIRECT_BROKEN, whileLINK_TO_REDIRECTandREDIRECT_SOURCE_IS_PAGEcould never fire. - e4506a0: Keep
/separators in Sanity and Notion slugs. Slugging deleted slashes along with other punctuation, so aguides/setupslug was mashed intoguidessetup— and two documents whose slugs differ only by a slash silently overwrote each other. Segments are now slugged individually. - e4506a0: Stop promoting a
title="…"embedded in another code-fence meta attribute’s quoted value (caption='set title="X" here' file.ts) to the block title. Other quoted attributes are blanked before the explicit-title scan, so the bare-token title (file.ts) wins as intended. - 4294b00: Carry the resolved
dateFormatconfig into the runtime data and itsBlumeDataConfigtype. A configureddateFormatwas silently dropped from the serialized site data, so the date stamps always rendered the default long style, andblume checkfailed with ts(2339) ondata.config.dateFormatin the generated catch-all page. - e4506a0: Fall through to the next platform env var when one is set but empty.
VERCEL_PROJECT_PRODUCTION_URL=""dead-ended the chain beforeVERCEL_URL(same for Netlify’sURL/DEPLOY_PRIME_URL/DEPLOY_URL), leavingdeployment.siteunset so canonicals, OG images, and the sitemap silently switched off for that deploy. - e4506a0: Leave the
.blume/node_modulesjunction alone when it already points at the right target. It was deleted and re-created on every dev regeneration in the split-install layout, opening a window in which the dev server’s module resolution raced a missingnode_modulesand intermittently failed with “Cannot find package”. - e4506a0: Watch
blume.config.ts,theme.css, andcomponents.tsvia their parent directory inblume dev. Watching the file path tracks the inode, so a rename-replace save (vim and most “atomic save” editors) orphaned the watcher after the first save — every later edit was silently ignored until the server restarted. - e4506a0: Localize internal
navigation.featuredhrefs per locale, like header tabs. A pinned/changeloglink rendered on/fr/…pages always targeted the default-locale route, kicking the reader out of their language. - e4506a0: Stop opening a phantom code fence on a line-leading inline backtick span. A paragraph line like
```inline```is not a fence opener (CommonMark forbids backticks in a backtick fence’s info string), but the heading/link scanner treated it as one and silently dropped every heading and link after it from the TOC, search index, and anchor validation. - e4506a0: Stop misreading a body-leading thematic break as front matter in heading extraction. A stripped body opening with
---followed by a blank line lost every heading up to the next---line — missing TOC and search entries, and falseBLUME_BROKEN_ANCHORfindings fromblume validate. - e4506a0: Strip the locale directory from a shared
.$file’s nav path with thedirparser.fr/changelog.$.mdxkept itsfr/segment, silently routing the default locale’s record inside the French URL namespace, the French copy to/fr/fr/changelog, and conjuring a spurious “Fr” sidebar group. - e4506a0: Skip images that are themselves links when wiring click-to-zoom. A linked image (
[](https://example.com)) navigates on click, so the zoom binding only flashed an overlay in the instant before navigation while thecursor-zoom-inaffordance promised a zoom that never happened. - e4506a0: Extract markdown link targets with balanced parentheses and image-wrapped labels intact.
[wiki](https://en.wikipedia.org/wiki/Foo_(bar))was truncated at the inner)and reported as a broken link, and the outer target of[](/target)was never validated at all (the nested image’s own target still is). - e4506a0: Make the MCP tools’ contract hold together:
search_docshits now include theroutethe tool description promises (alongsideurl), andget_pageaccepts a full URL or a base-prefixed path — an agent following “pass a route fromsearch_docs” no longer gets “No page found” for a page that exists on a site withdeployment.siteordeployment.baseconfigured. - ff0b2b0: Fix
.mdpages serving stale content inblume dev. The generated dev config kept Vite’s watcher out of Astro’s cache dir, which suppressed thedata-store.jsonchange events Astro relies on to invalidate content in a running dev server — so edited Markdown bodies (rendered into the data store at load time) kept serving the old HTML even after a hard reload, while.mdxpages (rendered through their own module) updated fine. The watcher ignore is now scoped to migrated (content.root: ".") projects, the only layout whose glob loader would otherwise churn on Astro’s own cache writes. - e4506a0: Pair backtick code in OpenAPI descriptions the way CommonMark does — a run only closes on an equal-length run. A lone inline backtick followed by a code fence used to “close” on the fence’s first backtick, leaving
{/<in the surrounding prose unescaped (an MDX compile error that fails the operation page’s build) and entity-escaping the fence body. - e4506a0: Stop escaping
>in OpenAPI descriptions rendered to MDX. It isn’t MDX-special on its own, and escaping it turned a common> **Note:** …blockquote into a literal “> Note:” paragraph. - e4506a0: Give every distinct OpenAPI tag a unique slug. Slugging strips all non-ASCII, so two non-Latin tags (
ペット,注文) both collapsed tooperations— merging their routes and sidebar groups and dropping the second tag’s overview section. Colliding slugs now gain-2,-3, … in first-seen order. - e4506a0: Leave shifted and alted keyboard chords to the browser. Ctrl+Shift+I (DevTools) toggled the Ask AI panel and Ctrl+Shift+K (Firefox web console) opened the search dialog, because the ⌘I/⌘K matchers ignored the Shift and Alt modifiers.
- e4506a0: Resolve explicit sidebar refs written with a trailing slash. A hand-written
"guides/"normalized to/guides/, missed the slashless/guidesroute, and the item was silently dropped from the sidebar with no diagnostic. - 6554485: Keep the docs sidebar scrolled to the current page across navigations. Each page load previously reset the sidebar’s own scroll container to the top, so on long sidebars the viewport visibly jumped away from the link you just clicked. A pre-paint inline script now centers the active link when it would otherwise be out of view — this also fixes deep links landing with the current page’s link below the fold. Short sidebars, and pages whose active link is already visible, are untouched.
- e4506a0: Only unwrap a single rendered paragraph in
<Prompt>,<Frame>, and<Tooltip>. The greedy unwrap matched across multiple paragraphs, injecting unbalanced</p>/<p>tags viaset:html— a multi-paragraph description, caption, or tooltip label broke the surrounding layout when the parser re-parented the stray tags. - e4506a0: Dedupe repeated
<Update>ids on a page, mirroring the accordion id dedupe. Two entries labeled “Bug fixes” produced duplicate DOM ids, so the second entry’s self-anchor permalink jumped to the first one; later duplicates now gain-2,-3, … and their header anchors follow. - e4506a0: Make
useAskAI().reset()revoke the in-flight stream, matching the built-in island. Resetting mid-answer used to let the next chunk re-append an orphaned assistant bubble onto the emptied conversation, and a fetch error after reset resurrected the entire pre-reset history. The request is now aborted and stale writes are discarded.
July 22, 2026
Patch Changes
- 42522fc: Downlevel
<Component>to its example’s source in agent-facing Markdown. The/<route>.mdmirror,llms-full.txt, and the MCPget_pagetool now render<Component path="…" />as a fenced code block of the example’s source (the same code the on-page “Code” tab shows) instead of leaving the raw JSX tag, so agents reading a page get the component’s code rather than an opaque element. An unknown path (or a missingpath) is left verbatim, and a same-nameai.markdownComponentsserializer still overrides the built-in. - a71f70e: Add a
dateFormatconfig option for the “last updated” stamp and the changelog timeline. Both surfaces previously hardcodeddateStyle: "long"; they now share a configurable pass-through toIntl.DateTimeFormatoptions, defaulting to{ dateStyle: "long" }so existing sites are unchanged. Set a preset (dateFormat: { dateStyle: "medium" }) or a numeric house style (dateFormat: { year: "numeric", month: "2-digit", day: "2-digit" }); dates still render in the site’s locale and in UTC unless atimeZoneis given. - 68fc939: Harden the agent-facing code-fence helper against a polynomial-time regex (ReDoS). Trailing newlines are now stripped with an unambiguous pattern, so example source with many interior blank lines can’t force quadratic backtracking.
- ee77cfd: Stop long OpenAPI routes from overflowing the native API reference layout. An operation’s heading now wraps a long
METHOD /pathtitle instead of clipping it off the content column, and the overview list rows stack the summary over the route (each getting the full row width) and wrap a long route inside the card — dropping the duplicate path that overlapped the label when a spec sets no summary. - 93a94a2: Fix two responsive/mobile layout issues. Twoslash code blocks now wrap their lines on narrow screens instead of pushing the page sideways (hover popups still escape as before), and a long site title in the header now truncates on one line instead of wrapping into the fixed-height bar.
- a27c543: Add a
scalarpassthrough object to theopenapiandasyncapiconfig blocks (Scalar renderer). Any Scalar configuration set there is forwarded verbatim to the embedded<ScalarComponent>—localization(to translate Scalar’s own UI),agent,hideTestRequestButton,orderSchemaPropertiesBy, and the rest. Options in thescalarobject win over Blume’s derived spec/theme config, making it a full escape hatch to Scalar’s API; the dedicatedthemefield remains the ergonomic shorthand. - fa07dc4: Create the
.blume/node_modulesdependency junction when an isolated linker (Bun’sisolatedmode, pnpm) dedupes the workspace’s ownastrodependency to Blume’s copy. The walk from.blume/found the “correct” astro through the workspace’s direct-dep symlink — in a directory holding none of Blume’s integrations — so the junction was skipped and the build died onCannot find module '@astrojs/mdx'.
July 20, 2026
Patch Changes
- 6cf2995: Allow custom Shiki theme objects in
markdown.codeBlocks.theme.lightandmarkdown.codeBlocks.theme.dark. Custom themes now flow through fenced code, inline highlighted code,<CodeBlock>, and<Diff>alongside bundled Shiki theme names. - cb30a40: Recognize the localized/based root tab when scoping the sidebar. With i18n enabled and header tabs configured, a non-default locale’s tabs arrive localized (
/becomes/en), but render-time scoping compared the active tab against a bare/— so on any/en/...route the root tab was misread as a section tab, and a root-level(group)folder (whose path is exactly the locale prefix) collapsed the sidebar to that one group. The same bare comparison blanked the sidebar entirely under abasePathwith a root tab. The navigation now carries its root in the tabs’ own path space (/,/en,/docs) and the sidebar scoping compares against it, so non-default locales show the full tree minus tab-owned sections, matching the default locale. - aa3f588: Size
<Component>preview panes to the rendered example instead of the source line count. The generated frame page now observes the example with a ResizeObserver and reports its height to the docs page, which applies it to both the Preview and Code tabs — so short sources that render tall UIs no longer clip, and long sources that render small components no longer float in dead space. The line-count estimate remains the SSR/no-JS initial height (288px floor, 400px ceiling; the measured height is unceilinged up to the viewport), with a height transition so the settle on lazy load doesn’t snap. Examples that resize after load keep the pane in sync, and viewport-tracking examples (h-screen) can’t feed the measurement back into unbounded growth. - 103733d: Re-point a cache-restored
.blume/node_modulesjunction that resolves a superseded Blume install. A restored build cache (e.g. Vercel’s) could resurrect the junction pointing into the previous release’s store directory; because releases rarely bump Astro, every astro-based health check passed straight through the stale link, and the freshly generated Astro config then imported the old package — crashing on any export added since (blumeTwoslashTransformer is not a function). The junction is now dropped and relinked whenever the directory behind it holds ablumeother than the one running.
July 19, 2026
Patch Changes
- b5ed87a: Add
seo.og.titlesto name the generated Open Graph card of a custom.astropage, keyed by route. A custom page has no frontmatter to read, so its card was titled by humanizing the last URL segment — turning/cliinto “Cli” with no way to say “CLI”. An entry here wins over the humanized segment;"/"addresses the home, whose card otherwise carries the site title. - 52cfa77: Give each GitHub-sourced changelog release page a unique meta description derived from its release notes, instead of every release falling back to the site-wide description. The summary is the notes reduced to plain text — section headings (“### Patch Changes”) and changesets’ commit-hash bullet prefixes dropped, code fences and link syntax stripped — then cut at a word boundary to fit the 110–160 character search-snippet range
blume auditchecks for. It’s carried asseo.description, so it feeds the meta/OG/Twitter description tags without adding a visible lede paragraph to the page. - 20ae16d: Fix
blume checkfailing on generated files under a strict tsconfig: island and example wrappers now mirror the wrapped component’s props ontoAstro.propsso required props type-check through the spread, and the OG endpoint’scustomRoutesarray is explicitly typed so an empty list is no longer an implicitany[]. - 9a6345f: Decouple Twoslash from the project’s hoisted TypeScript so sites can use TypeScript 7 (tsgo). The generated Astro config now wires in
blumeTwoslashTransformerfromblume/markdown, which compiles Twoslash fences with Blume’s own pinned classic TypeScript (passed explicitly astsModuleplustsLibDirectoryfor the default lib files) instead of resolving whatevertypescriptthe surrounding project installed — under TS7 that package’s main export is a version stub with no compiler API and nolib.*.d.tsfiles, so anytwoslashfence crashed the build. - 32ced54: Evaluate
{frontmatter.*}prop expressions when downleveling components for agent-facing output. Serializers — built-in andai.markdownComponents— now receive the same values the rendered page shows instead of empty props, and the page’s front-matter is exposed on the serializer context. - ff37999: Stop
blume auditflagging the changelog RSS feed link in llms.txt as a stale entry. The stale-entry check compared each llms.txt target against built pages only, but the generator itself links non-page assets — the changelog RSS feed — so a target the static file index serves now counts as valid, the same way redirect targets may land on a served asset. - f322ac1: Warn when an index page’s own frontmatter
titlediverges from its folder’s explicitmeta.title. The two are resolved independently, so a translator can update one and forget the other — the sidebar looks right while the page’s own<title>/heading stays stale. Reported asBLUME_NAV_INDEX_TITLE_MISMATCHfromblume doctor/blume check. A mismatch is reported once — untranslated pages filled in from the fallback locale are exempt, since their fix is translating the page, not editing the fallback locale’s frontmatter. - eb25103: Link Blume’s nested integrations into the generated runtime when npm’s split install hoists astro away from them. An
overridesastro pin plus an incrementalnpm install— the exact steps the Astro-conflict warning recommends — hoists astro to the project root while@astrojs/mdxand Blume’s other deps stay nested undernode_modules/blume/node_modules, so fresh checkouts andblume build --isolatedfailed withCannot find module '@astrojs/mdx'. The dependency link now probes for the integrations instead of astro alone and links the nested set, letting astro keep resolving from the hoisted copy. - 80dc1c1: Fail
blume build(exit 1) when any page fails frontmatter validation, instead of silently dropping the invalid pages and reporting a green build. Pass--no-strictto restore the old lenient behavior — the build then warns how many pages were dropped instead of printing an unqualified success. - 7c75bb8: Warn when two sidebar siblings (pages or folders) resolve to the same explicit or numeric
order, instead of silently falling back to an alphabetical tiebreak. Reported asBLUME_DUPLICATE_SIDEBAR_ORDERfromblume doctor/blume checkand any other diagnostics consumer.
July 19, 2026
Minor Changes
-
9fec53f: Add
blume audit, an offline site audit that replaces a hosted SEO crawler.blume auditreads the builtdist/HTML, joins each page back to the.mdxit came from, and reports SEO and site-health issues that name both the URL that is wrong and the front matter line that fixes it:⚠ Meta description too long or too short 5 pages /docs/configuration/export content/docs/configuration/export.mdx:3 fix: Rewrite `description` in the frontmatter to fit the length range.It runs 87 checks across content, duplicates, indexability, links, redirects, social tags, localization, assets, sitemap, robots.txt, structured data, and AI discovery. Findings are rolled up by check rather than dumped per page, and any tier that didn’t run says so. The check set deliberately skips things that can’t happen to an Astro-built site (missing hashed bundles,
rel=nofollow) in favor of checks a crawler can’t do — broken#fragmentanchors,draft: truepages that shipped,llms.txtheld to the sitemap’s standard, and Open Graph images verified as bytes.Flags:
--url <origin>also probes a live deployment for whatdist/can’t show (bad rewrites, missing compression,X-Robots-Tagdeindexing).--externalprobes outbound links.--claude/--codexwrite the JSON report and open the agent interactively to fix each finding at its source.--fail-on <severity>(defaulterror) as the CI gate, plus--only/--skip,--json,--verbose, and--list-checks.
blume validateis unchanged — it remains the fast source-level link check that needs no build.
Patch Changes
-
3aee378: Strip trailing slashes from the deploy adapter root with a linear scan instead of a
/\/+$/regex. The old pattern could backtrack polynomially on a root path containing long runs of/(CodeQLjs/polynomial-redos); the new trim is O(n) and yields the same single-trailing-slash directory URL. -
f779fd5: Make
blume audit --claude/--codexwork on Windows: npm installs the agent CLIs as.cmdshims that Node only runs through a shell, and cmd.exe can’t carry the multi-line prompt as an argument — the handoff now writes the prompt to a file and launches the shim with a one-line pointer, and a missing executable still gets the install hint instead of a raw ENOENT -
d99726a: Account for
deployment.basethroughout the audit: link, sitemap, hreflang, llms.txt, asset, and og:image checks now strip the deployment base from emitted URLs before comparing them to the built file tree, instead of reporting every internal link and sitemap entry as broken on subpath deploys -
d99726a: Make the audit’s double-slash check able to fire on the case it was written for: an href like
//docs/xfrom a trailing-slash base is now flagged (once per target) instead of being silently skipped as a protocol-relative external link -
d99726a: Replace a raw NUL byte in the audit’s duplicate-content grouping key with the
\u0000escape, so the shipped source is valid text (git diffed it as binary and grep skipped it) -
d99726a: Exempt the home page from the audit’s orphan-page check when it lives under a
basePath—/docswas reported as an orphan even though/never is -
d99726a: Stop reporting a redirect to a served static file (
/old-whitepaper→/files/whitepaper.pdf) as broken — the audit now resolves redirects against files as well as pages -
d99726a: Keep a gap between the URL and source-file columns in the audit report when a URL reaches the column width, instead of fusing them into one string
-
d99726a: Fix two robots.txt matching gaps in the audit:
Disallow: /docs*$now matches everything under/docs(the trailing wildcard absorbs the anchor), and trailing-slash rules likeDisallow: /page/are matched against the sitemap<loc>as served instead of a slash-stripped copy -
d99726a: Only report
ROBOTS_META_UNEXPECTEDwhen the robots meta actually blocks indexing — a page declaringindex, followis no longer claimed to “not be indexed” -
8d7e779: Format the changelog timeline’s dates in the configured locale. The timeline hardcoded
en, so an i18n site showed two languages at once: a page’s “last updated” stamp honored the locale while/changelogstayed English./changelogis an unlocalized route whose chrome already renders in the default locale, so its dates now follow that same locale. English sites are unaffected. -
d518958: Add
inlineandparamprops to theTabscomponent.inlinerenders borderless — a tab strip on a full-width rule with the content flowing beneath as prose — instead of the bordered box.paramsyncs the active tab to a URL query param instead of the hash; because each group owns its ownparam, severalTabscan share a page and every selection is deep-linkable (a link ending in?install=windowsopens on that tab). Existing boxed, hash-syncedTabsandCodeGroupare unchanged. -
d9dfdba: Stop shipping underscore-prefixed
.astrofiles inpages/as routes. Blume injects user pages itself and globbed every.astrofile, so private partials — shared layouts and home-page sections likepages/_home/Hero.astroorpages/_FeatureBrowser.astro— were each built into their own HTML page. Page discovery now honors Astro’s convention: any file or folder whose name starts with_stays importable but is never routed. -
437ce03: Fix
blume devunder pnpm’s default isolated linker. The generated runtime now checks Astro through the same physicalnode_modulesancestor lookup used by its ESM config, instead of mistaking pnpm’s CommonJS-onlyNODE_PATHexposure for a resolvableastro/configimport. -
9aad3e6: Add
frontmatter.extend: opt-in custom frontmatter keys, each validated by a user-supplied schema. Page frontmatter stays strictly validated by default; a project can now declare extra keys (e.g.owner,reviewedAt) inblume.config.ts, mapped to schemas consumed through the Standard Schema interface — so Zod (any version the project installs), Valibot, and ArkType all work. Declared keys are validated on every page (mark them.optional()to relax), validated values are preserved on each page record’scustomfield, and every other key keeps the strict typo-catching behavior. -
368b258: Emit a
_headersfile for static builds so hosts serve the raw AI-ready endpoints with an explicitcharset=utf-8. Blume’s/<route>.md,/<route>.mdx, and.txtoutputs (llms.txt,llms-full.txt) are valid UTF-8, but common static hosts serve them astext/markdown/text/plainwith no charset — so browsers fall back to Windows-1252 and non-ASCII docs (Japanese, accented Latin, …) render as mojibake when the raw URL is opened directly. The new_headerspins the samecharset=utf-8Content-Type the dev/server runtime already sends; Netlify and Cloudflare (Pages/Workers static assets) honor it, and hosts that ignore_headers(Vercel, S3) are unaffected. The globs carry anydeployment.base/basePathstack, and a_headersyou ship inpublic/is left untouched — exactly like_redirects. -
d99726a: Prefix the
_headers.txtcharset rule with onlydeployment.base—llms.txt/llms-full.txtare served at the deploy root, so abasePathdeployment shipped a/docs/*.txtrule that matched nothing and left the mojibake fix inert -
a4453e4: Upgrade Takumi to v2 via takumi-js. Emoji in titles now render as Twemoji glyphs, fetched once per glyph per build. The OG card palette accepts any CSS color, matching
theme.accent— a color the renderer can’t parse now fails the build instead of silently falling back.renderOgImage(exported fromblume/og) now returns aUint8Arrayrather than aBuffer. -
548bdd7: Link the RSS feeds from
llms.txt. The generated index mirrored the docs navigation but never referenced the per-content-type feeds (e.g./blog/rss.xml), so an agent readingllms.txthad no pointer to fresh blog posts or changelog entries. The index now closes with an## RSS Feedssection listing each configured feed that has pages, under the same condition the feeds themselves exist — RSS enabled and an absolutedeployment.site— and carrying anydeployment.basesubpath. This mirrors theartifacts.feedslist already emitted inagent-readability.json. -
d99726a: Fix a data-loss bug in the blume-migrate Mintlify codemod: renaming a key into a parent block that appears earlier in the frontmatter (e.g.
canonicalinto an existingseo:) deleted the wrong line and left the source key behind; icon remaps also now preserve trailing comments, and the skill references no longer document the pre-1.0.3 top-levelmcpconfig -
8769dd1: Add
seo.og.fontsto load Google Font families into the Open Graph card renderer. Takumi’s built-in font covers only Latin, so a non-Latin page or site title (CJK, and so on) rendered as tofu with no way to fix it. List the families by name — bare strings, or{ name, weight, style }for weight/style — and Blume fetches them from Google Fonts at build via Takumi’sgoogleFontshelper, registering only the glyph subsets each title uses. Latin text renders unchanged. -
d99726a: Add
seo.og.fontsto theOgConfigauthoring type — the schema accepted it but TypeScript rejected it inblume.config.ts, making the documented CJK/tofu fix untypeable — and correct the palette doc comment to say any CSS color works, not just hex -
de6403c: Fix the OG image build failing with “Cannot find native binding” on Vercel (Linux). The
googleFontsOG-font loader is imported fromtakumi-js/helpers, but only the baretakumi-jswas externalized for the static-prerender Vite environment — which matches by exact specifier, so the subpath (and the native@takumi-rs/corebackend it pulls in) got bundled into the prerender chunk, relocating the.nodebinding lookup. Externalizetakumi-js/helpersand the@takumi-rs/*packages so the native backend always resolves fromnode_modulesat runtime. -
bb9737e: Render authentication requirements in the native OpenAPI reference. Operations that declare security requirements — their own
security, or the document’s root default — now show an Authorization section above their parameters: the credential’s carrier (Authorizationheader, API-key header/query/cookie), a human label per scheme type (Bearer token, Basic auth, API key, OAuth2, OpenID Connect, Mutual TLS), the scheme’s description, and OAuth scopes. Multiple requirement alternatives render as “or” groups (schemes within one requirement are required together), an empty{}requirement marks auth as optional, andsecurity: []on an operation keeps it public with no section. The generated code samples now send a matching placeholder credential (e.g.-H "Authorization: Bearer YOUR_TOKEN"), with a spec-declared explicit header parameter still taking precedence, and a query-borne API key appended to the sample URL. Previously the renderer ignoredsecurityentirely, so authenticated endpoints were indistinguishable from public ones. -
d99726a: Align the accessible heading level of the Authorization and Parameters sections with Request body and Responses on API reference operation pages
-
d99726a: Stop duplicating a query API key in request samples when the spec also declares the credential as an explicit query parameter — the parameter’s own example now wins, matching the header behavior
-
e6be2f6: Match unordered-list bullets to ordered-list numbers in prose.
--tw-prose-bulletsnow defaults to--blume-muted-foreground(like--tw-prose-counters) instead of--blume-border, which rendered bullets much lighter than the numbers beside them. -
d99726a: Normalize
deployment.basebefore composing it into redirect targets — a trailing-slash (/base/) or bare (base) value produced/base//newor relative destinations in Astro redirects and the platform redirect files -
93ea41b: Fix redirects escaping the site under
deployment.base. A redirect’stowas only ever rewritten withbasePath, never with the deployment base, so withbase: "/docs"ato: "/new"emitted a redirect to/new— outside the base, 404ing on a subpath deploy (GitHub Pages project sites, most commonly) with no build-time error. Astro appliesbasewhen it builds the match pattern forfrom, but resolves a destination either by regenerating it from a matching route’s segments (which carry no base) or by passing it through verbatim — neither prependsbase, so Blume now applies it totoitself. The two bases also compose correctly:deployment.baseandbasePathset together stack as{base}/{basePath}, which the old front-prepend could not produce in that order.The static host redirect files (
_redirects,vercel.json,blume-redirects.json) had the mirror-image bug on the other side: they are matched against the real served URL, butfromwas written without the deployment base, so it never matched. Both sides now carry the full stack. Redirects are authored root-relative in every case, and a base already written intotoby hand is preserved rather than doubled. -
d99726a: Warn when a
search.popularlink uses an image or inline-SVG icon — the client search island can only render built-in icon names, and the silent fallback to the file glyph was exactly what the validator was meant to catch -
1c18379: Add
search.popularto curate the Cmd+K empty-state link list. When set, each{ href, label, icon? }entry replaces the default first-six sidebar pages — useful on multi-tab sites where sidebar order surfaces the wrong section. Eachhrefis authored root-relative and picks upbasePathautomatically (external URLs pass through);icontakes a built-in icon name and defaults to a file glyph. Omit or leave empty to keep the sidebar fallback. -
ea4c560: Stop headings inside a
<Prompt>block from leaking into the page’s table of contents.Prompt.astrorenders its children into a permanentlyhiddennode (used only to build the copy-to-clipboard and Cursor-deeplink text), butextractHeadingshad no way to know that — any##inside a<Prompt>was extracted as a real page heading and appeared in the “On this page” sidebar, linking to content that never renders visibly. Heading extraction now tracks<Prompt>/</Prompt>nesting depth the same way fenced code blocks already are, and skips headings while inside one. Tag detection is anchored to line starts — block-level JSX in MDX starts its own line — so a prose or heading mention of<Prompt>never opens a hidden region, and a tag whose attributes span several lines only counts once its closing>shows it isn’t self-closing. -
c6ca54a: Add a themeable content-column width. A new
--blume-content-widthtoken (default42rem) is exposed as amax-w-contentutility through Tailwind’s--container-contenttheme key, and the article, breadcrumb, mobile table of contents, page feedback, pagination, and last-updated line now use it instead of hardcoding42rem. Override--blume-content-widthto re-measure the whole column at once; the default is unchanged. -
31ee259: Bundle the Vercel serverless function with its chunks, virtual middleware, and dependencies. With
deployment.output: "server"andadapter: "vercel", the render function (.vercel/output/functions/_render.func) shipped asentry.mjsalone, so any server-rendered request — the Docs MCP endpoint, Ask AI — 500’d at runtime withERR_MODULE_NOT_FOUND. The adapter resolves both its Build Output tree and its@vercel/nftdependency trace against Astro’sroot, which Blume points at the hidden.blumeruntime; the trace’s base then excluded the server bundle (which lives underoutDir, outside.blume) and collapsed to a single file. The adapter is now shown the real project root, so the trace covers the function’s chunks andnode_modulesand the output lands at the project root natively. Projects inside a workspace were unaffected — nft’s base search climbed past.blumeto the workspace root — so this only ever broke standalone projects.
July 15, 2026
Patch Changes
-
cf8fa22: Fix the spacing inside directive callouts (
:::note,:::success, …). The global prose paragraph rule leaks a 1rem margin onto the callout’s paragraphs even though the callout isnot-prose, and with a title the body paragraph isn’t the first child — so that margin stacked under the title’s own gap and left a too-large space between the title and the body. The Callout now overrides that margin locally for a uniform, compact gap between the title, paragraphs, and lists. Callouts without a title and normal prose spacing are unchanged. -
bb5944d: Gate the language icon on
data-languageso it no longer overlaps the first code line on header-less standalone<CodeBlock>s. The icon transformer runs whenever icons are on, but a standalone block without atitlenever getsdata-language(the header bar that reserves the icon’s space), so the absolutely-positioned icon sat on top of the first line. Fenced code and titled blocks are unaffected. -
d59be0a: API references (OpenAPI, AsyncAPI) no longer add a header tab automatically. Point a
navigation.tabsentry at the reference route to surface it — this also lets you control the tab label and scopes the operations sidebar for the native renderer. -
f02b94e: Introduce the
blume-migrateagent skill. It teaches an AI agent to migrate an existing docs site — Mintlify, Docusaurus, Fumadocs, Nextra, Starlight, or any docs framework — into an idiomatic Blume project: translating the source config toblume.config.ts, restructuring content into filesystem-derived navigation (withredirectsfor every moved route), rewriting callouts to:::directives, converting icons to Lucide, inlining snippets, and pointing generated API references atopenapi.sourcesinstead of porting endpoint stubs. Ships with per-framework mapping references, a deterministic Mintlify codemod for the icon/frontmatter pass, and monorepo/Vercel integration recipes. The skill is bundled in the package underskills/. -
a421a1e: Make the generated runtime type-check cleanly under
blume check --strict --isolated(and any project whosetsconfigincludes the generated files). Previously a valid site could fail with dozens of errors in the generated.blume-verifyfiles:- The raw-Markdown, RSS, and OG endpoints imported
data.jsondirectly, so TypeScript widened the JSON (navigationkindtostring, empty arrays tonever[], theme mode tostring) and rejected it against Blume’s own types. They now import the typedblume:datavirtual module. - Endpoint handlers (
GET,getStaticPathscallbacks) and the changelog/content-page helper functions had implicit-anyparameters; they now carry explicit types. - The OG endpoint’s PNG
Bufferis wrapped in aUint8Arrayso it satisfies theResponsebody type, and the empty component-overrides module and theblume:examples/blume:examples-themevirtual modules now declare types.
Also stops
blume check --isolatedfrom reloading a runningblume devserver:.blume-verifyis now in Blume’s ignored-directory set, so generating the isolated runtime no longer trips the content watcher. - The raw-Markdown, RSS, and OG endpoints imported
-
6a97cb2: Reserve space for local per-mode SVG logos before they load.
-
5793ccf: Render Mermaid diagrams in
blume dev. Mermaid statically imports dayjs as CommonJS (dayjs/dayjs.min.js), and in dev Vite served that dependency un-pre-bundled, so it exposed nodefaultexport and mermaid threwdoes not provide an export named 'default'— leaving diagrams blank (the production build already handled the interop). Mermaid now goes through Vite’s dependency optimizer, which bundles dayjs with correct CommonJS interop. Because Blume’simport("mermaid")lives insidenode_modules/blume— a path Vite’s optimizer scan doesn’t crawl in a standalone install — the diagram library was never discovered on its own, so it’s included explicitly via the nestedblume > mermaidform (mermaid isn’t a direct dependency of the generated project). -
5c0b0f0: Add custom logo and palette settings for generated Open Graph cards.
-
1bdc849: Truncate long page titles in the previous/next pagination links so they no longer overflow their pill container, and allow inline code inside table cells to wrap instead of forcing the whole table to overflow horizontally.
-
f2ffcb5: Prerender Cloudflare adapter builds in Node so build-time
node:imports resolve. Astro 6 changed the@astrojs/cloudflaredefault prerender runtime from Node to workerd, which broke Blume prerender on Cloudflare (No such module "node:path"andnode:fsusage in Blume’s build-time content tooling). The generatedastro.config.mjsnow passesprerenderEnvironment: "node"to the Cloudflare adapter. On-demand pages still run in workerd at request time. -
d11a90c: Stop the
Promptcomponent’s copy button from shifting the layout. On copy, the button’s label swapped “Copy prompt” → “Copied”, and because the button was sized to its text, it shrank — giving the description beside it more room and reflowing it (a two-line description would collapse to one line, then jump back). Both labels now share a single grid cell, so the button is always sized to the wider “Copy prompt” and never resizes when the state changes. -
fcdc3b3: Match native browser controls, including scrollbars, to the active color theme.
-
3795fbb: Keep root navigation tabs active on descendant routes unless a more specific tab matches.
-
e5aa042: Use the
rounded-blumeradius token instead of a hardcodedrounded-fullon thePageFeedbackandPaginationbuttons so they respect the configuredtheme.radius. -
32e29f0: Keep the text caret inside the search input on mobile. The search dialog was vertically centered, so when the on-screen keyboard opened and the viewport shrank, the dialog re-centered and the input moved up while the native caret stayed put — stranding the cursor below the input, over the results. The dialog is now top-anchored on small screens (like DocSearch/cmdk) so the input sits above where the keyboard appears and doesn’t move, and its height is capped to the dynamic viewport so it fits above the keyboard. Desktop keeps its centered layout.
-
7806565: Resolve a section tab’s link to its first page when the section has no index page, so the tab no longer 404s. A tab’s
pathstill scopes its sidebar section and matches the active tab, but the clickable target now falls back to the first page in the section (sidebar order) when nothing lives at the path itself — e.g./exampleswith only/examples/hello-worldlinks to that page instead of a missing/examples. -
fe21c54: Wrap markdown tables in a horizontal-scroll container so wide tables no longer overflow the prose column. The wrapper is framed with a rounded border and cell padding, and header labels stay on one line, so a clipped wide table reads as scrollable rather than cut off.
July 13, 2026
Patch Changes
- e2f902c: Render the Ask AI trigger from the shared header instead of wiring it up per page. Custom pages built on
PageLayout(a landing page, most of all) never passed the header’saskslot, so the Ask AI button — and the search modal’s hand-off to it — silently went missing on them while the generated docs, changelog, and reference pages had it. The header now owns the trigger and reads whether Ask AI is on from the config, so every page gets it; passaskEnabled={false}to opt a page out. - 66b721b: Move the MCP server config under
aiinblume.config.ts, alongside the other agent-facing features. Renamemcp: { … }toai: { mcp: { … } }— the shape of the block is unchanged. - c8ae77e: Fix
ERR_MODULE_NOT_FOUNDin a deployed server function. Surfacing an adapter’s deploy bundle out of.blumeresolved every traced dependency’s symlink against the source dir, so the links pointed into a directory the same step then deleted — the function died on its first external import (Cannot find package '@orama/orama'with Ask AI or the MCP server enabled). The bundle is now copied verbatim, leaving those links relative and internal to it.
July 13, 2026
Patch Changes
-
d62dbd0: Harden the pre-paint inline scripts and the route-normalizing regexes against the issues CodeQL flagged.
The theme and banner scripts in
<head>used to be built by interpolating config values into JavaScript source withJSON.stringify. JSON escaping isn’t a code-context escape —</script>and U+2028/U+2029 pass straight through it — so a craftedbanner.idor theme value could break out of the script. Both scripts are now constants and take their values fromdata-*attributes on their own<script>tag, which Astro HTML-escapes.ReferenceLayouthad drifted to its own inline copies of both scripts; it now shares the same module asRootLayoutandPageLayout.Route and slug normalization used
/^\/+|\/+$/-style patterns to trim leading and trailing separators. Those take quadratic time on a long run of the trimmed character, and they run on values that come from outside Blume (configured routes, OpenAPI spec URLs, the site origin), so a pathological config could hang the build. They’re replaced by linear trimming helpers incore/trim.ts; behavior is unchanged.
July 13, 2026
Patch Changes
- 6afb56a: Declare the
blumebin asbin/blume.mjsinstead of./bin/blume.mjs. The leading./is redundant and some package managers normalize it away when linking the binary; dropping it keeps the published manifest consistent with what installers actually write.
July 13, 2026
Major Changes
- 7372fe8: Blume v1.
Patch Changes
-
e922ff5: Stop “Clear conversation” from resurrecting an orphaned answer bubble while a reply is still streaming. Clearing mid-answer emptied the panel, but the in-flight stream kept re-appending its assistant message to the cleared conversation — a growing answer with no question above it. Clearing now aborts the in-flight request and revokes the stream’s right to write into the conversation, and asking a new question right after a clear works as before.
-
e922ff5: Match locale folders and dot suffixes case-insensitively, the way BCP 47 codes are defined. A configured
pt-BRwith the conventional lowercasept-br/folder (or anintro.pt-br.mdxsuffix under thedotparser) previously fell through as default-locale content at a literal/pt-br/…route — and the unconfigured-locale warning, which already compared case-insensitively, stayed silent about it. Those files now route as the configured locale, with the configured casing in routes and labels. -
e922ff5: The generated changelog index’s heading, page title, and meta description now come from the translatable
changelog.titleandchangelog.descriptionUI strings (translated in every built-in pack), joining the reveal button the template already localized. Previously the page rendered a hardcoded English “Changelog” heading, an English “ changelog“ title suffix, and an English description even on non-English default locales. -
1e5446f: Lengthen the default changelog description. “Product updates and release notes.” is 34 characters — under the 50-character floor search engines want from a meta description, and the generated
/changelogpage uses it for both its meta tag and its on-page subtitle. It now reads “Product updates, new features, and fixes from every release.” Sites that setui.changelog.descriptionare unaffected. -
e922ff5: The syntax docs now label callouts and
package-installblocks as MDX-only, matching the existing notes on diagrams and math. Previously a reader following the page could write:::noteor apackage-installfence in a.mdfile and get literal text or a plain code fence with no hint why. -
e922ff5: Apply the js-yaml 4-safe YAML engine to
matter.readas well. The front-matter wrapper previously exposed gray-matter’s ownreadhelper unwrapped, so reading a file through it would parse with the removedsafeLoaddefault and crash with “Function yaml.safeLoad is removed in js-yaml 4” — the exact failure the wrapper exists to prevent formatter()andmatter.stringify(). -
68520af: Exclude Vite’s pre-bundled dep cache (
node_modules/.vite/) from @vitejs/plugin-react. Astro’s react() replaces the plugin’s defaultnode_modulesexclude, so Babel (carrying the React Compiler) was re-parsing every 500KB+ optimized dep chunk in dev — the source of the “[BABEL] Note: The code generator has deoptimised the styling” messages. Blume’s own components stay covered by the compiler. -
e922ff5: Serve image-path icons from under
deployment.base.<Icon>emitted an image icon’s path (/brand/mark.png) as-is, so on a site deployed under a base path the request went to the domain root and 404’d while<Card img>, the logo, and every other image emitter were correctly rebased. Image icons now get the samewithBasetreatment; external URLs, data URIs, and relative paths are untouched. -
e922ff5:
@astrojs/vueand@astrojs/svelteare now declared as optional peer dependencies, matching@astrojs/netlifyand@astrojs/cloudflare. Projects using Vue or Svelte islands must install the matching integration themselves, and package managers now surface and satisfy that requirement instead of the build relying on an undeclared package. -
e922ff5:
blume build --isolatednow reports the build’s actual output directory on success. Aserveroutput build with the Vercel adapter lands its deploy bundle at.blume-verify/.vercel/output(it is never surfaced to the project root), but the message previously pointed at.blume-verify/dist, which that build never populates. -
e922ff5: The breadcrumb and pagination
<nav>landmark labels are now translatable via the newnav.breadcrumbandpage.paginationUI strings, with translations in every built-in language pack. Previously both were hardcoded English (“Breadcrumb”, “Pagination”) while the sibling chrome landmarks were dictionary-driven, so screen readers announced English landmark names on localized sites. -
e922ff5: Fix
ai.markdownComponentscrashing config validation in projects that resolve Zod 4. The schema used the single-argumentz.record(...)form, which Zod 4 rejects at schema-construction time, so any config parse failed before your settings were even read; it now uses the dual-compatible two-argument form. -
e922ff5: The MCP
get_pagetool’s description — shipped user-facing intools/listand the server card — no longer claims to return a page’s original Markdown source. The tool serves the agent variant (components downleveled to plain Markdown,<Visibility>resolved for agents), and the description now says so. -
e922ff5: The sidebar’s panel-stack back arrow and drill-in chevrons now mirror under RTL locales (
rtl:-scale-x-100), matching the pagination arrows and the panel slide animation, which already flipped direction. Previously the arrows pointed against the reading direction on RTL sites. -
d9590fc: Emit a complete Open Graph card in the page head, so crawlers and social validators stop flagging the metadata as incomplete:
og:url— the page’s canonical URL (rendered only whendeployment.siteis set, or a page overridesseo.canonical).og:type—articleon blog posts and changelog entries,websiteeverywhere else. Article pages also emitarticle:published_timeandarticle:modified_time.og:site_name— the sitetitle.og:image:width,og:image:height,og:image:type, andog:image:alton Blume’s generated OG card, so a crawler can lay it out without fetching the image. Anseo.imageyou supply yourself declares none of these, since its dimensions and format are unknown.
-
1e5446f: Give every generated OpenAPI page its own meta description. Operation and overview pages set no
description, so all of them fell back to the site-wide default — a spec with twenty operations shipped twenty pages carrying one identical description, which search engines treat as duplicate content. Each operation page now derives a description from the spec’s own prose (its description, or its summary) followed by the endpoint it documents, and the overview page uses the spec description. These land inseo.description, so they feed the meta tag without also printing as a visible subtitle above the body prose. -
80eb252: Emit X (Twitter) card tags, and add
seo.xfor account attribution:seo: { x: { handle: "@acme", creator: "@jane" }, }handlebecomestwitter:siteandcreatorbecomestwitter:creator— the one piece of X card metadata with no Open Graph equivalent to fall back to. A page can credit its own author withseo.x.creatorfrontmatter, which is what a guest post wants. The@is optional in both places.Every page also now emits
twitter:card,twitter:title,twitter:description, andtwitter:image:alton the generated card.twitter:cardpreviously rendered only when a page had an image; a page without one now gets the compactsummarycard instead of sharing as a bare link. -
e922ff5: The Scalar API reference shell now sets
<html lang>anddirfrom the default locale, mirroring the changelog index’s locale wiring, and renders the same localized skip-to-content link as the other layouts. Previously it hardcodedlang="en"with nodir— so RTL default locales rendered LTR chrome — and offered keyboard users no way to skip past the navbar. -
e922ff5: The search dialog’s section-filter “All” pill is now translatable via the new
search.allUI string, with translations in every built-in language pack. Previously it rendered hardcoded English inside an otherwise fully localized dialog. -
e922ff5: ⌘K / Ctrl+K now toggles the search dialog: pressing it while the dialog is open closes it, matching how ⌘I toggles the Ask AI panel. Previously the shortcut unconditionally re-opened, calling
showModal()on an already-open dialog — a silent no-op on evergreen browsers but anInvalidStateErroron older engines. The/shortcut stays open-only, andopen()itself now guards against an already-open dialog. -
e922ff5: Reject a
tocconfig whoseminHeadingLevelexceeds itsmaxHeadingLevel. An inverted range (including an explicit min above the default max of 3) previously validated fine and silently rendered an empty table of contents on every page; it now fails config validation with a clear message. -
e922ff5: Internal-error stack traces now relativize
.blume/runtime frames on Windows too. The remap previously matched only POSIX absolute paths, so drive-letter frames likeC:\...\.blume\...printed the full machine path instead of the project-relative.blume\...form tagged(generated).