Skip to content
Blume
Esc
navigateopen⌘Jpreview

Changelog

blume@1.5.3

August 20, 2026

Release

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.
blume@1.5.2

August 20, 2026

Release

Patch Changes

  • 7650327: Add ai.ask.retrievalmaxResults, excerptChars, and contextBudget — 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: contextBudget caps the total, excerptChars decides how deep into one long page an excerpt reaches, and maxResults caps 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/receive operation ships an interactive Try it panel: a payload editor prefilled from the message’s examples (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’s servers with a free-text URL override. The protocol-aware code samples (wscat, browser WebSocket, kcat, mosquitto_pub/mosquitto_sub) stay in lockstep with the form, filling the channel address template with the parameter values you type. On ws/wss bindings 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.playground mirrors openapi.playground, so it’s on by default with the native renderer and false turns it off. playground.proxy stays 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 honoring prefers-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-transition rule 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 -dark sibling of your icon file — the same name and directory with -dark before the extension, like icon.svgicon-dark.svg — and emits both icons behind media="(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 -dark file 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_FILE and friends) when resolving git last-modified dates and shallow-clone status. A parent git process exports an absolute GIT_DIR to 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 servers and 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, or true for the built-in /_api-proxy server 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, and playground: false turns it off.
  • e31ff0a: Add ai.openInChat to configure the “Open in chat” page action: false hides 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.defaultLocale was a Cyrillic, Greek, Hebrew, or Devanagari language the index collapsed to zero tokens and every query silently returned no hits. The script now comes from Intl.Locale.maximize()sr-Latn keeps Orama’s tokenizer while az-Cyrl is 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 literal string, and degrades a no-example, no-schema payload to {} rather than null. The built-in CORS proxy allowlist skips templated server URLs, and a proxy with no allowable origin warns at build time.
  • c289eff: Stop blume validate reporting colocated images as broken assets. A relative image embed (![](./diagram.png)) — 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 under public/, where it never lands: the image pipeline emits it to _astro/ from beside the content. Every such reference came back as BLUME_BROKEN_ASSET, so --strict failed 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/content endpoint 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/#fragment suffix, 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 than public/, and even when no public/ 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-current on archived pages: they link back to the current docs, so none of them is the current page there.
blume@1.5.1

August 16, 2026

Release

Patch Changes

  • ca28fb8: Generate an Open Graph card for the changelog index. The /changelog page previously shipped no og: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.05em from the theme itself, which also means any font you configure for display reads correctly at heading sizes instead of depending on tracking built into the font. Inter Tight remains available as the inter-tight slug.
  • ca28fb8: Let a user-supplied ogImage on PageLayout declare its alt text and pixel size via the new ogImageAlt and ogImageSize props, emitted as og: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 same WebSite structured-data graph the docs pages do. On by default; pass structuredDataEnabled={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 of https://site), so they byte-match the sitemap’s <loc> for the root route.
  • ca28fb8: Warn when lastModified runs 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 a BLUME_SHALLOW_GIT_HISTORY warning pointing at the fix (VERCEL_DEEP_CLONE=true on Vercel, fetch-depth: 0 for 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-face rules with font-display: swap.
  • ca28fb8: Redirect trailing-slash URLs to their slashless twins on Vercel with a 308. /docs/ and /docs previously both served 200 as duplicate URLs; the routing config now collapses the slashed form onto the canonical slashless one (the root / is untouched).
blume@1.5.0

August 16, 2026

Release

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 in blume.config.ts. With a versions block 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-version canonical: "self" and noindex remain 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 MCP search_docs/list_pages tools default to the current docs and accept an archived id or "all" (plus a new locale filter), get_navigation serves snapshot and per-locale trees, llms.txt sections archived versions with an “(archived)” label, llms-full.txt stays 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, and blume translate never 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 asyncapi block now defaults to renderer: "blume" — one real page per send/receive operation, 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 from securitySchemes, protocol-aware code samples (wscat/WebSocket for ws, kcat for Kafka, mosquitto_pub/mosquitto_sub for 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, so publish/subscribe map onto stable operation URLs. The asyncapi block gains renderer, codeSamples, and expandSchemas for full parity with openapi; per-source includeInSearch/includeInLlms/noindex now 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 from string-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 eject no longer drops ai.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, or page without an explicit navigation.sidebar config: set display in the folder’s meta.ts, or — sugar for folders with an index page — sidebar.display in the index page’s frontmatter. A generated group’s effective mode resolves index frontmatter first, then folder meta, then the global navigation.sidebar.display, then the flat default; 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 renders flat (self-delimiting group/page rows keep authored interleaving), and under i18n a fallback-filled index page never overrides a locale’s own meta.ts display. sidebar.display anywhere it is dead — a non-index page, the content root’s own index page, or any page under an explicit config sidebar — reports a new BLUME_SIDEBAR_DISPLAY_IGNORED warning instead of being silently dropped. Explicit config sidebars render exactly as before.

blume@1.4.3

August 11, 2026

Release

Patch Changes

  • c19bfeb: Render Ask AI answers through a dedicated Marked instance instead of mutating the shared marked singleton, so other components importing marked no longer inherit the panel’s breaks option 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.instructions for 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 unbounded fs call per discovered file, which on large sites risked EMFILE and held every page’s HTML in memory at once.
  • 9156701: Keep punctuation out of the index terms the segmenting search tokenizer produces. Intl.Segmenter follows UAX #29, which holds connector punctuation, combining marks, format characters and mid-number punctuation inside a word, so スネーク_ケース and robots.txt each arrived as a single word-like segment and were indexed as they stood — reachable only by retyping the punctuation, and unreachable from ケース or txt. 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.Segmenter seeded by the page’s lang. The whitespace split it replaces counted a fully written Japanese or Chinese page as a handful of “words”, tripping BLUME_AUDIT_LOW_WORD_COUNT on 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 _redirects semantics: 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-slugger via one shared helper instead of three identical per-component copies. Component ids now slug exactly like heading anchors: unicode letters are kept (Españolespañol instead of espaol) 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 normalizeRoute in core, and retire the last quadratic edge-trimming regexes in favor of the linear trim helpers.
  • 92e1d2e: Spawn agent CLIs (claude/codex handoffs, eval and translate runners) through cross-spawn instead of shell: true on Windows. cmd.exe no longer parses the argument list — codex’s -c flags 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 with ENOENT on every platform instead of surfacing as cmd.exe’s exit code 9009.
  • d56e235: Declare p-retry and nanotar as dependencies. Both were imported by openapi/parse.ts and ai/tar.ts but only declared in the monorepo root, so installs of the published package could fail to resolve them.
  • be0d582: Deduplicate the escapeRawHtml/unwrapParagraph helpers that were copied verbatim into <Prompt>, <Frame>, and <Tooltip> into one shared module. No behavior change.
  • e5a374a: Hand the CLI’s .env/.env.local cascade to dotenv.config({ path }) instead of a hand-rolled apply loop. The walk up to the repo root stays; the first-wins ordering, never-clobber-process.env semantics, 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-size instead 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/pt lengths, 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/list from 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.gz archives with nanotar instead 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 with BLUME_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_case no longer becomes snakecase, C# no longer becomes C — in the seo.description tags 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 &#123; 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-retry instead of a hand-rolled loop. The behavior is unchanged — three attempts, exponential backoff capped at 10s, non-retryable statuses abort immediately, and a sane Retry-After replaces the backoff rather than stacking on it (now covered by a test).
  • b15a87a: Render package-install command tabs through package-manager-detector (the engine behind ni), 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 removed yarn global. blume eject also 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.txt link 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’s Wed, 21 Oct 2015 07:28:00 GMT spelling) 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 like Guide: Architecture.md build 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" with aria-expanded/aria-controls/aria-autocomplete, results render in a labeled role="listbox" with grouped role="option" rows, and the highlighted result is surfaced through aria-activedescendant and aria-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.site with emitted paths through one shared helper built on ufo’s joinURL. 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.xml chunks when a site exceeds 50,000 URLs, the per-file cap search engines enforce. Smaller sites keep the single classic sitemap.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 a meta.ts title), so authored casing like OAuth2 or Größe renders 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 under redirects in blume.config to 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 slug collapsed 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-assets endpoint’s traversal guard on Windows: it compared a forward-slash directory against path.resolve output (backslashes there), which 404’d every staged remote-source asset. The guard now uses path.relative, which also closes the sibling-directory-name edge a bare prefix test admits.
blume@1.4.2

August 9, 2026

Release

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 Link response header, so it had no path to agent-readability.json. Every rendered page now carries the discovery links in its HTML head: describedby links to agent-readability.json and llms.txt, plus a text/markdown alternate pointing 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

blume@1.4.1

August 9, 2026

Release

Patch Changes

  • 8865982: The built-in Ask AI panel now streams through the public useAskAI hook 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. useAskAI gains an optional errorMessage option 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 dev with chokidar. Raw fs.watch required 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, and recursive handling 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 [31m sequences. 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 standard NO_COLOR/FORCE_COLOR overrides.
  • 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 an src attribute, 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 doctor against the package’s full engines.node range 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 against NaN and the check silently stopped working.
  • 6d7abb4: Parse .env files 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/.env cascade 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 with markdown-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.imageZoom behavior is unchanged — click to zoom, dismiss on click/scroll/Escape, opt out per image with data-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: readOnly fields 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>". Declared example/const/default/enum values keep their precedence, and circular $ref chains 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 like a * b or x > y lost 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 include globs 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 same include array 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-file parseEnv export 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 undocumented escapeHtml from blume/components/layout/search/types, import escape from 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: / plus Allow: /docs/) flagged every sitemap URL as blocked; and it treated each User-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 &#38;, 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 its extends resolution accepted two forms real tsc rejects (relative paths naming a directory, bare specifiers resolved through a package main). get-tsconfig follows tsc’s own semantics — JSONC, the full extends chain 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.
blume@1.4.0

August 8, 2026

Release

Minor Changes

  • 6afe539: Add declared facets: content.types.<type>.facets names custom frontmatter keys whose values become filterable metadata. Faceted values ride along on search documents (blume-search.json and the MCP snapshot), and the MCP search_docs and list_pages tools accept a filters object 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_pages shows each page’s, and the shared Orama index gains a facetTerms enum-array field so one static schema serves every project’s facet keys. Each facet name must be a declared custom key (per-type or frontmatter.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 committed blume.translations.json ledger records the source hash behind every translation, so reruns are incremental, and hand-authored translations are adopted rather than overwritten (only --force retranslates them). Under the dir parser, folder-nav meta.ts titles are translated too, copying every other key verbatim so per-locale sidebars keep their ordering. blume translate --check is the read-only CI gate: it exits non-zero when any translation is missing or stale, with --json emitting the shared diagnostics report shape.

  • 6afe539: Let MCP clients filter by content type. search_docs and list_pages accept an optional contentTypes array that narrows results to pages of the given frontmatter types (["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.json included), and the shared Orama index gains a contentType enum field filtered with an exact where match — the same mechanism the locale filter uses.

  • 6afe539: Add per-type frontmatter schemas via content.types.<type>.frontmatter. Where frontmatter.extend declares custom keys site-wide, a per-type declaration scopes them to pages whose frontmatter type matches — so a project can require an RFC’s status or a runbook’s service without loosening every other page. Keys follow the same rules as extend: 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’s custom field. A declaration for content.defaultType applies to pages that set no type, 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/markdown on Cloudflare server builds. Deployed Workers sites ignored the header — the .md mirrors 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: after astro build, Blume writes a small wrapper Worker in front of the adapter’s entry and scopes assets.run_worker_first in dist/server/wrangler.json to 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 .md mirror from the assets binding with Vary: Accept; every other request is delegated to the Astro Worker untouched, so only content routes pay the extra Worker hop. Cloudflare does not apply _headers to worker-first routes, so the wrapper re-stamps what the static layer would otherwise add on the routes it takes over — the homepage agent-discovery Link header, the homepage x-markdown-tokens estimate, and the Markdown charset=utf-8 — while negative rules keep the raw .md/.mdx URLs on the static fast path with their _headers treatment intact. The agent readability manifest now advertises contentNegotiation on Cloudflare server builds too.

  • bd939ba: Keep configured redirects off the Cloudflare worker-first routes, so they ship the status you configured instead of a permanent 301. On a server build Blume routes redirects through Astro’s own config, and @astrojs/cloudflare turns those into dist/client/_redirects entries carrying the exact status — a file only Cloudflare’s static layer reads. But assets.run_worker_first is derived from the first path segment of the content routes, so a single page at /docs/reference emits /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, _redirects is 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]), so computeRedirectStatus defaults 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_first rule 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 own run_worker_first rules are exempted too (a configured true is 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 .md URL.

  • 2e231bc: Serve configured redirects from the generated Cloudflare Worker itself instead of carving them out of assets.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/markdown negotiation, and redirects claimed by your own run_worker_first rules (including a bare true) 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 the Location header.

  • 50a9ea7: Keep blume dev from 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-injected react/compiler-runtime import 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, the islands/ 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.openIn template ("Open in {name}").

  • b62278b: Emit _headers on 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 _headers writer returned early unless output === "static", the Vercel routing-config injection only runs for that adapter, and the middleware that calls res.setHeader("Link", …) is mounted on astro:server:setup, so it is dev-only. A Cloudflare server build therefore served no homepage Link header at all, and no Content-Type on the extensionless well-known files — an API catalog went out with no media type rather than application/linkset+json.

    The gate is now readsHeaderFiles(), which is true for any static build and additionally for a Cloudflare server build: the Worker serves dist/client through its ASSETS binding, and Workers static assets honor _headers from 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/_headers rather than in dist. @astrojs/cloudflare writes its own _headers during the build (an immutable Cache-Control for /_astro/*), so testing dist read an adapter-generated file as a user opt-out and skipped silently — the fix above would not have fired without this.
    • When a _headers already 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 _headers that reached dist some way other than public/ (an integration writing it directly, say): that file previously suppressed generation entirely and is now appended to. Shipping public/_headers remains the opt-out.

    The charset rules in this file remain redundant on a server build, where the runtime endpoint sets Content-Type on the Response itself; they are harmless, because a static-asset rule only applies to a file served from that directory. The Link and 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.

  • f9f045c: Fix blume check failing on the generated MCP endpoint when ai.mcp is enabled. JSON imports widen literal types, so mcp-data.json could never satisfy McpData’s discriminated navigation nodes; the generated endpoint now asserts the snapshot back to McpData at the JSON boundary.

  • 7e9f7dd: The header’s search field and language switcher now collapse to compact icon buttons below the lg breakpoint (previously sm), 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 Tab panels 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)))], so p-0 applies only when the panel holds nothing but fences and mixed panels keep the inset like every other code-block host. template is excluded because a Tab with an icon renders 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-child sees two children on every icon tab and so silently reintroduces the double inset, while :only-of-type ignores the template (it only counts sibling pre elements) 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/cloudflare declares preserveBuildClientDir: true, so it keeps Astro’s dist/client + dist/server split and points the ASSETS binding in the dist/server/wrangler.json it generates at ../client — the deployed Worker serves dist/client and nothing above it. Blume still treated dist/ as the served root, so robots.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, and blume audit crawled dist/ and read client/ as a route segment, reporting ~118 phantom broken links. deployStaticDir and its isolated-build mirror isolatedStaticDir now resolve to dist/client for a Cloudflare server build, matching the existing Node standalone handling. Cloudflare static builds are unaffected — they have no client/server split, so the outDir root is still what ships.

  • 8d4fd17: Allow search.popular icons 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 every blume translate prompt, pinning register and dialect from a locale’s first translation and winning over an existing translation’s style on reruns.

  • 7e9f7dd: blume translate now 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.

blume@1.3.1

August 3, 2026

Release

Patch Changes

  • 4d7dc87: Negotiate Accept: text/markdown on 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 the llms.txt index — the machine-readable map of the site — served at /index.md and wired into the dev server, the Vercel routing config, and the homepage Link header’s rel="alternate" entry.
  • ff31ab5: Remove a polynomial-backtracking regex from the font-name slugifier’s dash trim
  • 02eb6c7: Fix the homepage Link header and Vary: Accept never being sent on Vercel deploys. The injected header routes sat after handle: "filesystem" in the Build Output config — the miss phase, which prerendered static responses never reach — so agent-readiness checkers saw no Link header on GET /. Both header routes now ride in the main phase, ahead of static-file matching.
  • de62812: Stamp an x-markdown-tokens header (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.
blume@1.3.0

August 3, 2026

Release

Minor Changes

  • a089dbc: Upgrade the AI SDK to v7. The generated Ask AI endpoint now passes instructions instead of the deprecated system option, and the optional provider peer dependencies moved to their AI SDK 7-compatible majors: @openrouter/ai-sdk-provider@^3 and @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 (![alt](./diagram.png)) next to your content is now optimized at build time end-to-end — compressed, converted to WebP, and stamped with intrinsic width/height so 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 new image config (domains, remotePatterns) authorizes remote hosts for optimization, mapping directly onto Astro’s image config.
  • bd2b8cb: Custom fonts: theme.fonts roles 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.fonts gains a matching local form ({ name, src, weight?, style? }), and when a config sets theme.fonts explicitly, the generated Open Graph cards now render in the theme’s display and body fonts automatically (explicit og.fonts still wins; og.fonts: [] opts out). Sites that never touched theme.fonts are 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-directory with the registered media type on every build surface (static hosts via _headers, Vercel server builds via a Build Output content-type override), advertised in agent-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 a SKILL.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.gz archives (execute bits preserved), and a discovery index at /.well-known/agent-skills/index.json with the v0.2.0 $schema and per-skill SHA-256 digests. Artifacts get explicit media types on static hosts, the index is advertised in agent-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, with service-doc and, for remote specs, service-desc relations) and the hosted MCP server (with its discovery document as the service description). Served as application/linkset+json on every build surface, advertised via a rel="api-catalog" homepage Link header and in agent-readability.json. Sites with no APIs emit no catalog.
  • f4e6ce7: Serve Markdown to agents through Accept: text/markdown content negotiation on Vercel server builds. The build splices header-conditional rewrite rules into the Vercel routing config, so a content-page request that prefers text/markdown gets the page’s raw-Markdown mirror at the same URL — with Vary: Accept on both variants — and agent-readability.json now advertises contentNegotiation only on deployments that honor the header.
  • 7d426b2: Add seo.og.site and seo.og.description overrides for the generated OG card’s footer URL and subtitle, and accept false on them (and on seo.og.logo) to hide that layer of the card entirely
  • a730bef: Check DNS-AID agent discovery in blume audit. When deployment.site is 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. Set BLUME_DOH_URL to use your own resolver.
  • cacab69: Register WebMCP tools on every page. Agentic browsers with a model context (navigator.modelContext or document.modelContext, provideContext or registerTool) 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), and list_pages (the llms.txt index). The script is tiny and no-ops in browsers without the API. On by default; set ai.webmcp: false to opt out.
  • 8567927: Advertise the agent-facing surface with an RFC 8288 Link header on the homepage. The header points agents at agent-readability.json and llms.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 _headers file 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.json now declares the published $schema, a reverse-DNS name derived from the site host, title, websiteUrl, and remotes transport 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 existing transport/url fields 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: false still scroll horizontally

  • cbec130: Include deployment.base in the generated OG card’s footer site text, so a subpath deploy (like a GitHub Pages project site) shows user.github.io/repo instead 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, and blume init scaffolds 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 linear trimEnd helper 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 blume package instead of a bare node_modules/blume/docs. The blume and blume-migrate skills now tell agents to locate the package from the workspace that depends on it (via require.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 --verbose now 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 zod dependency 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 updated node_modules (a restored CI cache that a dependency bump left half-reconciled) resolve Scalar’s schemas against a Zod without z.function().optional, failing builds with TypeError: 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 like theme or seo to a bare {} instead of their fully-defaulted shape — now use .prefault(), which keeps Zod 3’s parse-the-default semantics. frontmatter.extend schemas still go through the Standard Schema contract, so any Zod version (or Valibot, or ArkType) works there unchanged.

blume@1.2.1

July 30, 2026

Release

Patch Changes

  • a015b0a: Match CJK and Thai content in the default Orama search provider. With i18n.defaultLocale set to a language written without spaces (Japanese, Chinese, Korean, Thai), the search index now uses a word-segmenting tokenizer built on Intl.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’s search_docs tool, 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.
blume@1.2.0

July 27, 2026

Release

Minor Changes

  • 217975c: Add a top-level integrations array to blume.config.ts for 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 to blume.config.ts rather 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 in evals.yaml using 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, --json emits the validate/audit-compatible machine report, --fix hands the failing report to the agent to edit the docs interactively, and blume eval init drafts 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’s path scopes its sidebar section and doubles as the link, so a section whose path isn’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 /changelog index, or a custom page under pages/ — lands on the section’s first entry instead of the page the reader expected. Setting href keeps 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 a deployment.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 SESSION KV and IMAGES bindings in the generated wrangler config. Without a configured session driver, @astrojs/cloudflare force-enables KV-backed sessions — making wrangler deploy demand a real KV namespace nothing reads — and defaults images to the runtime Cloudflare Images binding. Blume never reads Astro.session and every HTML route prerenders, so the generated Astro config now sets an inert in-memory session driver and imageService: "compile", which pre-optimizes images at build time with sharp.
  • 38475cb: blume init now adds node_modules/ to the generated .gitignore alongside 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’s group-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 own details.
  • 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 own details, 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 inside node_modules/blume, which Vite’s optimizer scan doesn’t crawl — so in dev it was served as raw ESM, where the UMD finds no exports/define, exposes no default, and strands its callable on window.epubGen. It now joins mermaid in optimizeDeps.include, naming the /bundle subpath 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.exclude frontmatter (and hidden pages, unless search.indexing.includeHiddenPages opts them in) no longer appear in local search results.
blume@1.1.4

July 23, 2026

Release

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/--codex launch 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.base before comparing canonical and sitemap URLs in blume audit. Canonicals and <loc>s are emitted as site + base + route while page URLs come from the base-less file tree, so on a subpath deployment every page false-fired CANONICAL_BAD_TARGET, NON_CANONICAL_IN_SITEMAP, and INDEXABLE_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 are encodeURI’d and URL#pathname re-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-fired SITEMAP_BAD_URL, and a percent-encoded href false-fired LINK_TO_BROKEN.
  • e4506a0: Probe live URLs under deployment.base in blume 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 of HTTP_4XX findings from probing the wrong URLs.
  • e4506a0: Strip query strings and fragments from redirect destinations before blume audit checks them against the build. A working redirect to /guide#setup or /search?q=x was reported REDIRECT_BROKEN because the suffixed path is not a file-tree member.
  • e4506a0: Apply basePath to configured redirects before blume audit resolves 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 a basePath site was reported REDIRECT_BROKEN, while LINK_TO_REDIRECT and REDIRECT_SOURCE_IS_PAGE could never fire.
  • e4506a0: Keep / separators in Sanity and Notion slugs. Slugging deleted slashes along with other punctuation, so a guides/setup slug was mashed into guidessetup — 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 dateFormat config into the runtime data and its BlumeDataConfig type. A configured dateFormat was silently dropped from the serialized site data, so the date stamps always rendered the default long style, and blume check failed with ts(2339) on data.config.dateFormat in 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 before VERCEL_URL (same for Netlify’s URL/DEPLOY_PRIME_URL/DEPLOY_URL), leaving deployment.site unset so canonicals, OG images, and the sitemap silently switched off for that deploy.
  • e4506a0: Leave the .blume/node_modules junction 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 missing node_modules and intermittently failed with “Cannot find package”.
  • e4506a0: Watch blume.config.ts, theme.css, and components.ts via their parent directory in blume 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.featured hrefs per locale, like header tabs. A pinned /changelog link 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 false BLUME_BROKEN_ANCHOR findings from blume validate.
  • e4506a0: Strip the locale directory from a shared .$ file’s nav path with the dir parser. fr/changelog.$.mdx kept its fr/ 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 ([![alt](/shot.png)](https://example.com)) navigates on click, so the zoom binding only flashed an overlay in the instant before navigation while the cursor-zoom-in affordance 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 [![alt](/img.png)](/target) was never validated at all (the nested image’s own target still is).
  • e4506a0: Make the MCP tools’ contract hold together: search_docs hits now include the route the tool description promises (alongside url), and get_page accepts a full URL or a base-prefixed path — an agent following “pass a route from search_docs” no longer gets “No page found” for a page that exists on a site with deployment.site or deployment.base configured.
  • ff0b2b0: Fix .md pages serving stale content in blume dev. The generated dev config kept Vite’s watcher out of Astro’s cache dir, which suppressed the data-store.json change 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 .mdx pages (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 to operations — 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 /guides route, 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 via set: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.
blume@1.1.3

July 22, 2026

Release

Patch Changes

  • 42522fc: Downlevel <Component> to its example’s source in agent-facing Markdown. The /<route>.md mirror, llms-full.txt, and the MCP get_page tool 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 missing path) is left verbatim, and a same-name ai.markdownComponents serializer still overrides the built-in.
  • a71f70e: Add a dateFormat config option for the “last updated” stamp and the changelog timeline. Both surfaces previously hardcoded dateStyle: "long"; they now share a configurable pass-through to Intl.DateTimeFormat options, 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 a timeZone is 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 /path title 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 scalar passthrough object to the openapi and asyncapi config 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 the scalar object win over Blume’s derived spec/theme config, making it a full escape hatch to Scalar’s API; the dedicated theme field remains the ergonomic shorthand.
  • fa07dc4: Create the .blume/node_modules dependency junction when an isolated linker (Bun’s isolated mode, pnpm) dedupes the workspace’s own astro dependency 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 on Cannot find module '@astrojs/mdx'.
blume@1.1.2

July 20, 2026

Release

Patch Changes

  • 6cf2995: Allow custom Shiki theme objects in markdown.codeBlocks.theme.light and markdown.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 a basePath with 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_modules junction 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 a blume other than the one running.
blume@1.1.1

July 19, 2026

Release

Patch Changes

  • b5ed87a: Add seo.og.titles to name the generated Open Graph card of a custom .astro page, keyed by route. A custom page has no frontmatter to read, so its card was titled by humanizing the last URL segment — turning /cli into “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 audit checks for. It’s carried as seo.description, so it feeds the meta/OG/Twitter description tags without adding a visible lede paragraph to the page.
  • 20ae16d: Fix blume check failing on generated files under a strict tsconfig: island and example wrappers now mirror the wrapped component’s props onto Astro.props so required props type-check through the spread, and the OG endpoint’s customRoutes array is explicitly typed so an empty list is no longer an implicit any[].
  • 9a6345f: Decouple Twoslash from the project’s hoisted TypeScript so sites can use TypeScript 7 (tsgo). The generated Astro config now wires in blumeTwoslashTransformer from blume/markdown, which compiles Twoslash fences with Blume’s own pinned classic TypeScript (passed explicitly as tsModule plus tsLibDirectory for the default lib files) instead of resolving whatever typescript the surrounding project installed — under TS7 that package’s main export is a version stub with no compiler API and no lib.*.d.ts files, so any twoslash fence crashed the build.
  • 32ced54: Evaluate {frontmatter.*} prop expressions when downleveling components for agent-facing output. Serializers — built-in and ai.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 audit flagging 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 title diverges from its folder’s explicit meta.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 as BLUME_NAV_INDEX_TITLE_MISMATCH from blume 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 overrides astro pin plus an incremental npm install — the exact steps the Astro-conflict warning recommends — hoists astro to the project root while @astrojs/mdx and Blume’s other deps stay nested under node_modules/blume/node_modules, so fresh checkouts and blume build --isolated failed with Cannot 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-strict to 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 as BLUME_DUPLICATE_SIDEBAR_ORDER from blume doctor/blume check and any other diagnostics consumer.
blume@1.1.0

July 19, 2026

Release

Minor Changes

  • 9fec53f: Add blume audit, an offline site audit that replaces a hosted SEO crawler.

    blume audit reads the built dist/ HTML, joins each page back to the .mdx it 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 #fragment anchors, draft: true pages that shipped, llms.txt held to the sitemap’s standard, and Open Graph images verified as bytes.

    Flags:

    • --url <origin> also probes a live deployment for what dist/ can’t show (bad rewrites, missing compression, X-Robots-Tag deindexing).
    • --external probes outbound links.
    • --claude / --codex write the JSON report and open the agent interactively to fix each finding at its source.
    • --fail-on <severity> (default error) as the CI gate, plus --only/--skip, --json, --verbose, and --list-checks.

    blume validate is 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 / (CodeQL js/polynomial-redos); the new trim is O(n) and yields the same single-trailing-slash directory URL.

  • f779fd5: Make blume audit --claude/--codex work on Windows: npm installs the agent CLIs as .cmd shims 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.base throughout 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/x from 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 \u0000 escape, 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/docs was 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 like Disallow: /page/ are matched against the sitemap <loc> as served instead of a slash-stripped copy

  • d99726a: Only report ROBOTS_META_UNEXPECTED when the robots meta actually blocks indexing — a page declaring index, follow is 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 /changelog stayed English. /changelog is 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 inline and param props to the Tabs component. inline renders borderless — a tab strip on a full-width rule with the content flowing beneath as prose — instead of the bordered box. param syncs the active tab to a URL query param instead of the hash; because each group owns its own param, several Tabs can share a page and every selection is deep-linkable (a link ending in ?install=windows opens on that tab). Existing boxed, hash-synced Tabs and CodeGroup are unchanged.

  • d9dfdba: Stop shipping underscore-prefixed .astro files in pages/ as routes. Blume injects user pages itself and globbed every .astro file, so private partials — shared layouts and home-page sections like pages/_home/Hero.astro or pages/_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 dev under pnpm’s default isolated linker. The generated runtime now checks Astro through the same physical node_modules ancestor lookup used by its ESM config, instead of mistaking pnpm’s CommonJS-only NODE_PATH exposure for a resolvable astro/config import.

  • 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) in blume.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’s custom field, and every other key keeps the strict typo-catching behavior.

  • 368b258: Emit a _headers file for static builds so hosts serve the raw AI-ready endpoints with an explicit charset=utf-8. Blume’s /<route>.md, /<route>.mdx, and .txt outputs (llms.txt, llms-full.txt) are valid UTF-8, but common static hosts serve them as text/markdown / text/plain with 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 _headers pins the same charset=utf-8 Content-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 any deployment.base/basePath stack, and a _headers you ship in public/ is left untouched — exactly like _redirects.

  • d99726a: Prefix the _headers .txt charset rule with only deployment.basellms.txt/llms-full.txt are served at the deploy root, so a basePath deployment shipped a /docs/*.txt rule 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 from blume/og) now returns a Uint8Array rather than a Buffer.

  • 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 reading llms.txt had no pointer to fresh blog posts or changelog entries. The index now closes with an ## RSS Feeds section listing each configured feed that has pages, under the same condition the feeds themselves exist — RSS enabled and an absolute deployment.site — and carrying any deployment.base subpath. This mirrors the artifacts.feeds list already emitted in agent-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. canonical into an existing seo:) 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-level mcp config

  • 8769dd1: Add seo.og.fonts to 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’s googleFonts helper, registering only the glyph subsets each title uses. Latin text renders unchanged.

  • d99726a: Add seo.og.fonts to the OgConfig authoring type — the schema accepted it but TypeScript rejected it in blume.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 googleFonts OG-font loader is imported from takumi-js/helpers, but only the bare takumi-js was externalized for the static-prerender Vite environment — which matches by exact specifier, so the subpath (and the native @takumi-rs/core backend it pulls in) got bundled into the prerender chunk, relocating the .node binding lookup. Externalize takumi-js/helpers and the @takumi-rs/* packages so the native backend always resolves from node_modules at 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 (Authorization header, 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, and security: [] 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 ignored security entirely, 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-bullets now 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.base before composing it into redirect targets — a trailing-slash (/base/) or bare (base) value produced /base//new or relative destinations in Astro redirects and the platform redirect files

  • 93ea41b: Fix redirects escaping the site under deployment.base. A redirect’s to was only ever rewritten with basePath, never with the deployment base, so with base: "/docs" a to: "/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 applies base when it builds the match pattern for from, 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 prepends base, so Blume now applies it to to itself. The two bases also compose correctly: deployment.base and basePath set 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, but from was 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 into to by hand is preserved rather than doubled.

  • d99726a: Warn when a search.popular link 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.popular to 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. Each href is authored root-relative and picks up basePath automatically (external URLs pass through); icon takes 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.astro renders its children into a permanently hidden node (used only to build the copy-to-clipboard and Cursor-deeplink text), but extractHeadings had 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-width token (default 42rem) is exposed as a max-w-content utility through Tailwind’s --container-content theme key, and the article, breadcrumb, mobile table of contents, page feedback, pagination, and last-updated line now use it instead of hardcoding 42rem. Override --blume-content-width to 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" and adapter: "vercel", the render function (.vercel/output/functions/_render.func) shipped as entry.mjs alone, so any server-rendered request — the Docs MCP endpoint, Ask AI — 500’d at runtime with ERR_MODULE_NOT_FOUND. The adapter resolves both its Build Output tree and its @vercel/nft dependency trace against Astro’s root, which Blume points at the hidden .blume runtime; the trace’s base then excluded the server bundle (which lives under outDir, 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 and node_modules and the output lands at the project root natively. Projects inside a workspace were unaffected — nft’s base search climbed past .blume to the workspace root — so this only ever broke standalone projects.

blume@1.0.4

July 15, 2026

Release

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 is not-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-language so 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 a title never gets data-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.tabs entry 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-migrate agent 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 to blume.config.ts, restructuring content into filesystem-derived navigation (with redirects for every moved route), rewriting callouts to ::: directives, converting icons to Lucide, inlining snippets, and pointing generated API references at openapi.sources instead 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 under skills/.

  • a421a1e: Make the generated runtime type-check cleanly under blume check --strict --isolated (and any project whose tsconfig includes the generated files). Previously a valid site could fail with dozens of errors in the generated .blume-verify files:

    • The raw-Markdown, RSS, and OG endpoints imported data.json directly, so TypeScript widened the JSON (navigation kind to string, empty arrays to never[], theme mode to string) and rejected it against Blume’s own types. They now import the typed blume:data virtual module.
    • Endpoint handlers (GET, getStaticPaths callbacks) and the changelog/content-page helper functions had implicit-any parameters; they now carry explicit types.
    • The OG endpoint’s PNG Buffer is wrapped in a Uint8Array so it satisfies the Response body type, and the empty component-overrides module and the blume:examples/blume:examples-theme virtual modules now declare types.

    Also stops blume check --isolated from reloading a running blume dev server: .blume-verify is now in Blume’s ignored-directory set, so generating the isolated runtime no longer trips the content watcher.

  • 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 no default export and mermaid threw does 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’s import("mermaid") lives inside node_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 nested blume > mermaid form (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/cloudflare default prerender runtime from Node to workerd, which broke Blume prerender on Cloudflare (No such module "node:path" and node:fs usage in Blume’s build-time content tooling). The generated astro.config.mjs now passes prerenderEnvironment: "node" to the Cloudflare adapter. On-demand pages still run in workerd at request time.

  • d11a90c: Stop the Prompt component’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-blume radius token instead of a hardcoded rounded-full on the PageFeedback and Pagination buttons so they respect the configured theme.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 path still 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. /examples with only /examples/hello-world links 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.

blume@1.0.3

July 13, 2026

Release

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’s ask slot, 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; pass askEnabled={false} to opt a page out.
  • 66b721b: Move the MCP server config under ai in blume.config.ts, alongside the other agent-facing features. Rename mcp: { … } to ai: { mcp: { … } } — the shape of the block is unchanged.
  • c8ae77e: Fix ERR_MODULE_NOT_FOUND in a deployed server function. Surfacing an adapter’s deploy bundle out of .blume resolved 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.
blume@1.0.2

July 13, 2026

Release

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 with JSON.stringify. JSON escaping isn’t a code-context escape — </script> and U+2028/U+2029 pass straight through it — so a crafted banner.id or theme value could break out of the script. Both scripts are now constants and take their values from data-* attributes on their own <script> tag, which Astro HTML-escapes. ReferenceLayout had drifted to its own inline copies of both scripts; it now shares the same module as RootLayout and PageLayout.

    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 in core/trim.ts; behavior is unchanged.

blume@1.0.1

July 13, 2026

Release

Patch Changes

  • 6afb56a: Declare the blume bin as bin/blume.mjs instead 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.
blume@1.0.0

July 13, 2026

Release

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-BR with the conventional lowercase pt-br/ folder (or an intro.pt-br.mdx suffix under the dot parser) 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.title and changelog.description UI 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 /changelog page 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 set ui.changelog.description are unaffected.

  • e922ff5: The syntax docs now label callouts and package-install blocks as MDX-only, matching the existing notes on diagrams and math. Previously a reader following the page could write :::note or a package-install fence in a .md file and get literal text or a plain code fence with no hint why.

  • e922ff5: Apply the js-yaml 4-safe YAML engine to matter.read as well. The front-matter wrapper previously exposed gray-matter’s own read helper unwrapped, so reading a file through it would parse with the removed safeLoad default and crash with “Function yaml.safeLoad is removed in js-yaml 4” — the exact failure the wrapper exists to prevent for matter() and matter.stringify().

  • 68520af: Exclude Vite’s pre-bundled dep cache (node_modules/.vite/) from @vitejs/plugin-react. Astro’s react() replaces the plugin’s default node_modules exclude, 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 same withBase treatment; external URLs, data URIs, and relative paths are untouched.

  • e922ff5: @astrojs/vue and @astrojs/svelte are now declared as optional peer dependencies, matching @astrojs/netlify and @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 --isolated now reports the build’s actual output directory on success. A server output 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 new nav.breadcrumb and page.pagination UI 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.markdownComponents crashing config validation in projects that resolve Zod 4. The schema used the single-argument z.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_page tool’s description — shipped user-facing in tools/list and 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 when deployment.site is set, or a page overrides seo.canonical).
    • og:typearticle on blog posts and changelog entries, website everywhere else. Article pages also emit article:published_time and article:modified_time.
    • og:site_name — the site title.
    • og:image:width, og:image:height, og:image:type, and og:image:alt on Blume’s generated OG card, so a crawler can lay it out without fetching the image. An seo.image you 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 in seo.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.x for account attribution:

    seo: {
      x: { handle: "@acme", creator: "@jane" },
    }

    handle becomes twitter:site and creator becomes twitter:creator — the one piece of X card metadata with no Open Graph equivalent to fall back to. A page can credit its own author with seo.x.creator frontmatter, which is what a guest post wants. The @ is optional in both places.

    Every page also now emits twitter:card, twitter:title, twitter:description, and twitter:image:alt on the generated card. twitter:card previously rendered only when a page had an image; a page without one now gets the compact summary card instead of sharing as a bare link.

  • e922ff5: The Scalar API reference shell now sets <html lang> and dir from 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 hardcoded lang="en" with no dir — 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.all UI 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 an InvalidStateError on older engines. The / shortcut stays open-only, and open() itself now guards against an already-open dialog.

  • e922ff5: Reject a toc config whose minHeadingLevel exceeds its maxHeadingLevel. 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 like C:\...\.blume\... printed the full machine path instead of the project-relative .blume\... form tagged (generated).