# blume

## 1.5.3

### 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.

## 1.5.2

### Patch Changes

- 7650327: Add `ai.ask.retrieval` — `maxResults`, `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.svg` → `icon-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.

## 1.5.1

### 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).

## 1.5.0

### 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.

## 1.4.3

### 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ñol` → `españ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.

## 1.4.2

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

## 1.4.1

### 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.

## 1.4.0

### 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 `type`s (`["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.

## 1.3.1

### 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.

## 1.3.0

### 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.

## 1.2.1

### 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.

## 1.2.0

### 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.

## 1.1.4

### 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 "&gt; 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.

## 1.1.3

### 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](https://github.com/scalar/scalar/blob/main/documentation/configuration.md) 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'`.

## 1.1.2

### 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.

## 1.1.1

### 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.

## 1.1.0

### 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.base` — `llms.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.

## 1.0.4

### 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.

## 1.0.3

### 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.

## 1.0.2

### 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.

## 1.0.1

### 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.

## 1.0.0

### 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:type` — `article` 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:

  ```ts
  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)`.

## 0.8.0

### Minor Changes

- 89b5685: The `.md` raw-Markdown mirror now downlevels components to plain Markdown for agent consumers: `<TypeTable>` becomes a GFM table, `<Callout>` a labeled blockquote, `<Steps>` an ordered list, `<Tabs>` bold-labeled sections, and `<YouTube>` a link. The `.mdx` mirror keeps serving the source exactly as written, so both audiences get what the extension implies. The same conversion applies to `llms-full.txt` and the MCP server's `get_page` tool. Unknown components, props that can't be recovered statically, and component markup inside fenced code blocks are all left verbatim.

  Custom components can join in via `ai.markdownComponents` in `blume.config.ts`: a map of JSX name to `ComponentMarkdown` serializer, receiving the statically-evaluated props and downleveled children. A same-name entry replaces a built-in serializer.

- 1fd8368: `blume init` is now interactive: in a terminal it asks where to create the project (also available as `blume init <dir>`), what your docs site is called, which template to use, and which content sources you need (filesystem, GitHub Releases, Notion, Sanity, remote MDX), then scaffolds a matching `blume.config.ts` — including a `content.sources` block with placeholder values, env-var hints, and the Notion/Sanity SDK dependencies when those sources are picked. Explicit flags pre-answer their prompts; `--yes`, CI, or piped stdio keep the previous non-interactive behavior with identical default output. The package manager for next-steps hints is auto-detected from `npm_config_user_agent`, and a non-default `--content-dir` now also emits `content.root` so the scaffolded project reads the right folder.
- 8161706: `llms.txt` now mirrors your navigation tree instead of emitting one flat list: sidebar folders and groups become Markdown headings (nested groups nest the heading level), each locale gets its own labeled section under i18n, and pages an explicit sidebar omits are appended under "Other". `ai.llmsTxt` also accepts an object form — `{ enabled, openapi }` — where `openapi: false` keeps generated API reference pages (OpenAPI/AsyncAPI) out of both `llms.txt` and `llms-full.txt`, for sites whose reference documents a placeholder or example spec. The bare boolean shorthand keeps working.

### Patch Changes

- 21aa451: API overview operation cards now link to the operation pages' routes under the site-wide `basePath`, matching where the content pipeline actually mounts them, so the cards no longer 404 when a `basePath` is configured.
- 759b148: API overview operation links now route through `withBase`, so they resolve under `deployment.base` like the sidebar and header links to the same pages.
- 759b148: Ask AI citation links now resolve under `deployment.base`: the rendered answer prefixes root-relative routes with the base, so citations point at the pages' real served URLs.
- 559024b: Ask AI grounding excerpts now always contain the matched query term: when the remaining context budget shrinks the excerpt window below the 160-character lead-in, the lead-in is capped so the window no longer ends before the match and injects an irrelevant snippet.
- 559024b: The generated Ask AI endpoint now rejects a missing API key (or missing AI Gateway credential) up front with a real 500 and a pointer to the env var to set, and logs provider errors server-side via streamText's `onError` callback. Previously `streamText` deferred auth failures to stream consumption, so the endpoint returned a 200 whose stream aborted mid-flight, the UI showed a generic error, and nothing was logged.
- 759b148: The closed Ask AI panel is now `inert`, so its buttons and textarea no longer sit in the keyboard tab order on every page, and closing the panel returns focus to the element that opened it.
- 7cda9bf: Opening Ask AI on a "bare" layout page (the changelog index) forced the docs sidebar grid tracks onto its single-column grid, squeezing the whole page into the 17.5rem sidebar track on desktop. The column override now only applies to grids that actually have a TOC column, so bare pages just shrink to make room for the panel like every other layout.
- 559024b: `<AutoTypeTable>` now reads optionality from the checker's symbol instead of the declaration's question token, so mapped and utility types document correctly: `Partial<Base>` properties render as optional and `Required<Base>` properties as required.
- 759b148: `<Badge color>` with a 3- or 4-digit hex (e.g. `#f00`) now renders a valid background: the alpha is applied with `color-mix` instead of appending a hex alpha byte.
- 7cda9bf: The announcement banner rendered by `PageLayout` and `ReferenceLayout` fell back to the English "Dismiss announcement" label even on localized sites — only `RootLayout` passed the UI dictionary through. Both layouts now forward the localized banner strings (`ReferenceLayout` gained the same optional `ui` prop the other layouts have, and the generated Scalar reference page passes the resolved dictionary through).
- 21aa451: BreadcrumbList JSON-LD now passes Google's Rich Results validation on nested pages. Sidebar groups without an index page used to be emitted as link-less ListItems, which Google rejects on every position except the last; those crumbs are now skipped (with positions renumbered), and the list is omitted when fewer than two linked crumbs remain.
- 7cda9bf: Corrected a stale comment in `scripts/bundle-docs.mjs`: the script runs on the repo root's `prepare` and the package's `prepack` — the package itself defines no `prepare` script.
- 7cda9bf: A `deployment.site` with a trailing slash (`https://docs.example.com/`) produced double-slash canonical and `og:image` URLs on pages rendered through `PageLayout` (custom pages, the default 404). The trailing slash is now stripped before joining, matching how content pages build theirs.
- 759b148: The changelog index's canonical URL now carries `deployment.base` (`site + base + /changelog`), matching how content pages canonicalize.
- 7cda9bf: Changelog timeline heading links now resolve under `deployment.base`: `<Update>` routes its heading href through `withBase` at emit time, like every other link emitter, so with a base of `/docs` the headings point at `/docs/changelog/vX` instead of 404ing at `/changelog/vX`. In-page anchor fallbacks (`#id`) and external URLs pass through untouched.
- 7cda9bf: The generated changelog index now passes the resolved UI dictionary and the default locale's lang/dir to the layout, and the generated 404 page now passes lang/dir alongside the dictionary it already used. Previously every chrome string on `/changelog` reverted to English on a non-English default locale, and both pages rendered `dir="ltr"` under an RTL default locale.
- 7cda9bf: Changelog entries whose labels slug identically (e.g. repeated titles, or several entries with neither a title nor a version) no longer render duplicate element ids: later duplicates are suffixed `-2`, `-3`, ... at build time, so each heading and TOC item deep-links to its own entry instead of the first match. The first occurrence keeps the plain slug, so existing anchors stay stable.
- 759b148: Closed mobile nav drawers are now `inert` and `aria-hidden`, so their links drop out of the keyboard tab order while closed; the docs sidebar stays fully interactive at desktop widths where it becomes a static column.
- 6d412dc: Rebase `<Card href>`, `<Tile href>`, `<Tooltip href>`, and `<Card img>` under the served URL. Markdown links are rewritten to the composed `deployment.base` + `basePath` at build, but these component props were emitted raw — so on a GitHub Pages project site (or any `deployment.base`/`basePath` deploy), `<Card href="/quickstart">` linked to a base-less path and 404'd while the adjacent `[x](/quickstart)` worked. Component hrefs now follow the same "write links as if mounted at root" contract as markdown links (idempotent per layer, inert for external URLs, fragments, relative paths, and asset links), `<Card img>` gains the deployment base like every other `public/` asset emitter, and `<Update>` — which previously applied only the deployment base — composes `basePath` too. The site-wide `basePath` is now exposed on `blume:data` as `config.basePath` for custom pages that need the same treatment.
- 759b148: Content links that hand-write the site-wide `basePath` (`[x](/docs/guide)`) are no longer double-prefixed when `deployment.base` is also set: the markdown link rewriter now layers the two bases separately, so the link resolves to `/base/docs/guide` instead of `/base/docs/docs/guide`.
- 21aa451: Example generation now honors `const` schemas (the OpenAPI 3.1 discriminator idiom), so `{ type: "string", const: "dog" }` renders `"dog"` in request/response examples instead of the `"string"` placeholder.
- 759b148: "Copy as Markdown" now checks the fetch response before writing to the clipboard, so a failed request no longer copies an error page and flashes "Copied!".
- 537d768: A custom `llms.txt` or `llms-full.txt` in your `public/` folder now replaces the generated file, matching the existing sitemap.xml and robots.txt override convention.
- 7cda9bf: A bare `--host` on `blume dev` and `blume preview` now binds all network interfaces, matching Astro's own flag semantics. Previously the valueless flag parsed as an empty string, which Vite treated as a literal hostname and printed malformed URLs like `http://:4321/`. An explicit `--host 10.0.0.1` still binds that address.
- 21aa451: The refusal printed when a command would corrupt a running `blume dev` server's `.blume` runtime now only suggests re-running with `--isolated` for commands that actually support the flag (`build` and `check`). `blume eject` now gets accurate advice instead of a flag it silently ignores.
- 21aa451: Content and config errors introduced while `blume dev` is running are now printed to the terminal as well as shown in the browser error overlay. On published installs the overlay channel could be unavailable, so an edit that broke frontmatter or content previously produced no message anywhere.
- 759b148: Fix `blume dev` staying permanently down after a failed structural restart: the route signature is now committed only once the restart succeeds, so the next file change retries the restart instead of taking the hot-reload path against a stopped server.
- 7cda9bf: The deployment docs claimed every adapter is pulled in automatically; they now say `vercel` and `node` ship with Blume while `@astrojs/netlify` and `@astrojs/cloudflare` must be installed in the project.
- 559024b: The AI docs now list Codex among the clients the **Connect to MCP** menu offers copy-and-go install for, matching the actual menu, and use American spelling ("labeled") consistently.
- 7cda9bf: The deployment docs' env-var table understated the Ask AI warning — it fires for every non-gateway provider's default key env var (`OPENROUTER_API_KEY`, `LLMGATEWAY_API_KEY`, `INKEEP_API_KEY`) unless `apiKeyEnv` overrides it — and the sources page's `blume sync --force` comment is now aligned with the lines above it.
- 559024b: The CLI reference now quotes the actual error `blume build` prints when a `blume dev` server holds the `.blume` runtime — the previous wording was stale and no longer matched the message.
- 559024b: The configuration overview now names the edit page action by its actual UI label (**Edit on GitHub**) and no longer implies `search` supports only two providers — Algolia and the other providers get a mention.
- 6d412dc: Add the `Feedback` layout slot to the customization docs' wired-slots table. The slot is real and wired (it replaces the "Was this page helpful?" rating, and `blume add feedback` scaffolds an override for it) but the table — which presents itself as the complete list — omitted it.
- 7cda9bf: The navigation docs' "built-in icon" link pointed at the Customization page, which says nothing about icons; it now links to the Icon section of the components page.
- 7cda9bf: The docs landing page's customization sentence read awkwardly ("start replacing … file or even ejecting"); it now reads "start by replacing the built-in components, modifying the single configuration file, or even ejecting".
- 559024b: The docs homepage's "Why Blume exists" opener now reads "fast, AI-ready, and zero-config", fixing the faulty parallelism in the previous phrasing.
- 759b148: Documentation fixes:

  - Restore collapsed `:::warning` / `:::note` callout directives to their multi-line form in the syntax guide, the FAQ, and the Blume agent skill
  - FAQ: soften the oxfmt patch claim to "ships the same fix", include `:::info` in the list of affected container directives, and update the example patch to the shipped `oxfmt@0.58.0` version (which now also preserves titled directives like `:::warning[Heads up]`)
  - AI guide: describe the MCP tool list without a hardcoded count, correct the `agent-readability.json` default-state wording, and drop the reference to the deferred migration skill
  - i18n guide: describe UI translation packs as "over 30 languages" instead of a hardcoded count
  - Quickstart and Deployment: require Node.js 22.12 or newer, matching the package engines
  - Configuration reference: correct the `markdown` feature description (code blocks, heading anchors, image zoom), split the `toc` example into two valid snippets, and document the `github` option (owner, repo, branch, dir)
  - Custom pages: update the `BlumeDataConfig` field list to include `ask`, `codeThemes`, and `toc`
  - Skills guide: use the verified `npx skills add haydenbleasel/blume --skill blume-update-docs` install command

- 559024b: The navigation docs' page-actions list no longer mentions the **Ask AI about this page** action removed in 0.6.0, and the edit action is named by its actual UI label, **Edit on GitHub**.
- 559024b: The SEO docs no longer hardcode a count of configurable features (the list has grown past three) and use American spelling ("honor") consistently.
- 6d412dc: Fix `blume eject`'s next-steps hint telling bun users to run `bun build` — that invokes Bun's bundler ("error: Missing entrypoints"), not the package.json `build` script, because unlike `dev` the script name is shadowed by a builtin subcommand. The hint now prints `bun run build` (npm already used the `run` form; pnpm/yarn are unaffected).
- 759b148: `blume eject` now emits the hosted MCP server (endpoint, data snapshot, and `.well-known` discovery documents) and the `/changelog` index page, so ejected apps no longer 404 on routes the generated runtime served.
- 559024b: `blume eject`'s success message now prints run commands matching your package manager (detected the same way as `blume init`) instead of always suggesting `bun run dev` and `bun run build`.
- 7cda9bf: `blume eject` discarded the Scalar API reference warnings that `blume dev` and `blume build` print — a spec file that wasn't found (so the ejected page points Scalar at a URL that 404s) or a reference route colliding with a content page went unreported. Eject now surfaces those warnings the same way the generated runtime does.
- 559024b: `blume eject` now resolves the installed blume package's real location for the `@source` glob in `src/generated/app.css` when it isn't in the project's own `node_modules` (hoisted npm/yarn workspace installs), so Blume's utility classes no longer silently disappear. If resolution fails, the previous default is kept and a warning explains how to adjust the glob.
- 7cda9bf: `blume eject` rewrote the build script to plain `astro build` without mentioning that the `blume build` post-build artifacts stop being produced — most severely, a `search.provider: "pagefind"` site ejected into a build whose search fails at runtime because the Pagefind index is never created. The eject confirmation and summary now warn exactly which artifacts the project's config actually uses (the Pagefind index with a `pagefind --site dist` post-build hint, hosted search sync, llms.txt/llms-full.txt, sitemap.xml, robots.txt, agent-readability.json, and platform redirect files), and the Eject docs explain how to recreate each one.
- 759b148: Reword the `BLUME_ENTRY_ID_MISMATCH` suggestion to the configurations that actually resolve it: a single filesystem source, or every filesystem source rooted at `content.root` and partitioned with `include` globs — the previous advice (any root under `content.root`) reproduced the error.
- 759b148: Fix `.env` parsing corrupting escaped backslashes in double-quoted values (e.g. `"C:\\path\\new"` gained a newline): escape sequences are now expanded in a single pass so each backslash is consumed exactly once.
- 21aa451: Clicking "Export to EPUB" again while a generation was in flight could leave the menu item permanently reading "Generating…" until reload — the second click captured the in-progress label as the text to restore. The original label is now remembered once, so the button always returns to its proper label.
- 21aa451: Mixedbread search results are now HTML-escaped before being rendered in the search dialog, so markup in a hit's title or excerpt displays literally instead of being injected into the page. Query matches in those hits also get the same highlight treatment as every other provider.
- 21aa451: Search result excerpts no longer end with a stray ellipsis when a page has no description and its content is short enough to show in full — previously short content rendered as "some short text…" and empty content as a bare "…".
- 759b148: Multiple `<Expandable>` (or same-titled accordion) components on one page no longer render duplicate ids; later duplicates get a numeric suffix while the first keeps the plain slug so hash deep-links stay stable.
- 6d412dc: Fix the FAQ's oxfmt patch instructions for pnpm. The page told Bun and pnpm users alike to add a top-level `patchedDependencies` key to `package.json` — Bun's convention, which pnpm silently ignores, leaving the patch unapplied. The pnpm form (nested under the `pnpm` key, or in `pnpm-workspace.yaml`) is now shown separately.
- 6d412dc: Restore the "about this page" qualifier in the French `actions.askAI` string ("Demander à l'IA à propos de cette page"). French was the only locale pack that dropped it, making the page-action label identical to the Ask panel's title.
- 6d412dc: Document three functional frontmatter fields the schema reference omitted: the top-level `hidden` and `noindex` shorthands (for `sidebar.hidden` / `seo.noindex`) and `deprecated`, which renders a "Deprecated" badge in the sidebar. The page claims to list everything a page accepts, and unknown keys are build errors — so working fields users can't discover from the docs are a gap.
- 7cda9bf: Git-derived "Last updated" dates now follow each filesystem source's own root: with `lastModified: true` and a source configured with a non-default `root` (e.g. `documentation/`), the `git log` pathspec previously pointed at the global `content.root` (`docs/`), so every page silently lost its date.
- 21aa451: A header logo configured as a public-dir path (e.g. `logo: "/logo.png"`) now renders correctly on sites deployed under `deployment.base` — the light and dark logo images get the base prefix like the favicon and brand link already did, instead of 404ing on every page. Remote http(s) logo URLs pass through unchanged.
- 21aa451: The Algolia, Typesense, and Orama Cloud search providers now honor the search dialog's language filter on i18n sites. The locale was uploaded with every record at sync time but ignored at query time, so results always mixed every language regardless of the per-language toggle.
- 759b148: Pressing Enter to confirm an IME conversion (CJK input) no longer submits the Ask AI question or activates the selected search result mid-composition.
- 21aa451: `blume init --eject` now rewrites the scaffolded package.json scripts to run Astro directly after a successful eject (matching `blume eject`), and its next-steps output includes the `cd <dir>` hint and package-manager-appropriate commands. When eject can't run yet because dependencies aren't installed, the fallback explains that and points at `npx blume eject --yes` (or the pnpm/yarn/bun equivalent) instead of a bare `blume eject` that isn't on PATH.
- 559024b: Inline code with an unknown highlight language (for example a typo'd `` `foo(){:typescrpt}` ``) now strips the `{:lang}` marker and renders as plain inline code instead of shipping the literal marker in the page.
- 639c802: The Search input and Ask AI textarea now render at 16px on touch devices (`pointer-coarse:text-base`), so iOS Safari no longer auto-zooms the page when they receive focus; fine-pointer devices keep the 14px size.
- 7cda9bf: `blume build --isolated` now honors `--analyze`, `--budget-js`, and `--budget-css`, measuring the isolated build's own output. Previously the isolated path skipped the bundle report and budget gate entirely, so a CI run like `blume build --isolated --budget-js 100` exited 0 without measuring anything — a silent false pass.
- 21aa451: Broken-link and broken-anchor diagnostics now point at the correct line of the source file. Line numbers were counted on the frontmatter-stripped body, so a link below a frontmatter block was reported several lines above where it actually sits (e.g. line 2 instead of line 6 after a 4-line block).
- 559024b: llms.txt, llms-full.txt, and agent-readability.json now layer `deployment.base` onto their root-relative URLs when no `site` is configured, so advertised links like `/llms.txt` and page routes resolve under a subpath deployment instead of 404ing.
- 759b148: Exclude hidden and `noindex` pages from `llms.txt`/`llms-full.txt` (matching the sitemap) and percent-encode the emitted page URLs.
- 21aa451: The announcement banner's dismiss button had a hardcoded English "Dismiss announcement" accessibility label. It now resolves through the UI strings dictionary (`banner.dismiss`), ships translations in every built-in locale pack, and can be overridden per locale via `i18n.ui`.
- 759b148: Previously hardcoded UI chrome strings (search dialog groups/hints/error, export menu, navigation and theme-toggle aria-labels, nav-tree back/deprecated labels, changelog reveal button, Mermaid error, Ask AI transcript prefixes) now come from the i18n dictionary with English fallback.
- 7cda9bf: Localize header-tab dropdown item paths under i18n: a tab's own path was locale-prefixed (`/docs` -> `/fr/docs`) but its `items[].path` entries were not, so dropdown links always pointed at the default locale. External item URLs still pass through untouched, and selector items keep their intentionally locale-specific targets.
- 559024b: The `<Math>` component is now wired into the generated runtime whenever math can actually appear: block math (`$$…$$`) in plain `.md` files, math in staged remote-source content, and explicitly authored `<Math code="…" />` tags all count. Detection previously only scanned local `.mdx` files for a literal `$$`, so those cases rendered a raw "Expected component Math to be defined" MDX error instead of the equation.
- 21aa451: The MCP `search_docs` fallback excerpt (used when a page has no description) now appends an ellipsis only when the content was actually truncated. Short pages no longer get a fake truncation marker, an empty page no longer yields a bare "…", and a cut that lands on whitespace is trimmed before the marker.
- 7cda9bf: Normalize `mcp.route` to a leading slash (and no trailing slash) like other configured routes: a slash-less value such as `"docs-mcp"` was string-concatenated onto the site origin, so `/.well-known/mcp.json`, the MCP server card, and `agent-readability.json` advertised a malformed URL like `https://acme.comdocs-mcp`.
- 759b148: MCP URLs now carry `deployment.base`: the server URL advertised by `/.well-known/mcp.json` and the server card, and the page URLs returned by the `search_docs` and `list_pages` tools, previously pointed at base-less paths on subdirectory deployments.
- 759b148: An mdx-remote source missing both `{ github }` and `{ url, files }` now always fails with `BLUME_SOURCE_MISCONFIGURED`: the config is validated before the cached-fetch path, which previously masked it as `BLUME_SOURCE_FETCH_FAILED` or silently served stale cached entries with an offline warning.
- 559024b: `blume build` with the Netlify adapter now surfaces only the `.netlify/v1` deploy bundle instead of replacing the whole `.netlify` directory, so the `.netlify/state.json` written by `netlify link` survives every build.
- 7cda9bf: Server builds targeting Netlify or Cloudflare now warn up front when `@astrojs/netlify` or `@astrojs/cloudflare` isn't installed, naming the exact package to add. Previously the generated `astro.config.mjs` imported the adapter unconditionally — including when it was auto-selected from platform env vars — so the build died with an opaque `ERR_MODULE_NOT_FOUND` from a hidden generated file. Both adapters are now declared as optional peer dependencies so package managers surface and satisfy the requirement.
- 6d412dc: Fix Node server builds (`output: "server"`, `adapter: "node"`) publishing every deploy artifact to a directory the server never serves. Astro puts served static files in `dist/client/` and `@astrojs/node`'s standalone server reads only that directory, but Blume wrote `sitemap.xml`, `robots.txt`, `llms.txt`/`llms-full.txt`, `agent-readability.json`, redirect files, and the Pagefind bundle into `dist/` — logging success while every one of those URLs 404'd in production. The same wrong directory also fed `--analyze` and the `--budget-js`/`--budget-css` gate, which read the nonexistent `dist/_astro`, reported "No client JavaScript emitted", and passed every budget against 0 bytes. Both the deploy static dir and the isolated-build static dir now point at `dist/client/` for Node server builds; Netlify (publishes `dist/`), Cloudflare (serves the `outDir` root), and Vercel (`.vercel/output/static`, already special-cased) are unchanged.
- 759b148: The generated 404 page's "home" link now routes through `withBase` instead of a hardcoded `/`, so it lands on the site root under `deployment.base`.
- 759b148: The Notion source's `publishedValue` now defaults to `Published` as documented: pages whose Status/select property holds any other value import as drafts, while pages without a Status property stay published.
- 759b148: Treat numeric-prefixed index files (`01-index.mdx`) as directory indexes everywhere routing already does: they now sort first in the sidebar, keep their group's route path intact, and resolve relative links against their own route.
- 559024b: An OpenAPI spec file that isn't a valid document (an empty file, or YAML that parses to a scalar or list) now fails with a clear "is not a valid OpenAPI document" error and a fix-the-file suggestion, instead of crashing with a raw TypeError and a misleading reachability hint.
- 559024b: Two Blume-rendered OpenAPI sources that resolve to the same route now emit a warning ("keeping the first"), matching the Scalar renderer, instead of silently dropping the second spec's pages.
- 559024b: An OpenAPI spec that parses but declares no operations (say, a config file pointed at by mistake) now emits a build warning naming the spec, instead of silently shipping an empty API reference tab.
- 21aa451: Operation-level parameters now override path-level parameters with the same name and location, per the OpenAPI spec, so a re-declared parameter renders once in the parameters table instead of twice — and no longer duplicates itself in sample request URLs (`?limit=0&limit=0`).
- 21aa451: `blume build --output server` now participates in platform adapter auto-detection. On Vercel, Netlify, or Cloudflare Pages, the matching adapter is selected just as if `deployment: { output: "server" }` had been set in blume.config.ts — previously the flag produced a server build with no adapter and the Astro build failed.
- 6d412dc: Render the yarn tab of a `package-install` `ci` command as `yarn install --immutable`. It previously emitted `--frozen-lockfile`, which Yarn 4 removed — so there was no yarn version where both the generated `ci` command and the Berry-only `yarn dlx` (from `exec`) worked; a Yarn 4 reader copying the tab got "Unsupported option name --frozen-lockfile".
- 759b148: Page actions now reach the raw-markdown endpoint under `deployment.base`: the "Open in ChatGPT/Claude/…" links and "Copy as Markdown" previously fetched the base-less `/page.md` path (a 404) on subdirectory deployments.
- 759b148: The search dialog's ⌘J preview hint and the Ask AI panel's ⌘I hint now show "Ctrl" on non-Apple platforms, matching the existing ⌘K/Ctrl K button hint.
- 759b148: Include CHANGELOG.md in the published package so it's browsable on npm and unpkg.
- 559024b: The README's CONTRIBUTING link now uses an absolute GitHub URL so it no longer 404s on the npm package page, and the opening tagline no longer repeats the "drop Markdown into a folder" pitch that the very next paragraph makes in full.
- 7cda9bf: The README dropped a verb in "run `blume eject` to a standalone Astro app"; it now reads "run `blume eject` to get a standalone Astro app".
- 6d412dc: Update the README's API-reference bullet to lead with the native renderer. Since the 0.5.0 rebuild the default is Blume's own reference (one real page per operation, woven into sidebar/search/`llms.txt`); Scalar is the opt-in embed (and the AsyncAPI path) with the "Try it" playground. The bullet still described the pre-0.5.0 Scalar-only behavior.
- 759b148: Remove the dead `itemsRoot` registry export, which pointed at a directory that doesn't exist.
- 759b148: Remove the unused `deepmerge` dependency.
- 21aa451: `$ref`s to `components.requestBodies` and `components.responses` are now resolved on operation pages, so a referenced request body renders its real content type, schema, and code-sample bodies (instead of an empty `application/json` section and samples with no body), and a referenced response shows its description and schema.
- 6d412dc: Fix a `path: "/"` navigation tab falsely triggering tab-section scoping under `basePath` or a non-default locale. The root-tab exclusion compared the literal `"/"` after tab paths had already been localized and rebased, so with `basePath: "/docs"` (or on a `/fr` locale tree) the root tab's final path matched a root-level `(group)` folder's route path and hoisted that group's pages above its subgroups — the sidebar ordered differently with a base than without one under `display: "group"`/`"page"`. The exclusion now compares against the tree's based, localized root.
- 7cda9bf: A section filter picked in the search dialog persisted after the query changed, even when the new results no longer included that section — and since the filter pills hide when fewer than two sections match, the stale filter could empty the results with no visible way to clear it. The filter now resets automatically when its section is missing from the new result pool, and the search re-runs unfiltered.
- 21aa451: A search client that fails to load in production (for example a transient failure fetching the Pagefind bundle) now shows the "Something went wrong" error message in the search dialog instead of the misleading "Search is available in the production build" dev-only hint. Loading still retries the next time the dialog opens.
- 21aa451: Search indexes now honor `<Visibility>` audiences. Content marked `for="agents"` no longer appears in the site's search dialog excerpts or gets uploaded to hosted search providers (Algolia, Orama Cloud, Typesense, Mixedbread), and the MCP `search_docs` index and Ask AI grounding now apply the same rules as `get_page` and llms-full.txt: web-only content is removed and agents-only content is included.
- 6d412dc: Stop the search dialog showing "Search is available in the production build." on production sites while the search client is still loading. Typing before the lazy client import and index fetch resolved (seconds, on a slow connection) rendered the dev-only hint for every keystroke because the not-loaded and dev-missing states were indistinguishable. The load window now shows a neutral placeholder and re-renders with real results (or the error state) once the load settles.
- 759b148: A search provider request that fails (network error, provider outage) now shows an error message in the search dialog instead of silently leaving a blank results pane.
- 6d412dc: Index a config-sidebar section's landing page under its own section facet. A section declared as `sidebar: [{ label: "Guides", root: "guides/index", items: […] }]` carries its landing route on the group node, which the search crumb index skipped — so `/guides` itself indexed under the "Docs" default with an empty breadcrumb, the search dialog's "Guides" filter pill omitted the section's own landing page, and hosted-provider syncs uploaded the wrong facet for it. (Filesystem sidebars were unaffected: their index pages are page leaves, which still win over the group entry when both exist.)
- 7cda9bf: Recognize setext headings (`Title` underlined with `=` or `-`) and ATX headings indented 1-3 spaces when extracting headings, matching what the renderer actually renders — these previously vanished from the TOC, search, and page metadata and triggered false `BLUME_BROKEN_ANCHOR` warnings. Underline look-alikes (front matter delimiters, thematic breaks, list/blockquote closers, table delimiter rows, fenced-code content) are not misread as setext underlines.
- 759b148: Ship the README and LICENSE in the npm tarball so the package page on npm shows the project readme and license.
- 21aa451: `sitemap.xml` now includes custom `.astro` pages and the generated `/changelog` index. A site with a custom `pages/index.astro` landing page previously published a sitemap missing its own root URL, and the indexable changelog page was absent too. Dynamic (`[param]`) and private (`_partial`, `.well-known`) routes stay excluded, and the deployment base is layered onto the new URLs like every other entry.
- 559024b: sitemap.xml no longer lists user-authored error pages: a custom `pages/404.astro`/`pages/500.astro` (or a `404` content override) is excluded, since error routes aren't crawlable destinations.
- 6d412dc: Fix the build summary's sitemap line telling users to "set deployment.site" when the sitemap was deliberately disabled. The line now distinguishes `seo.sitemap: false` from a missing `deployment.site`, so the remediation hint only appears when it's the actual fix.
- 559024b: The bundled Blume agent skill now states the correct minimum Node.js version (22.12, matching the package's `engines` field) and clarifies that the MCP server is an endpoint served by your docs site itself, not a Blume-operated hosted service.
- 7cda9bf: The update-docs skill claimed `blume build` validates links and anchors; it now runs `blume validate` for link and anchor checks, with build covering frontmatter and duplicate routes.
- 21aa451: Remote CMS images are now content-addressed by their query-less URL. Notion's pre-signed file URLs change their query string on every API call, so the same image was written to a new file on each refresh — triggering a full-reload loop on every dev poll tick and piling up duplicate files under `blume-assets/`. Repeated builds now reuse one stable filename per asset.
- 759b148: `<Steps titleSize>` now actually resizes step titles: `<Step>` consumes the title-size variables `<Steps>` sets, with fallbacks that keep the default rendering unchanged.
- 759b148: `<Tabs>` triggers and panels now get stable generated ids and are wired both ways with `aria-controls`/`aria-labelledby`, so screen readers announce which panel each tab controls without requiring an explicit `id` prop.
- 759b148: Recognize `~~~` tilde code fences when extracting headings, links, and component tags, fixing false `BLUME_BROKEN_LINK`/`BLUME_UNKNOWN_COMPONENT` diagnostics and phantom heading anchors for tilde-fenced content (a ```line inside a`~~~` fence no longer toggles the fence state either).
- 21aa451: Fix two doc callouts whose body text was silently dropped at render. The `:::warning` examples on the syntax page (including the fenced sample that teaches the form) and the oxfmt FAQ had their body collapsed onto the directive fence line — the exact formatter collapse the FAQ itself documents — so the directive parser kept only the label and rendered an empty callout. The body now sits on its own line after `:::warning[Title]`, so both callouts render their text again.
- 759b148: `useAskAI().ask()` now catches a thrown fetch (offline, DNS failure, CORS) and shows the error notice instead of rejecting and leaving an empty assistant message stuck as a placeholder.
- 559024b: `useSearch()` now guards against out-of-order provider responses: only the latest query may commit results or clear `loading`, so a slow response for "a" no longer clobbers the results for "ab". `loading` also flips on before the lazy client creation, so the first search's index download shows as loading instead of idle.
- 6d412dc: Stop `blume validate` flagging links to fallback-rendered locale routes as broken. With i18n fallback active, an untranslated page is prerendered at its localized URL (`/fr/guide` serving the fallback content) — the i18n docs promise "the link works" — but the validator only accepted routes backed by a real page, so a French page linking an untranslated sibling failed CI with `BLUME_BROKEN_LINK`. The validate command now derives the fallback-materialized routes from the route manifest and accepts them as link targets.
- 21aa451: `blume validate` no longer reports links to custom `.astro` pages or the generated `/changelog` index as broken. A docs page linking to a custom landing page (e.g. `[home](/)` with a `pages/index.astro`) previously failed validation — and CI — with BLUME_BROKEN_LINK; those routes now count as known link targets. Anchors on them are accepted unchecked, since their headings aren't indexed.
- 6d412dc: Fix `blume validate` resolving relative links from localized index pages one directory too high. Index detection only matched a literal `index.md(x)` basename, so a dot-parser localized index (`guides/index.fr.mdx`, route `/fr/guides`) and a shared locale-agnostic one (`guides/index.$.mdx`) weren't recognized as directory indexes — `./setup` resolved to `/fr/setup` instead of `/fr/guides/setup` and a fully correct site failed validation with `BLUME_BROKEN_LINK`. Index-ness is now derived from the locale-stripped `navPath`, matching how route mapping recognizes these files.
- 759b148: `blume validate --strict` no longer fails on info-level notes (like `BLUME_ASSETS_UNCHECKED` when there is no `public/` directory) — it treats warnings and errors as failures, as documented.
- 759b148: Preserve exact redirect status codes in the generated `vercel.json` (using `statusCode` instead of the boolean `permanent`), so a configured 301 no longer ships as 308 and a 302 no longer ships as 307 on Vercel.
- 21aa451: `<Visibility>` is now honored in agent-facing Markdown. Content marked `for="web"` used to leak into llms-full.txt, the `.md`/`.mdx` mirrors, and the MCP `get_page` tool, and `for="agents"` content appeared wrapped in literal `<Visibility>` tags; web-only blocks are now removed from those outputs and agents-only blocks are unwrapped. Fenced code samples that show `<Visibility>` markup are left untouched.
- 21aa451: A `paths` entry that is a `$ref` to a shared path item now emits a warning naming the path and the spec, instead of silently dropping its operations from the API reference.
- 559024b: Hydrated component-override wrapper filenames are now injective: distinct override keys that only differ in punctuation (for example `"Foo.Bar"` and `"Foo_Bar"`) no longer collapse to the same generated `.astro` file, which raced two concurrent writes at one path and silently rendered one key with the other's component.

## 0.7.0

### Minor Changes

- 7799732: Remove the migration tooling and the Mintlify-compatibility surface. This is a breaking change for projects upgrading from 0.5.x/0.6.x.

  **Migration tooling is gone for now.** `blume migrate <tool>`, the automatic `docs.json` bridge mode, and the `mintlify` content source are removed; a migration path will return in a future release.

  **Icons are Lucide-only.** The FontAwesome and Tabler icon sets, the `icons.library` config, the `iconType` frontmatter/prop, and the `fa6-*`/`tabler` name prefixes are removed. Use bare Lucide names everywhere an icon is accepted.

  **Removed config fields** (validate-but-never-rendered Mintlify-compat): `banner.color`, `banner.type`, top-level `favicon` (favicons are detected by filename — drop `icon`/`favicon.{svg,png,ico}` in the project root or `public/`), `navigation.chromeVariants`, `icons`, `seo.metatags`, `search.prompt`, `variables`, `theme.backgroundDecoration`, and `content.assets` (move root asset dirs into `public/` — Astro serves it at the site root).

  **Removed frontmatter keys** (unknown keys are build errors): `sidebarTitle` (use `sidebar.label`), `tag` (use `sidebar.badge`), `mode`, `public`, `rss`, `hideApiMarker`, `hideFooterPagination`, `groups`, `keywords`, and `iconType`.

  **Removed components:** `<Warning>` (use the `:::warning` directive) and the `<ParamField>`/`<ResponseField>`/`<RequestField>`/`<ApiField>` field family (use `<TypeTable>`, or the OpenAPI reference for spec'd APIs).

- 331c6cb: Isolate `<Component />` previews from the docs CSS and let projects style them with their own design tokens. Previews used to render inline in the page, where the theme's prose styles (margins, typography, link/heading rules pierce `not-prose` by design) bled into the example — so a shadcn button previewed with docs styling on top. Each example now renders in its own generated route (`{basePath}/blume-examples/<path>`) embedded as an iframe: the frame boundary keeps every docs style out, and the frame loads a dedicated Tailwind entry with just preflight, utilities scanned from the example files and their imports, and Blume's design tokens (so `bg-background`-style classes still follow the site palette). The frame mirrors the site's light/dark toggle live, setting both `data-theme` and a `.dark` class so either dark-mode convention works. To bring your own tokens — e.g. shadcn variables or `@theme` mappings — point the new `examples.css` config at a stylesheet; it's injected into every frame after Blume's defaults. The existing string form stays as shorthand for `examples.source`: `examples: { source: "examples", css: "examples/theme.css" }`.
- 24f63c2: Enable the React Compiler automatically whenever React is used. Islands and other React components are now auto-memoized by `babel-plugin-react-compiler` (which ships with Blume — nothing to install), so hand-written `useMemo`/`useCallback` is no longer needed. Opt out with `react: { compiler: false }` in `blume.config.ts`.

### Patch Changes

- e816f99: Apply the `markdown.codeBlocks.theme` config to code rendering. The `theme.{light,dark}` Shiki theme names (defaults `github-light`/`github-dark`) were validated but never read; they now drive every highlighted surface — fenced code blocks, inline `` `code`{:lang} `` snippets, the `<CodeBlock>` and `<Component>` source panes, OpenAPI request/response samples, and `<Diff>` — so e.g. `markdown: { codeBlocks: { theme: { dark: "vesper" } } }` recolors dark-mode code while an unset side keeps its github default. Any bundled Shiki theme name works.
- fad14bc: Fix `deployment.base` so a subdirectory deploy (e.g. GitHub Pages project sites) prefixes everything it renders, not just bundled assets. Previously Astro prefixed `_astro/*`/fonts/CSS but Blume's own output stayed base-less, so navigation links, in-content Markdown links, canonical URLs, Open Graph images, the sitemap, `llms.txt`, RSS feeds, JSON-LD, `agent-readability.json`, the robots `Sitemap:` line, and search-result links all pointed at the wrong (base-less) path. Each is now prefixed with the deployment base at the point it's emitted — via `import.meta.env.BASE_URL` in components and templates, and `deployment.base` in the build-time SEO files — while active-route matching stays in base-less logical space. Composes with the new site-wide `basePath`: with both set, a link resolves to `{deployment.base}/{basePath}/page`.
- 75cd991: Fix `blume dev` serving stale 404s after a content file or folder is renamed, added, or removed. Two dev-only Astro/Vite issues combined to leave a page 404ing (`Entry docs → … was not found`) until a manual restart: Astro's in-memory content store only refreshes on a cold container restart — its in-place restart does an incremental sync and its glob watcher misses directory renames — and a full reload could fail to resolve the `astro:server-app` dev entry (`Failed to load url astro:server-app.js`), corrupting the SSR module runner. Now a route-set change (add/remove/rename, including folders) triggers a clean dev-server restart that re-globs the content store, and a Vite resolver shim (`serverAppResolvePlugin`) keeps full reloads from breaking. Editing a page body still hot-reloads without a restart.
- 21d4473: Fix blank UI chrome (empty search labels, aria-labels, skip link) on `ui`-less `PageLayout`/`RootLayout` pages when the consuming project resolves Zod 4. The English baseline was derived with `uiStringsObject.parse({})`, relying on each group's nested `.default({})` to deep-populate its inner field defaults — Zod 3 behavior. Zod 4's `.default()` returns the literal default without re-parsing it through the inner type, so every group collapsed to `{}` and a `ui`-less layout rendered empty strings. Derive the baseline by naming each group explicitly so field defaults apply on both Zod 3 and 4, and merge component dictionaries (`Search`, `PageFeedback`) over the baseline per key so a partial or empty strings object still falls back to defaults.
- fa18a6c: Add a top-level `basePath` config option — a site-wide mount point that prepends a segment to every generated route (e.g. `/docs/getting-started`) while staying invisible to the sidebar/nav tree, so no wrapper group appears. Links (write them as if mounted at root), redirects, the sitemap, canonical URLs, Open Graph image URLs, `llms.txt`/`llms-full.txt`, and the search index all flow through it; public assets stay at the site root. It's distinct from a per-source `prefix` (which namespaces one source and adds a group) and from `deployment.base` (Astro's host-subdirectory base) — the two compose, so with both set a page lands at `{deployment.base}/{basePath}/page`.
- b6d0761: Fix `blume dev`/`build` crashing on Windows with "The URL must be of scheme file" while generating the content config. The generated `content.config.ts` embedded each collection's glob `base` as a raw absolute path, and Astro's glob loader resolves it with `new URL(base, config.root)` — on Windows the drive letter (`C:\…`) is parsed as a URL scheme, so the result isn't a `file:` URL and Astro's subsequent `fileURLToPath` throws. Absolute bases are now emitted as `file://` URLs (via `pathToFileURL`) so the drive letter can't be mistaken for a scheme; relative bases (e.g. an ejected `blume-staged`) pass through unchanged.

## 0.6.7

### Patch Changes

- 37e1434: Make changelog entry heading/paragraph spacing actually apply. The `h2`/`h3`/`p` margins were normal-weight utilities, which Tailwind Typography's `.not-prose` margin reset outranked, so subsequent section headings still butted against the paragraph above. Mark the margins `!important` so they win.
- aa0665c: Stop the inline-code pill background from leaking into `not-prose` components. The hand-written `.prose :not(pre) > code` rule lacked the `not-prose` exclusion that Tailwind's generated prose rules carry, so components like the `<TypeTable>`, OpenAPI parameter/schema tables, and operation paths inherited a gray pill — which, as a grid cell, stretched to fill its column and painted edge-to-edge. Scope the rule to skip `not-prose` subtrees so those components render their own intended code styling.

## 0.6.6

### Patch Changes

- aae9653: Give changelog entry content proper vertical rhythm. Section headings (`h2`/`h3`) now carry a top margin so a subsequent heading reads as a new section rather than butting against the paragraph above it, and paragraphs gain spacing between them. The entry's first and last elements keep flush outer margins.
- a77a968: Hoist loose pages above groups inside tab-owned sidebar sections. When a content source uses a `prefix` (so its pages nest under a group that a tab surfaces as the sidebar), the section's top-level pages now sort above its groups — matching the tree-root behavior — instead of interleaving alphabetically.

## 0.6.5

### Patch Changes

- 3f3402d: Collapse `display: group` sidebar sections by default. Groups now start closed and open only along the active path — the current page's ancestor groups stay expanded, so just the section you're in is open. Top-level groups no longer force open; use `collapsed: false` in folder meta to pin a group open.
- beb5206: Hoist root-level pages above groups in every sidebar display mode. Previously only `flat` pulled loose top-level pages to the top; now `group` and `page` modes do too, so a root page never reads as a group's trailing child. Deep per-level hoisting remains exclusive to `flat`.

## 0.6.4

### Patch Changes

- 7b70ddd: Order changelog entries newest-first in the sidebar. Pages with `type: changelog` now sort by publish date descending — matching the generated `/changelog` timeline — instead of alphabetically by version label, so a changelog section reads latest-at-the-top. Explicit `sidebar.order` still wins.
- 4a7aa42: Emit `llms.txt` by default. `ai.llmsTxt` now defaults to `true`, so machine-readable docs ship out of the box alongside the already-on `agentReadability` and `contentSignals` signals — set `ai: { llmsTxt: false }` to opt out.
- 8252012: Remove the unused `theme.strict` config field. It validated but nothing ever read it, so it was a no-op; dropping it keeps the config surface honest. Any leftover `theme: { strict: … }` is now rejected as an unknown key.

## 0.6.3

### Patch Changes

- 1421891: Fix the mobile menu on changelog (and other "bare") pages: the header hamburger only locked page scroll because bare layouts skipped the drawer entirely. The drawer now renders on mobile — with the section tabs kept visible when the page tree is empty — while desktop keeps the bare landing layout.
- 78ad357: Render the OG card header as the logo only — no brand-name label beside it — so a wordmark logo no longer reads "Ultracite Ultracite". Without a logo, the accent tile with the brand initial still stands in.
- a155af5: Hide the "Previous"/"Next" eyebrow labels on pagination cards below `md`, so the stacked mobile cards show just the page title and arrow instead of a stubby two-line pill.

## 0.6.2

### Patch Changes

- 7b84669: Match active header tabs and nav-selector items on path boundaries, so a `/api` tab is no longer highlighted on `/api-reference/*` routes.
- 7ed2b2b: Decode Ask AI streams with `{ stream: true }` (in both the built-in panel and `useAskAI`), so multi-byte characters split across network chunks no longer render as `�`.
- 6f44994: Stop downloading and rewriting image URLs that appear inside fenced code blocks when materializing remote (e.g. Notion) assets — code samples keep showing what the author wrote.
- dfa956e: Acquire the dev lock atomically (`wx` create) so two `blume dev` processes started simultaneously can no longer both claim `.blume/` and corrupt each other's runtime.
- 6f44994: Keep a trailing `#` that is part of a heading's text (`## What is C#`) in derived titles, search entries, and the manifest TOC — only a whitespace-preceded closing hash sequence is stripped, per CommonMark.
- 9d60b00: Remove `markdown.math` and `markdown.code.inline` from the `BlumeConfig` authoring type — both features are always-on and the strict config schema rejects the keys, so configs written from autocomplete failed to load.
- 5468295: Fix code-fence meta parsing: `subtitle="..."` no longer reads as the block title, and the word "lineNumbers" inside a quoted title no longer switches on the line-number gutter.
- 9d60b00: Make `content.defaultType` actually apply: the frontmatter schema no longer forces `type: "doc"` onto every untyped page.
- 7ed2b2b: Copy buttons on Twoslash-annotated code blocks now copy the source only, stripping the hover popups' type signatures and docs that are nested inside the code element.
- dfa956e: Re-bake the dev runtime's `site` fallback when Vite bumps to a free port, so OG images, canonicals, and other site-gated URLs point at the port actually serving instead of the one that was busy.
- dfa956e: `blume doctor` now checks Node against the package's declared `engines` minimum (>=22.12.0) instead of a stale hardcoded 20, so unsupported Node versions are actually flagged.
- 36ff4da: Recognize a default-locale filename suffix under the `dot` i18n parser: `intro.en.mdx` now pairs with `intro.fr.mdx` on the `/intro` translation key instead of routing to a literal `/intro.en`.
- dfa956e: Strip unquoted inline `# comments` from `.env` values, matching dotenv/Vite — a line like `GITHUB_TOKEN=abc # note` no longer hands sources a token with the comment appended.
- 7e0f413: Make example wrapper filenames injective: distinct example paths like `button.demo` and `button-demo` no longer collide onto one generated wrapper (which made one example render the other's component).
- 4ed9f6a: Align `hasIcon` with `resolveIcon` for unknown prefixes: `tabler:check` no longer passes the existence check while rendering nothing, and unknown-prefix icons now trigger the nav "unknown icon" diagnostic.
- 7e0f413: Decode percent-encoded request paths in markdown content negotiation, so `Accept: text/markdown` requests for non-ASCII routes serve the `.md` variant instead of silently falling through to HTML.
- 7e0f413: Check custom `.astro` pages (not just content pages) before generating the MCP endpoint and the `/changelog` index, so a user page at those routes warns and wins instead of silently colliding.
- 6f44994: Import nested Notion blocks: children of list items (and other leaf blocks) were silently dropped; nested bullets now render indented under their parent item.
- 4ed9f6a: Take the OG card's brand initial by code point, so a site title starting with an emoji no longer renders a blank accent tile (a split surrogate pair).
- d660cf6: Stop entity-escaping backtick code in OpenAPI descriptions: inline code and fences like `/pets/{petId}` now render verbatim instead of showing `&#123;` entities, while surrounding prose is still MDX-neutralized.
- d660cf6: Cap remote-spec retry backoff at 10s: a server answering 429/503 with a large `Retry-After` no longer stalls `blume build` for hours.
- d660cf6: Mount a root-routed OpenAPI reference (`route: "/"`) without emitting `//tag/operation` double-slash routes or a malformed `/index.mdx` overview ref.
- d660cf6: Disambiguate OpenAPI reference slugs when distinct routes slugify identically (`/api/v1` vs `/api-v1`), so one spec no longer silently overwrites the other's data in the `blume:openapi` module.
- d660cf6: Render one OpenAPI overview section per tag slug: declared tags that slugify identically (`Store` and `store`) no longer list every shared operation twice.
- 5468295: Map `yarn global add`/`yarn global remove` in `package-install` fences onto each manager's global form instead of rendering nonsense `npm run global add …` tabs.
- 36ff4da: Percent-decode link paths and fragments before validation, so browser-copied links to non-ASCII routes and anchors (`/caf%C3%A9`, `#caf%C3%A9`) are no longer reported broken.
- 6f44994: Make `pollInterval` hot-reload actually observe remote changes: the poller now fetches fresh instead of re-reading the dev snapshot cache, and seeds its baseline from what the dev server served so the first remote change is never swallowed. Applies to the GitHub-releases, Notion, Sanity, and remote-MDX sources.
- b8a1b24: Fix folder `meta.ts` being ignored and doc pages 404ing in dev when a `content.sources` filesystem source's `root`/`prefix` diverges from `content.root`. Folder meta is now discovered per filesystem source — scanned under each source's own root and keyed by its route prefix — so a `meta.ts` inside a prefixed source (e.g. `{ type: "filesystem", root: "docs", prefix: "docs" }`) lines up with its prefixed sidebar group path and its `title`/`order` apply. A project with a single filesystem source now roots the generated `docs` collection at that source's own `root`, so entry ids resolve in dev (not just in static builds). A residual mismatch that can't be reconciled — a second filesystem source rooted outside the collection base — now raises a build-time error (`BLUME_ENTRY_ID_MISMATCH`) instead of silently 404ing at runtime.
- 7b84669: Apply locale-specific folder meta for prefixed filesystem sources under dir-parser i18n: `docs/fr/guides/meta.ts` now keys to `fr/docs/guides`, matching the navigation lookup, instead of being silently ignored.
- 7ed2b2b: Hide the mobile hamburger on the Scalar reference layout: it has no drawer to open, so tapping it only locked page scroll.
- 6f44994: Compile `**/` in remote-MDX include patterns to whole path segments, matching tinyglobby semantics — `docs/**/guide.md` no longer matches `docs/subguide.md`.
- 7e0f413: URI-encode RSS item links and guids (matching the sitemap), so routes with spaces or non-ASCII characters emit valid feed URLs.
- 7ed2b2b: Escape `<` in the search dialog's popular-pages JSON payload, so a page title containing `</script>` can no longer terminate the inline script and inject markup.
- 7ed2b2b: Guard search rendering with a generation counter: a section-pill or locale toggle racing an in-flight remote query no longer appends duplicate or misfiltered result rows.
- 41bac89: Strip only tag-shaped `<name …>` markup when building the search index: a bare `<` in prose ("costs < 5 credits") no longer deletes everything up to the next `>` — potentially whole paragraphs — from search results.
- 7e0f413: Stop prepending the site origin to an external `seo.image` URL in the generated catch-all page — `og:image`/`twitter:image` no longer render as `https://docs.example.comhttps://cdn…`.
- 7b84669: Fix sidebar group route paths when a folder's `index` page is inserted first (the group's path collapsed to `/`, emptying the sidebar for header-tab sections) and when `(group)` folders sit between the folder and its route segments.
- dfa956e: Make `blume sync` regenerate with a running dev server's URL as the `site` fallback (read from `dev.lock`), instead of silently dropping `site` and OG output from the live runtime.
- 4ed9f6a: Actually apply the table-cell inline-code nowrap rule: GFM renders `<td><code>` directly, which the descendant-only selector never matched, so inline code in table cells still wrapped.
- 9d60b00: Honor top-level `hidden` and `noindex` frontmatter as shorthands for `sidebar.hidden` and `seo.noindex` — the schema accepted both but nothing read them.

## 0.6.1

### Patch Changes

- 54099c6: Always-on inline code highlighting and block math; remove their config flags. `markdown.code.inline` and `markdown.math` are gone. Inline `` `code{:lang}` `` highlighting now always runs — it only fires on the explicit `{:lang}` marker, so plain inline code is untouched and there was nothing to opt out of. Math is now always on but **block-only** (`$$…$$`): a bare `$` (currency, shell, code) is always left as literal text, which is exactly why the flag existed, so there's no longer a `$`-in-prose caveat to gate. The `<Math>` component and KaTeX's stylesheet are still only shipped when a page actually uses `$$` — now detected from content instead of a config toggle — so a math-free site pays nothing. Inline `$…$` math is no longer supported (the single-dollar delimiter is reserved for literal text).
- 7999013: Redesign the generated `/changelog` timeline and paginate it by major version. The page now renders as a focused, full-width column — no sidebar or table of contents — and each release heading links to that release's own page, so an entry is both a timeline line and a shareable permalink. When the releases follow semver and span more than one major, older majors collapse behind a **Show N.x releases** button that reveals the next-oldest major one click at a time (detected automatically; tolerates scoped monorepo tags like `pkg@2.0.0`). It's progressive enhancement — every release stays in the page HTML, RSS feed, and search index, so no-JS readers and crawlers still get the full history.
- 5adcc8a: Add rich per-field editor docs to `blume.config.ts`. `defineConfig` gains a comprehensive JSDoc overview, and its argument is now a hand-documented `BlumeConfig` type tree so every config field — theme, navigation, content sources, search, AI, SEO, OpenAPI, i18n, and more — shows a hover description and default value with autocomplete. A compile-time guard keeps the documented type structurally in sync with the Zod schema (still the single source of validation truth), so the two can't drift. Type-only change; runtime behavior is unchanged.
- 3a6e794: Fix `output: "server"` builds deploying a 404. The Vercel/Netlify adapters write their deploy bundle relative to the Astro project root, which Blume points at the hidden `.blume/` runtime — so the bundle (`.vercel/output`, `.netlify/`) landed at `.blume/.vercel/output`, where the platform never looks, while `dist/` held only the client assets (no root page, no function). `blume build` now surfaces a server adapter's bundle up to the real project root, writes deploy artifacts (robots.txt, sitemap.xml, llms.txt, …) into the served static dir, and adds the surfaced dir to `.gitignore`. Vercel (and any Build Output API host) then picks it up with zero config — an imported Blume project deploys on the default Astro settings. Node/Cloudflare emit into `dist/` and are unaffected.

## 0.6.0

### Minor Changes

- 3dcd5b2: Emit `agent-readability.json` at the site root: a manifest that indexes the project's agent-facing surface so agents can discover and cite the docs without scraping HTML. It lists the raw-Markdown mirror pattern (with content negotiation), `llms.txt`/`llms-full.txt`, the MCP server and its `.well-known/mcp.json` discovery doc, the Ask AI endpoint, the sitemap, and RSS feeds — including only the ones actually enabled. It also echoes the `seo.contentSignals` usage policy and the configured source repository. URLs are absolute when `deployment.site` is set and root-relative otherwise.

  On by default; disable with `seo.agentReadability: false`. As with `sitemap.xml` and `robots.txt`, a file you ship in `public/` takes precedence.

- 44646d2: Add `navigation.featured` — pinned links rendered above the sidebar sections. Each takes a `label`, an `href` (external URL or internal route), and an optional `icon`, and appears on every route and breakpoint, outside the tab-scoped sidebar tree. External links open in a new tab with an indicator; internal targets are validated against your pages at build time, and unknown icons warn like anywhere else.

  ```ts
  export default defineConfig({
    navigation: {
      featured: [
        { label: "Blog", href: "https://example.com/blog", icon: "newspaper" },
        { label: "Contact", href: "/contact", icon: "headphones" },
      ],
    },
  });
  ```

  This restores a common Mintlify layout where standalone destinations (blog, changelog, support) sit at the top of the sidebar rather than folding into the generated content tree.

- 3dcd5b2: Declare `Content-Signal` usage preferences in the generated `robots.txt`. Blume now emits a `Content-Signal` line **on by default** with every signal set to `yes` — `search` (traditional and AI search indexing), `ai-input` (grounding / RAG at answer time), and `ai-train` (model training) — matching its stance that docs are open to humans and agents alike:

  ```
  User-agent: *
  Content-Signal: search=yes, ai-input=yes, ai-train=yes
  Allow: /
  ```

  Tune it with `seo.contentSignals`. Restrict individual signals — the rest stay `yes`:

  ```ts
  export default defineConfig({
    seo: {
      contentSignals: { aiTrain: false }, // → search=yes, ai-input=yes, ai-train=no
    },
  });
  ```

  Or set `contentSignals: false` to drop the declaration entirely. Existing sites that ship their own `public/robots.txt` are unaffected — Blume never overwrites it.

### Patch Changes

- 0b729bc: Redesign the Ask AI assistant. The trigger is now a ghost chat-icon button on the far right of the header, and it opens a full-height docked side panel (in the style of `vercel.com/docs`): on desktop the docs content shrinks to make room and the table of contents hides while the panel is open; on smaller screens it's a full-width overlay. The panel renders answers as Markdown, streams responses, supports a `⌘I` / `Ctrl+I` toggle, and has copy/clear/close controls. The input is a single flush textarea.

  Add `ai.ask.suggestions` — empty-state prompts shown before the first question, each a clickable `{ label, icon? }` chip.

  Improve grounding quality:

  - Retrieval now injects the section of a page most relevant to the question instead of always slicing the page head, so a long page's below-the-fold content is reachable.
  - Ask AI grounds on Markdown (code blocks preserved) rather than search-flattened plain text, so the model can answer from fenced examples the docs actually contain.
  - The model cites sources as Markdown links, rendered as small source pills that navigate to the cited page.

  Remove the "Ask AI about this page" entry from the page actions menu.

- 923877e: Exclude dependency, output, and cache trees from the generated Astro content collection. The content-layer glob previously only skipped `node_modules`, so a `.`-rooted `content.root` re-ingested build output — for example a prior `dist/*.mdx` render — and crashed the build in rolldown with an unresolvable `astro:content-layer-deferred-module` import. It now mirrors the content scan's baseline ignores (`node_modules`, `.git`, `.vercel`, `dist`, `.next`, `.turbo`, `.cache`), while the runtime directory (`.blume`, or a custom `distDir`) stays excluded precisely by the existing output-dir handling.
- 3312c07: Check the dev lock before regenerating `.blume/`, and record the server's port in it. A second `blume dev` previously regenerated the runtime (with its own port baked in) before noticing the lock, so even a refused invocation churned the running server's generated files on its way out; it now refuses before touching anything. The lock file (`.blume/dev.lock`) stores `{pid, port}` — updated with the actual bound port if Vite bumps a busy one — so the refusal messages from `dev`, `build`, `check`, and `eject` point at the live server's URL (e.g. "A `blume dev` server is already running at http://localhost:3001 — reuse that server"), steering callers (especially agents) toward reusing the running server instead of killing it.
- 1081989: Add a "Copy Codex command" option to the Connect to MCP menu, alongside the existing Claude Code, Cursor, and VS Code installs. It copies `codex mcp add <name> --url <url>` for the hosted MCP server. Also make `PageActions` labels fall back to the English defaults per-key, so a string missing from a translation renders the default instead of coming out blank.
- 1081989: Improve the page-actions dropdowns (Export, Open in chat, Connect to MCP). Only one opens at a time — opening one closes the others. A menu flips above its trigger when opening downward would run past the viewport bottom (and there's room above), so the Connect to MCP menu no longer gets clipped. Menus keep a padding gap from the viewport edge and size to their content instead of the narrow sidebar column, so longer items like "Copy Claude Code command" no longer wrap.
- 923877e: Default-ignore never-content directories during the content scan. The filesystem source now always skips `node_modules`, `.git`, `.blume`, `.vercel`, `dist`, `.next`, `.turbo`, and `.cache` — in addition to the user's `content.exclude`, and even when `exclude` is overridden. Previously a broadly-scoped `content.root` (`"."` or an app directory that also holds `node_modules`/build output — the common shape when migrating a docs app that lives at the repo or app root) would glob thousands of stray Markdown files out of dependencies and build artifacts. `content.root` still defaults to `docs/`, where this rarely bit.
- 4ff9c26: Fix the Vercel (`output: "server"`) build failing in `astro:build:done` with `dist/server/entry.mjs does not exist`. Blume declared its render-time SSR externals under a user-owned `vite.environments.ssr` block, which collides with the internal environment Astro 7 builds the server under and detaches the adapter's server entrypoint from the rolldown input — so the SSR entry was emitted as `index.mjs` and the Vercel adapter couldn't find `entry.mjs`. The SSR externals now go through the legacy `vite.ssr.external` key (the `prerender` environment is Astro-only and stays under `environments`).
- 923877e: Widen the dev watcher's ignore set to cover build and deploy caches. Alongside `.blume`, `.git`, and `node_modules`, the recursive content watcher now also ignores `.vercel`, `dist`, `.next`, `.turbo`, and `.cache`, sharing one canonical directory list with the content scan so the two never disagree about what counts as content. This keeps churn from build output and framework caches from needlessly re-triggering a rescan during `blume dev`.

## 0.5.4

### Patch Changes

- 4de9522: Add a `blume-update-docs` agent skill for keeping a Blume docs site in sync with the product it documents. A scheduled agent run audits recently merged PRs, changelogs, config schemas, and CLI help against the docs, updates only pages that are factually stale (feature-flagged work is ignored), verifies with `blume build`, and opens or updates a `blume/*` pull request — or reports a clean no-op. Install it with `npx skills use haydenbleasel/blume@blume-update-docs`; it also ships in the package at `node_modules/blume/skills`. The new `/docs/advanced/skills` page documents all shipped skills.
- ac4dd2a: Fix code blocks rendering flush against the edge (no horizontal padding, inline, unscrollable) inside content components — `<Steps>`, `<Callout>`/`:::note`, `<Card>`, `<Accordion>`, `<Expandable>`, `<Panel>`, `<Update>`, `<Tabs>`, and `<CodeGroup>`. The rule that gives code blocks their inset opted out of every `not-prose` subtree, but only the API request panel actually owns its own code layout — every other component wraps its chrome in `not-prose` while still hosting real prose content. The exclusion now targets just the API panel, so code keeps its standard padding everywhere else. The Component source pane and API request panel are unchanged.
- 3ac07da: Add a global `min-width: 0` base reset so flex and grid children shrink to their container instead of forcing horizontal overflow. This defuses the common case where a long or truncating child (a code snippet, a long nav label) pushes its row — and the whole page — past the viewport edge on narrow screens, and removes the need for the per-element `min-w-0` overrides the components previously carried. The base layer also now defaults interactive controls (`button`, `[role="button"]`) to `cursor: pointer` unless disabled, and enables `text-rendering: optimizeLegibility`.
- d8a413e: Harden remote OpenAPI spec loading and stop a failed fetch from shipping a dead reference tab. Remote (`http(s)`) specs are now fetched defensively — bounded by a per-attempt timeout, retried with backoff on transient failures (network errors, timeouts, 408/425/429/5xx, honoring `Retry-After`), sent with a User-Agent, routed through an HTTP(S) proxy when `HTTP(S)_PROXY` is set (Node's `fetch` ignores proxy env vars on its own, the classic "curl works but the build doesn't" gap), and cached on disk so a transient outage falls back to the last good copy with a warning instead of dropping the reference. A spec that still can't be loaded is now an error in `build` (a configured reference otherwise ships a nav tab pointing at a route that was never generated — a silent 404) while staying a warning in `dev` so offline work keeps running.
- 9afab11: Split the `logo` config into an `image` mark and a `text` wordmark so a site can show an image-only logo, a text-only logo, or both. The object form is now `{ image, text, href }`: `image` takes the same value as the string shorthand (a single path, or `{ light, dark, alt }` for themed artwork), and `text` controls the wordmark independently — omit it to fall back to the site title (the previous behavior), set `text: ""` to render the mark alone (handy when the logo image already carries the wordmark), or set `text` with no `image` for a text-only logo. The bare-string shorthand (`logo: "/logo.svg"`) is unchanged; the old flat object form `{ light, dark, alt }` now nests under `image`.
- 6d4ae0d: Restyle collapsible sidebar group headers to sit back visually: the group `<summary>` now renders in the muted foreground color and brightens to the full foreground on hover, matching the treatment of leaf nav links. The active-page group header still resolves to the foreground color and bold weight so the current section stays legible.
- fdf6863: Fix tab navigation on routes that own no sidebar group. A page under a tab whose source produced no sidebar sections — a standalone page like the generated `/changelog` timeline — now renders an empty sidebar instead of falling back to the full tree, which had leaked every other tab's sections (e.g. the OpenAPI operations) onto the page. Chrome-only pages rendered via `PageLayout` (a landing page with tabs but no sidebar) also gain a working mobile navigation drawer: the header's nav toggle now appears whenever there are tabs — not only when a sidebar is present — and opens a slide-in tabs drawer below `lg`.

## 0.5.3

### Patch Changes

- 72ce7c8: The generated Ask AI endpoint now validates message shapes, not just the array size. Previously any JSON array of 1–40 items was forwarded to `streamText` verbatim, so a caller could POST a `role: "system"` message and repurpose the unauthenticated endpoint as a general LLM proxy on the site owner's API key. Each message must now be `{ role: "user" | "assistant", content: string }`, and the array is rebuilt so only those two fields ever reach the model.
- 9d6bf61: Ask AI and search now work when `deployment.base` has no trailing slash. Astro passes the base through as-is, so `base: "/docs"` made the Ask AI island POST to `/docsapi/ask` (every question failed with a 404), page grounding send `//guide` paths, and the generated search clients request `/docsblume-search.json` / `/docsapi/search` / `/docspagefind/pagefind.js`. All island and generated-client URL joins now go through a shared helper that normalizes the trailing slash.
- c0dcfc1: A round of CLI and generated-endpoint hardening: config validation now reports every issue in one failing run instead of one per rerun; a second concurrent `blume dev` refuses instead of silently sharing (and corrupting) `.blume/` with the first; the dev-lock liveness probe treats an `EPERM` (process alive under another user) as locked instead of stale; an explicit `/index` sidebar ref resolves to the root route instead of an empty link; the generated Mixedbread search endpoint returns an empty result for malformed JSON instead of a 500; malformed percent-encoding in an asset URL 404s instead of throwing in middleware; and a `client:media` override value with quotes or newlines can no longer break the generated wrapper component.
- c9c12d8: `<Color.Item>` dual-value swatches now actually switch in dark mode. The dark-mode style targeted a `.dark` class that Blume never sets — dark mode is `data-theme="dark"` on `<html>` — so `value={{ light, dark }}` always rendered the light color. The selector now matches the real dark-mode attribute.
- a1797e8: A batch of component and markdown fixes: code-fence titles may now contain the other quote character (`title="foo's file.ts"` no longer mangles into `file.ts"`); package-manager install tabs no longer rewrite the page hash when clicked (clobbering the heading anchor the reader arrived with); search-dialog highlighting can't mark inside HTML entities anymore (querying "amp" corrupted excerpts containing `&`); a failed search-index load retries on the next open instead of disabling search until reload; the `useAskAI` hook surfaces an error message instead of streaming a 4xx/5xx body as the assistant's answer; copy buttons can't get stuck on "Copied!" after a double-click; the Cursor MCP install deeplink URL-encodes its base64 config; generated cURL samples survive apostrophes in example bodies and Python samples no longer rewrite `true`/`false`/`null` inside string values; Mermaid diagrams disconnect their theme observer when removed and drop stale renders from rapid theme toggles; and a `<blume-tabs>` moved in the DOM re-attaches its sync/hash listeners.
- 8b1de21: Folder `meta.ts` files now apply to every locale under the `dot` i18n parser. Non-default locales looked meta up under a locale-prefixed key (`fr/guides`) that only exists in the `dir` layout, so with `parser: "dot"` — where translations sit next to the originals — translated sidebars silently lost their configured titles, ordering, icons, and collapsed state and fell back to humanized folder names. Locale-prefixed lookups now only happen under the `dir` parser.
- ebadad0: The Fumadocs migrator no longer deletes a `meta.json` whose conversion was skipped. When a `docs/<dir>/meta.ts` already existed at the destination, the migrator skipped writing the converted meta but still removed the source `meta.json` — permanently losing its title and page ordering. The source file is now kept alongside a warning telling you to merge it by hand, matching how page collisions already behave.
- 27b3c71: Link validation no longer flags Markdown link syntax shown inside inline code. Prose like `` use `[label](/page)` syntax `` registered `/page` as a real link, and since broken internal links are build errors, any page demonstrating link syntax failed `blume validate`. Inline code spans are now masked out (with column positions preserved) before link extraction, matching how component-tag extraction already behaves.
- d8d458c: MCP server URLs now honor a subpath `deployment.site`. `search_docs`/`list_pages` result URLs and the `/.well-known/mcp.json` server address were built with `new URL(route, site)`, which drops the path of a base like `https://acme.com/docs` — pointing agents at nonexistent root-level pages while `llms.txt` linked correctly. Both now concatenate like the rest of the AI surface.
- fc9a4a1: The `mdx-remote` content source no longer sends `GITHUB_TOKEN` to non-GitHub hosts. Every fetch — including files enumerated from a custom `url` base pointing at an arbitrary server — attached `Authorization: Bearer $GITHUB_TOKEN`, leaking the repo credential to whatever host the source was configured against. The token is now only attached for `api.github.com` and `raw.githubusercontent.com`.
- edf6fef: `blume dev` on a migrated Mintlify project (`content.root: "."`) no longer loops or fails to load. A migrated project keeps its pages at the project root, which also contains Blume's generated `.blume/` output — and the dev server rewrites files under `.blume/` (its data store, content-module manifests, self-hosted fonts, and the regenerated `src/generated/*` data modules) as it renders. Two watchers fed on those writes:

  - The **filesystem content source**'s recursive `fs.watch` saw every `.blume/` write and re-ran a full rescan + runtime regeneration, whose own writes landed back under `.blume/` and re-fired the watcher — a self-sustaining loop that spammed the console with repeating `data-store.json` reloads and, by rewriting the runtime mid-render, broke Astro's dev module graph (`Failed to load url astro:server-app.js`). This is the same storm the Mintlify bridge source was already hardened against; the fix is now shared, so the filesystem source ignores events under `.blume/`, VCS/dependency trees, and every excluded directory.
  - Astro's content-layer `docs` glob loader is rooted at the project directory, so it logged `No entry type found` / `Reloaded data` for every write Astro makes under its own cache dir (`.blume/.astro/`). The generated dev config now keeps Vite's file watcher out of that cache dir.

  As a defence in depth, dev regeneration is now single-flighted: a burst of watch events (or any future storm) coalesces into one trailing scan instead of piling up overlapping scans, which on a large project could outlast the debounce and exhaust the heap (the loop above eventually OOM-crashed the dev server).

  Normal projects, whose content root sits outside `.blume/`, were unaffected.

- c57377e: Migrator and search-sync cleanups: stripping old-framework imports no longer collapses double blank lines inside code fences (the gap-collapse now targets only the removal seams, across the Mintlify, Fumadocs, and Starlight migrators); the Nextra migrator repoints `package.json` scripts at Blume and gitignores `.blume/`/`dist/` like the Fumadocs one (previously `npm run dev` still launched Next against the gutted content tree); the Mintlify migrator's two local path-containment checks now use the shared `isInsideRoot`, closing a Windows-only gap where a cross-drive `$ref`/snippet path escaped the project root; and the Typesense and Orama Cloud syncs now carry the documented `locale` facet so i18n sites can filter hosted results per language.
- 5fc39d4: `blume migrate mintlify` now keeps images referenced only by page content resolvable. Asset relocation previously considered just `/images` plus paths named in the config (logo, favicon, backgrounds) — a page using `![…](/screenshots/home.png)` or `<img src="/img/logo.svg">` silently 404'd after migration, since Mintlify serves every top-level directory at the site root but Blume only serves `public/` and `content.assets` mounts. Root-absolute asset references in content are now collected during the rewrite and their directories added to `content.assets`.
- ad120e0: `blume migrate mintlify` now removes the source `docs.json`/`mint.json` after writing `blume.config.ts`. Leaving the Mintlify config on disk kept the project a bridge-mode candidate: any later run without a loadable `blume.config.*` would silently fall back to serving the un-migrated Mintlify project. The foreign config is only deleted after the Blume config is safely written, so a mid-migration failure never leaves the project with neither.
- ff6b5d4: A dangling Mintlify snippet import no longer aborts the whole migration. `blume migrate mintlify` crashed with a raw `ENOENT` when any page imported a snippet file that doesn't exist — after some pages were already rewritten in place and before `blume.config.ts` was written, with no hint which page failed. The affected page (missing snippet or circular snippet chain) is now left unconverted with a warning naming both the page and the snippet, and the rest of the migration completes.
- e796b18: Migrators now convert nested callouts correctly. The callout-to-directive rewrite found its close tag with a flat string search, so `<Warning>… <Info>…</Info> …</Warning>` left the inner component unconverted (failing the build — Blume ships no `<Info>`), and same-tag nesting closed at the inner tag, leaking a stray `</Note>` into the page (an MDX compile error). Close tags are now matched depth-aware, inner bodies are converted recursively, and outer directive fences grow (`::::`) so nested `:::` blocks parse as containers. Applies to the Mintlify, Fumadocs, Nextra, and Starlight migrators.
- f38e7dd: Circular OpenAPI schemas no longer crash the build with a stack overflow. `typeLabel` recursed forever on arrays whose `items` `$ref` pointed back at the same schema (it resolved the ref before recursing, bypassing its own `$ref` shortcut — array-of-ref types now label as `Name[]`), and `objectProperties` followed mutually-recursive `allOf` chains with no visited-ref guard, which also took down `exampleValue` and every generated request sample. Both now terminate on any cyclic spec.
- 132f6a9: OpenAPI descriptions that start a line with `import` or `export` no longer crash the build. Operation and overview descriptions are embedded as markdown in generated MDX pages, and MDX parses lines beginning with those keywords as ESM — so common API prose like "import the SDK and call the endpoint" failed compilation with an acorn parse error. The keyword's first letter is now entity-escaped alongside the existing `<>{}` neutralization; the rendered text is unchanged.
- 2ed8345: Leading/trailing slashes in a frontmatter `slug` or a source `prefix` no longer produce malformed routes. `slug: /getting-started` mapped to `//getting-started` and `slug: guides/` to `/guides/` — routes nothing could link to, tripping false `BLUME_BROKEN_LINK`/`BLUME_NAV_MISSING_PAGE` diagnostics and diverging translation keys under i18n. Slugs and prefixes are now trimmed of surrounding slashes and empty route segments are dropped; dotted slug segments like `releases/v1.2` are preserved.
- 5807c22: `SourceEntry.slug` is now honored. The custom-source SPI documents `slug` as "logical route input; defaults to `ref` if omitted", but normalization only ever read `ref` — an adapter returning `{ ref: "abc123.md", slug: "custom/path" }` routed to `/abc123` with no diagnostic. Frontmatter `slug` still wins over the adapter-supplied one.
- e0bc986: `theme.accent`, `theme.action`, and `theme.backgroundDecoration` now actually apply in dark mode. The base stylesheet's `:root[data-theme="dark"]` block outranked the `:root` config tokens on CSS specificity, so dark mode silently kept its neutral defaults — `accent: "teal"` gave a teal light mode and a near-white dark mode, contradicting the documented "light and dark share one accent". The generated config CSS now re-declares the mode-shared tokens (accent + foreground, action, background decoration) in a dark-scoped block; `accentDark` still takes precedence when set.
- a45d842: Hardened theme and OG rendering against hostile-shaped config and content values. A `theme.accent` (or icon name/library) matching an `Object.prototype` member like `constructor` resolved a function up the prototype chain — stringifying into the generated CSS or crashing icon resolution mid-build with no pointer to the offending page; lookups are now own-property only. A malformed hex accent (`#12345`) no longer throws a native error inside the OG renderer and fails the build — it falls back to the default accent. The OG logo viewBox parser now also accepts single-quoted attributes and non-zero origins, so wide wordmarks keep their aspect ratio instead of being squeezed into a square.

## 0.5.2

### Patch Changes

- 34ccd52: `blume dev` in Mintlify bridge mode is no longer slow and noisy. Bridge mode roots content at the project directory, which contains Blume's generated `.blume/` output — and the dev server rewrites `.blume/.astro/*` (its data store and font cache) on every request. Two things fed on those writes:

  - The Mintlify source's recursive `fs.watch` saw them and re-ran a full rescan + runtime regeneration, whose own writes landed back under `.blume/` and re-fired the watcher — a self-sustaining storm that stalled page renders (8–26s). The watcher now ignores events under `.blume/`, `node_modules`, and other non-content trees.
  - Astro's content-layer `docs` collection was rooted at the project directory even though, in bridge mode, every page renders through the staged collection — so its glob loader watched `.blume/.astro/fonts/` and warned `No entry type found` for each `.woff2` on every rebuild. The `docs` collection now globs nothing when no filesystem source feeds it (glob-pattern negations can't exclude a subtree from Astro's watcher, so an empty pattern is the only reliable fix), which also avoids double-loading the staged bodies under `.blume/content`.

  Normal (non-bridge) projects were unaffected, since their content root sits below `.blume/`.

- b47f927: `blume migrate mintlify` now scaffolds the project files a config-only Mintlify repo lacks. Mintlify docs are driven by `docs.json`/`mint.json` and ship no npm manifest, so a fresh migration previously left nothing to run `blume dev` with. The migrator now writes a minimal, runnable `package.json` with `blume` pinned as a dependency and `dev`/`build`/`doctor` scripts (derived name, `private: true`), plus a `.gitignore` for Blume's generated `.blume/` runtime and `dist/` build output — so `npm install && npm run dev` works immediately and the generated output stays untracked. Both are idempotent: an existing `package.json` is left untouched and an existing `.gitignore` is only extended. The `package.json` template is now shared with `blume init`.

## 0.5.1

### Patch Changes

- 348b6f4: Dropped `navigation.sidebarVariants`, an unused per-partition sidebar mechanism. It was built, validated, and shipped in the data module, but no layout ever read it — the sidebar has always been scoped at render by the active **tab** via `sidebarForRoute`, and that remains the model. The Mintlify migrator emitted one variant per page (each carrying the full section sidebar), which is what ballooned a large migrated site's `blume.config.ts` to ~4.9 MB / 126k lines; it no longer emits any, so migrated configs shrink to a few KB. The `sidebarVariants` config field, its schema, the `NavSidebarVariant` type, and the variant folds in nav diagnostics are all removed.

  Because `navigation` config is `.strict()`, a stale config that still carries a `sidebarVariants` key will now fail validation — drop the key or re-run `blume migrate mintlify` to regenerate a clean config.

- 1d5937c: `blume migrate mintlify` now rewrites Mintlify accordions to Blume's shape. Mintlify wraps `<Accordion title="…">` items in an `<AccordionGroup>`, whereas Blume inverts that: `<Accordion>` is the container and each item is an `<AccordionItem title="…">`. The migrator previously left both tags untouched, so migrated pages that used accordions failed the MDX build outright — Blume ships no `<AccordionGroup>` component, so rendering threw `Expected component AccordionGroup to be defined` — and the nested `<Accordion title=…>` items lost their title and expand behavior. The group is now remapped to `<Accordion>` and every item to `<AccordionItem>` (preserving `title`, `icon`, and other props), so those pages build and render correctly.
- 8ac7101: `blume migrate mintlify` now drops dynamic (wildcard/param) redirects instead of emitting ones that break the build. Mintlify's `:slug*`/`:id` redirect params were translated to Astro `[...slug]`/`[id]` segments, but Blume redirects are static path-to-path — a dynamic destination matches no route, so Astro aborted the entire build (`The destination "…/[...slug]" does not match any existing route in your project`). Such redirects are now skipped, and the migrator emits a warning that names the dropped sources and points to host-level redirect files (`_redirects`, `vercel.json`), which do support wildcards. Static redirects are unaffected, so the migrated site builds.

## 0.5.0

### Minor Changes

- 48d68f4: Icons now resolve from the full open icon libraries — **Font Awesome** (free), **Lucide**, and **Tabler**, the three Mintlify exposes — instead of a hand-curated subset. Resolution happens at build time and inlines zero-JS SVG, so there's no runtime CDN fetch (unlike Mintlify) and unused icons cost nothing on the client.

  - New `icons.library` config picks the default library for bare names (`"lucide"` default, or `"fontawesome"` / `"tabler"`).
  - `iconType` selects a Font Awesome style (`solid`, `regular`, `brands`); Pro-only styles (`light`, `thin`, `duotone`, `sharp-solid`) aren't in the free data and fall back to solid.
  - An explicit `library:name` prefix (`fa6-brands:github`, `lucide:rocket`, `tabler:heart`) overrides the default per icon, so libraries can be mixed.
  - Font Awesome brand names resolve even under the solid default (`icon="github"` finds the brands set).
  - The Mintlify migrator sets `icons.library: fontawesome` (Mintlify's default), so a migrated site's existing Font Awesome icon names — previously mostly unresolved — now render.

  The curated inline-SVG set and its FontAwesome alias map are gone; a small internal set is kept only for Blume's own client-side chrome (copy/search/etc.). The five Iconify data packages are build-time dependencies (server-side only), so they add nothing to shipped pages.

- 6d17781: Add `--isolated` to `blume build` and `blume check` so you can build/verify while a `blume dev` server is running. Both commands regenerate the shared `.blume/` runtime, so running them against a live dev server would corrupt it — `build` refused and `check` (which had no guard) silently corrupted it. `--isolated` relocates the whole generated runtime — and, for `build`, its `dist/` output — to a throwaway `.blume-verify/` sibling (auto-gitignored), leaving the dev server's `.blume/` and your real `dist/` untouched. Isolated builds skip the deploy post-steps (search index, hosted-provider sync, `llms.txt`, sitemap/robots, redirects) since a verify only needs to confirm the site compiles and renders. `check` now also refuses a live dev server when not isolated, the refusal message points at `--isolated`, and `BLUME_RUNTIME_DIR` lets plain `build`/`check` isolate without the flag (useful for coding agents verifying changes alongside an open dev server).
- e023325: Rebuild OpenAPI support with a native, Blume-rendered API reference (default `renderer: "blume"`). Blume now parses each spec with Scalar's OpenAPI parser (upgrading Swagger 2.0 / OpenAPI 3.0 to 3.1) and lowers every operation into a real content page — so operations get their own route (`/reference/<tag>/<operation>`), a tag-grouped, tab-scoped sidebar with colour-coded method badges, and inclusion in site search, `llms.txt`, and Open Graph, just like any hand-written doc. Operation pages use a two-column layout: parameters and schema tables on the left, a Scalar-style Request/Response panel (language tabs + copy, status-tabbed response examples) on the right in place of the table of contents. New `openapi` options: `renderer` (`"blume"` | `"scalar"`), `codeSamples`, and `expandSchemas`. Set `renderer: "scalar"` to keep the embedded Scalar reference; AsyncAPI continues to render through Scalar.
- 1780373: Added Mintlify-compatible `<ParamField>`, `<ResponseField>`, and `<RequestField>` components for documenting request/response fields (CLI flags, SDK arguments, endpoint parameters). Each renders a labeled field row — name, type, and `required`/`deprecated`/`default` badges styled to match the native OpenAPI reference — with the description taken from the element's body (rich MDX, including a nested `<Expandable>`). `<ParamField>` reads the field's location from the attribute that names it (`path`, `query`, `header`, `body`, or a plain `name`) and shows it as a small label. Previously these had no Blume equivalent, so ~488 uses across a migrated Mintlify site failed to compile as undefined MDX components; they now render as-is in both a migrated project and live Bridge mode. The Mintlify migrator no longer flags them (or points at the OpenAPI reference for surfaces a spec doesn't cover); Mintlify's `<Update>` changelog component remains flagged since Blume's changelog is frontmatter-driven.

### Patch Changes

- 515277c: Page frontmatter now accepts an `authors` field (a name, an array of names, or an array of author objects with a name plus optional avatar/url and any extra fields). Blume's `pageMetaBaseSchema` is `.strict()`, so a page carrying `authors` — common on blog/changelog content, including sites moved over with `blume migrate mintlify` — previously failed frontmatter validation entirely (`BLUME_FRONTMATTER_INVALID`) and dropped out of the scan. The field is preserved as-is (not yet rendered), so those pages validate and keep their author metadata.
- 0897ca9: Expanded the built-in icon set and its FontAwesome/Lucide synonym map so far more icon names resolve. The curated set grows from 53 to 158 icons (common docs glyphs — `gauge`, `layers`, `shield`, `cpu`, `database`, `terminal`, `cable`, `file-text`, `folder-tree`, chart/cloud/list/user variants, and more), and the alias map grows from 15 to 177 entries mapping FontAwesome names to their closest Blume icon (`shield-halved` → `shield-half`, `layer-group` → `layers`, `arrows-rotate` → `refresh-cw`, `wand-magic-sparkles` → `wand-sparkles`, `user-shield` → `shield-user`, `gauge-high` → `gauge`, …). Sites migrated from FontAwesome-based Mintlify projects now render icons on Cards, Steps, and sidebar groups that previously resolved to nothing, and several icons referenced by Blume's own docs (`cable`, `file-text`, `folder-tree`) now render. Icons are inlined as zero-JS SVG server-side, so unused entries add nothing to the client payload.
- 86761c1: The Mintlify migrator now maps `fonts` to `theme.fonts` and stops silently dropping site chrome it can't model. A `fonts.family` (or a `heading`/`body` split) resolves to the matching Blume Google-font slug — `Space Grotesk` → `space-grotesk`, `Geist` → `geist`, and so on — and a family outside Blume's curated set is reported rather than guessed. Header links (`navbar.links`/`navbar.primary`) and footer socials (`footer.socials`), which have no `blume.config` equivalent, are now surfaced as migration warnings (pointing at `navigation.tabs` or a Header/Footer layout override) instead of disappearing. The contextual page menu and last-updated timestamp are already covered by Blume defaults, so they're intentionally treated as no-ops.
- 5aacd93: The Mintlify migrator now maps `openapi` spec sources onto Blume's native OpenAPI reference instead of silently dropping them. Previously `loadMintlifyConfig` never read a top-level or per-group `openapi` key and skipped `GET /path` endpoint refs, so a site whose entire API reference is a remote spec declared in nav produced no `openapi:` block and no warning. It now walks the navigation tree (plus top-level `openapi` and `api.openapi`) collecting every spec — a string, an array, or a `{ source, directory }` object — dedupes by spec, maps a group's `directory` to the reference's `route`, and emits `openapi: { enabled: true, sources: [...] }`. Endpoint refs are still skipped (the native renderer generates those pages from the spec), and the migration prints a warning listing how many spec sources were mapped so you can verify each path or URL resolves.
- 07aa14c: The Mintlify migrator now translates path-to-regexp wildcard redirects into Astro's dynamic-segment syntax. Previously `mintlifyRedirects` copied `from`/`to` verbatim, so a `/old/:slug*` → `/new/:slug*` redirect reached Astro's `redirects` unchanged and never matched. Both sides of each redirect are now converted (repeatable params `:name*`/`:name+` → `[...name]`, others → `[name]`), preserving the param name so Astro can substitute it into the destination.

## 0.4.0

### Minor Changes

- dddb157: Add a `<YouTube />` content component for embedding YouTube videos. It renders a responsive, privacy-enhanced (`youtube-nocookie.com`) 16:9 iframe with `loading="lazy"` and ships no client JavaScript. Pass a video `id` or a full `url` (any of the `youtu.be`, `watch?v=`, `/embed/`, `/shorts/`, `/live/` forms), plus an optional `title` and a `start` time in seconds. Available in `.mdx` pages, in `<BlumePage>` embeds, and via `blume add youtube`.
- 2727bfd: Add `content.assets`: top-level directories served at the site root alongside `public/`. This lets a project keep root-served asset folders in place instead of relocating them under `public/`. The generated runtime serves each mount in dev (Astro only serves `publicDir`) and copies it into `dist/` on build, and link validation resolves asset references against these mounts too.

  The Mintlify migrator now uses this instead of moving whole asset directories: referenced dirs (e.g. `images/`) stay put and are recorded in `content.assets`, so a migration no longer churns every file under them. Loose top-level asset files (a root `favicon.png`/`logo.png`) still move under `public/`.

### Patch Changes

- c2ef9a0: Guard the shared `.blume` runtime dir with a dev lock. `blume dev` continuously regenerates and serves `.blume`, so a `blume build` or `blume eject` run in another shell could regenerate or delete it out from under the live Vite server and corrupt the session. `dev` now writes a PID lock, and `build`/`eject` refuse with a clear message while it's held (stale locks from a crashed dev server are ignored). `blume sync` is unaffected — it's designed to refresh content while `dev` is running.
- b0c592b: Validate a raw `theme.accent` (and `accentDark`/`action`) before writing it into the generated theme CSS and the Scalar OpenAPI theme. A value containing CSS control characters like `;}` could otherwise break out of the declaration and inject rules; such a value now falls back to the default accent. Named presets and normal colors (hex, `rgb()`, `oklch()`, …) are unaffected.
- c38af59: Fix the grounded **Ask AI** endpoint failing to build. The generated `/api/ask` route lives at `src/pages/api/ask.ts` but imported its retrieval data from `../generated/ask-data.json`, which resolves one directory too high (`src/pages/generated/…`) and doesn't exist. It now climbs two levels (`../../generated/ask-data.json`), matching the other depth-two endpoints, so Ask AI builds under the default (gateway) provider and every grounded backend.
- 7ac6fc5: Show the error notice, not the raw error body, when an Ask AI request fails. The in-page island streamed `response.body` without checking `response.ok`, so a 4xx/5xx (e.g. a rate-limit or server error) had its error text decoded and rendered as the assistant's answer. Non-OK responses now surface the friendly error message instead.
- fc4d1c4: Reject a non-numeric `--budget-js` / `--budget-css` on `blume build` instead of silently disabling the performance gate. `Number("250kb")` is `NaN` and `total > NaN` is always false, so a typo'd budget made the check pass no matter the bundle size. The flags are now validated up front and error out like `--output` and `--adapter`.
- 805eb5b: Anchor config/frontmatter diagnostic positions to whole keys. When locating a Zod issue in the source, a path segment like `title` could match the tail of an unrelated key such as `subtitle:`, pointing the error at the wrong line/column. Key matching now requires a word boundary.
- aba6f5e: Keep formatted words in a callout's `[label]` title. The title text was gathered only from a paragraph's immediate text children, so any bold, italic, inline-code, or linked word was dropped — `:::note[Read **this** now]` became `Read  now`. The label text is now collected recursively, preserving every word.
- ee6e165: Stop `blume validate` from misreading a route with a dot in its last segment as a missing asset. A link to a real page like `/releases/v1.0` matched the asset-extension heuristic (`.0`) before the route was checked, producing a false `BLUME_BROKEN_ASSET` warning. Link validation now checks the route map first, so a real route always wins over the asset heuristic.
- 3ad8b84: `blume eject` no longer silently overwrites a customized root `tsconfig.json`. Eject wrote its own `tsconfig.json` unconditionally, clobbering any paths or compiler options you'd tuned, and the confirmation only mentioned `astro.config.mjs` and `src/`. It now leaves an existing `tsconfig.json` in place (writing one only when absent), and the confirmation discloses the `tsconfig.json` and `package.json` changes.
- 779fa7d: Fix a build crash when a callout directive is empty. An empty `:::note` / `:::` (no body) parses to a node with `children: null`, which the callout plugin spread into an array and threw on — failing the whole page build. Empty callouts now render as an empty `<Callout>` instead of crashing.
- 543068d: Report the correct column for a Markdown link whose label repeats its target. The link position was found by searching for the target text from the start of the `[label](target)` match, so `[/a/b](/a/b)` pointed at the occurrence inside the label. The column is now taken from the `](` boundary.
- be9cc25: Contain the Fumadocs migrator to the docs tree. A `pages` entry in a `meta.json` (e.g. `"../../victim"`) could resolve to a file outside the source directory and get `rename`d out of place, and an `<include>../../secret</include>` could read and inline an arbitrary file into the migrated output. Both paths now reject targets that escape the docs root — matching the Mintlify migrator's existing guard — and skip them with a warning.
- 5e43a64: Fix `blume validate` falsely flagging relative links from index pages. A directory index (`guides/index.mdx`, route `/guides`) has a route that already _is_ its directory, but relative resolution still popped a segment, so a link like `./setup` resolved to `/setup` and was reported as a broken link. Index pages now resolve relative links against their own route.
- 6b2e84a: Reject a `--content-dir` on `blume init` that escapes the project. The value is joined into every scaffolded file path, so `--content-dir ../../foo` would write seed content outside the target directory. An absolute or `../`-escaping content dir now errors out.
- d9d1057: Skip island files whose name isn't a valid identifier instead of emitting a broken module. A file like `islands/Time-Picker.tsx` starts uppercase but its name is used verbatim as an unquoted object key in the generated island map (`Time-Picker: I0`), which is a syntax error that failed the entire build with no pointer to the offending file. Island names are now validated as full PascalCase identifiers (letters, digits, underscores) and non-conforming files are skipped with a warning, like lowercase names already were.
- 0ad5cb7: Don't truncate `--json` diagnostics output in CI. `blume doctor --json` and `blume validate --json` wrote the JSON payload and then called `process.exit`, which doesn't flush a piped stdout — so a large payload could be cut off into invalid JSON that the consumer couldn't parse. The commands now drain stdout before exiting non-zero.
- 340acaa: Honor a non-root `deployment.base` in the dev server's `Accept: text/markdown` negotiation. The rewrite matched the base-prefixed request URL against the base-less content routes, so markdown negotiation silently did nothing under a configured `base`. The base is now stripped before matching and re-added to the rewritten `.md` URL.
- 22aa026: Truncate OG image titles by code point instead of UTF-16 unit. A long title that was cut mid-emoji could leave a lone surrogate — a broken glyph — right before the ellipsis. Truncation now slices whole characters.
- 637e935: Fix two package-manager conversions in ` ```package-install ` blocks. A global uninstall (`npm uninstall -g eslint`) produced the invalid `yarn remove -g eslint` for Yarn Classic; it now emits `yarn global remove eslint`, mirroring the global-add handling. And `npm ci` was rewritten as the nonsensical `yarn run ci` / `pnpm run ci`; it now maps to each manager's frozen-lockfile install (`pnpm install --frozen-lockfile`, etc.).
- af71d2a: Only collapse a trailing `index` segment when deriving a custom page's route. A folder literally named `index` (e.g. `pages/index/foo.astro`) previously lost its segment and mapped to `/foo` instead of `/index/foo`, because every `index` part was stripped rather than just the filename.
- 48b2d55: Validate `--port` on `blume dev` and `blume preview`. A non-numeric value (`--port abc`) became `NaN`, which then flowed into `http://localhost:NaN` as the dev server's `deployment.site` fallback — corrupting canonical URLs, OG image links, and the sitemap fallback. An invalid or out-of-range port now errors out instead.
- eb56673: Don't let one missing file abort a whole remote Markdown source. The `files` mode fetched every file in a single `Promise.all`, so a single 404 (a page renamed or deleted upstream) rejected the batch and failed a cache-less build with none of the healthy pages imported. Failed files are now skipped with a per-file warning and the rest import; a source only hard-fails when _every_ file fails (so it can still fall back to cache or surface a real outage).
- 28b95cc: Stop Sanity documents with non-ASCII slugs from overwriting each other. When a `slug.current` (or configured slug field) slugified to an empty string — e.g. a CJK slug — the entry fell back to a constant `untitled.md`, so multiple such documents collided on one ref and all but the last were silently dropped. The fallback now uses the document's unique `_id`, matching the Notion source.
- 8fb4105: Keep angle-bracket type parameters inside inline code in the search index. When reducing Markdown to searchable text, the HTML/JSX strip ran before inline code was unwrapped, so `` `Array<Item>` `` indexed as just `Array` — searches for `Item`, `Response`, `u8`, and the like silently missed. Inline-code contents are now preserved through the HTML strip, in both the on-page and MCP/Ask AI indexes.
- 0a2e7ef: Stop the Algolia and Typesense search syncs from leaving stale records behind. Both previously upserted keyed on the route, so a page deleted or renamed between builds stayed in the hosted index forever and surfaced as a search result that 404s. Algolia now uses `replaceAllObjects` (an atomic full replace) and Typesense drops and recreates its collection each sync, matching the Orama Cloud sync's snapshot-and-replace behavior.
- 94aaee1: Escape and URL-encode routes in `sitemap.xml`. A route containing an `&` (e.g. a content file named `Tips & Tricks.md`) previously emitted an unescaped `&` in `<loc>`, which is not well-formed XML — strict parsers and Google Search Console reject the entire sitemap. Routes are now percent-encoded and XML-escaped, sharing the same escaper as the RSS feed.
- 352fc90: Harden the generated **Ask AI** endpoint. `POST /api/ask` now validates the request body — a malformed or non-JSON body, or a `messages` value that isn't a 1–40 item array within a size cap, returns a `400` instead of throwing an unhandled `500`. The model call is wrapped so a streaming error returns a `500` rather than crashing the request. The message caps also bound how much a single call to this unauthenticated endpoint can spend against your model; the docs now recommend fronting it with a rate limiter.
- 5637599: `blume init` now ensures `.blume/` and `dist/` are git-ignored. It creates a `.gitignore` if the project doesn't have one, and appends only the missing entries (trailing-slash agnostic) when it does, so re-running is a no-op.
- 3754281: Retry Notion API calls on rate limits instead of aborting the import. A large workspace fans out many concurrent block-children requests, so a single `429` would reject the batch and fail the whole Notion source. Requests now retry with `Retry-After`-aware exponential backoff before giving up.
- 670415c: Make `blume add`'s import rewriting statement-aware. It previously rewrote any `from "./…"` substring, so a relative specifier appearing inside a string or JSX text in a component could be mangled. Rewriting is now anchored to actual `import`/`export` statements at the start of a line (multiline import bodies still handled), leaving in-string and in-JSX text alone.
- 97ddec1: Add `<lastmod>` to `sitemap.xml`. Pages that carry a modified date (from git or frontmatter) now emit a W3C-format `<lastmod>`, giving crawlers a recrawl signal; pages without a date are left as a plain `<url>`.
- def7cd4: Warn when a GitHub remote source hits the tree-listing limit. The git-trees API caps very large repos and sets `truncated: true`, which was ignored — so a big repo would silently enumerate only part of its files with no indication. Blume now emits a `BLUME_SOURCE_TRUNCATED` warning when that happens.

## 0.3.0

### Minor Changes

- 5115383: `blume build` gains deployment override flags — `--output static|server`, `--adapter vercel|node|netlify|cloudflare`, and `--base <path>` — that override the corresponding `blume.config.ts` deployment fields for one build (handy for CI matrices and previews). `--analyze` prints a client-JavaScript bundle report (each `_astro/*.js` chunk largest-first, plus the total) so you can catch weight regressions without extra tooling.
- 6f20875: Complete the **component override API**. `defineComponents` now supports:

  - **An `islands` group** — register interactive framework components for use in every MDX page (the config-file equivalent of the `islands/` folder), hydrated by default (`client: "visible"`).
  - **Hydration on overrides** — any `mdx` or `layout` override can take a descriptor `{ component, client, media }` and hydrate with a real Astro `client:*` directive (`load`/`idle`/`visible`/`media`/`only`).
  - **Path-string references** — reference a component by path (`Footer: "./components/footer.astro"`) instead of importing it.
  - **A friendly diagnostic** — Blume warns at build time when an override points to a React/Vue/Svelte component with no hydration mode (so it would silently render as dead static HTML).

  Overrides are read by statically analyzing `components.ts` (never executing it), so Blume can emit the static imports and hydration wrappers Astro needs. Imported components still work as before; the new forms are additive.

- 968d449: Export per-component prop types from `blume/components`, so you can type an override or wrapper against the built-in's contract:

  ```tsx
  import type { CalloutProps } from "blume/components";
  ```

  Types are provided for the content components (`CalloutProps`, `CardProps`, `CardGroupProps`, `BadgeProps`, `TabsProps`, `TabProps`, `StepsProps`, `StepProps`, `AccordionProps`, `ColumnsProps`, `FrameProps`, `TooltipProps`, `IconProps`, and more). Each is derived from the component with Astro's `ComponentProps`, so it can never drift from the real props.

- a18b0e4: Static builds now emit platform redirect files so hosts issue real HTTP redirects instead of only Astro's client-side redirect pages: `_redirects` (Netlify, Cloudflare Pages), `vercel.json` (Vercel), and a structured `blume-redirects.json` manifest for manual wiring. A `_redirects`/`vercel.json` you ship in `public/` is preserved. Server/adapter builds are unchanged (the adapter handles redirects natively).
- e2f7d90: `blume dev` gains `--content-dir <dir>` (scan a different content folder without editing `blume.config.ts`, applied to the initial scan and every hot regenerate) and `--debug` (verbose Astro/Vite logging for troubleshooting).
- 7b8f026: Blume's own diagnostics (invalid config, frontmatter, or content errors) now show in the browser error overlay during `blume dev`, not just the terminal — each with its code, file/line, fix hint, and docs link. The overlay updates on every save and clears on the next successful reload.
- 316d862: Add `--json` to `blume validate` and `blume doctor`. With the flag, diagnostics are emitted as a JSON document on stdout — each with `code`, `severity`, `message`, root-relative `file`, `line`/`column`, and `docsUrl`, plus a severity summary — for CI pipelines and editor integrations. Human output is suppressed so stdout stays parseable.
- 5afd8dd: `blume init` gains starter and workflow flags:

  - `--template docs|api|sdk|changelog` — scaffold from a starter (an OpenAPI reference, an SDK layout, or a changelog with a first entry) instead of the plain docs seed.
  - `--package-manager npm|pnpm|yarn|bun` — tailor the printed next-steps.
  - `--eject` — scaffold and immediately eject to a standalone Astro project, or (when dependencies aren't installed yet) guide you to `blume eject` after install.

- 624d797: Expose the full set of overridable **layout slots**. Alongside the existing `Header`, `Sidebar`, `Breadcrumbs`, `TableOfContents`, and `Pagination`, you can now replace `Layout` (the whole page shell), `Logo`, `Search`, `MobileNav`, and the three content-injection slots `PageHeader`, `PageFooter`, and `Footer` — the last three have no built-in and render nothing until you set them. Each override receives the same props as the built-in it replaces. Register them the same way as before:

  ```ts
  import { defineComponents } from "blume";
  import Footer from "./components/Footer.astro";

  export default defineComponents({ layout: { Footer } });
  ```

- 40ed2b8: Render `navigation.selectors`. Configured selectors (Mintlify-style partition switchers — product, version, or any grouped destinations) now appear as zero-JS dropdowns in the header, highlighting the option that matches the current route. Each item supports a label, path, icon, description, and tag. Previously selectors validated and built into the graph but nothing displayed them.
- c94bcb9: Add performance-budget enforcement to `blume build`. `--budget-js <kb>` and `--budget-css <kb>` measure the total client `_astro/*.js` / `*.css` a build ships and fail (exit 1) when it exceeds the cap — turning a documented budget into a real CI gate. Pairs with `--analyze` (the per-file report).
- 2f1e33d: Prune the orphan `blume.config.ts` fields that validated but nothing read: `navbar`, `footer`, `icons`, `contextual`, and `styling` (Mintlify-compat leftovers). They no longer silently no-op — setting one is now a config error, so the surface reflects what Blume actually does. The Mintlify/Starlight migrators stop emitting them; per-partition `chromeVariants` keep only their `banner` override. (Site footers are available via the `Footer` layout slot, and code icons via `markdown.code.icons`.)
- 80cde3d: Add React island hooks, importable from `blume/hooks`:

  - `useBlume()` — the site `config` + `navigation`.
  - `usePage()` — the current page's `route` + `title`.
  - `useSearch()` — query the configured search provider (`search`, `results`, `loading`); the provider client loads lazily on first use.
  - `useAskAI()` — stream answers from the grounded Ask AI endpoint (`ask`, `messages`, `loading`, `reset`).

  Islands hydrate independently, so `useBlume`/`usePage` read a compact JSON snapshot the layout serializes into the page (emitted only when the project ships React, so static sites pay nothing). Custom pages built with `PageLayout` opt in by passing a `clientData` prop.

- 16a7a15: Ship every built-in content component through `blume add`, not just the five layout slots. `blume add callout`, `card`, `card-group`, `code-group`, `badge`, `steps`, `step`, `tabs`, `tab`, `accordion`, `accordion-item`, `columns`, `column`, `frame`, `expandable`, `panel`, `tooltip`, `tile`, and `prompt` copy the component into your project as editable source (imports rewritten to `blume/*`), then print the `defineComponents({ mdx })` snippet to wire it back. The page `feedback` rating is also available (`blume add feedback`) via a new `Feedback` layout slot.
- 408d4ef: Add `blume/runtime` data helpers for custom pages. `getBlumeCollection(data, query?)` selects content routes from `blume:data` — filtered by collection, locale, or path prefix, with drafts/hidden pages excluded and sorted by path — so building a custom index or listing is a one-liner. The new `<BlumePage>` component (`blume/components/BlumePage.astro`) renders a content entry's body inside a custom page with Blume's built-in MDX components already wired in, with `components` and `collection` props for the rest. The runtime data types (`BlumeData`, `BlumeRoute`, …) are re-exported from `blume/runtime` too.
- 944d2aa: Add a `toc` option to `blume.config.ts`. `toc: false` hides the on-this-page table of contents site-wide; `toc: { minHeadingLevel, maxHeadingLevel }` changes which heading levels it lists (default: H2–H3). Previously the range was hardcoded and the TOC couldn't be turned off from config.

### Patch Changes

- 9159662: Ground **Ask AI** in your docs. The `/api/ask` endpoint now retrieves the most relevant pages for each question — via the same lexical Orama index that powers search — and injects them into the model's system prompt, so answers stay tied to your content and cite the pages they draw from instead of relying on the model's own knowledge. The in-page island also forwards the current page, which is added to the context first and used to scope retrieval to that page's locale. Grounding turns on automatically with Ask AI for the gateway, OpenRouter, and OpenAI-compatible backends; **Inkeep** is left untouched since it runs its own retrieval. No new configuration or dependencies.
- 6d03896: Add a `blume check` command that type-checks the docs site with `astro check`. It regenerates the `.blume` runtime, syncs Astro's content types, then runs the checker against the project — using the project-root `tsconfig.json` when present so authored `pages/` are covered, not just the generated project. Exits non-zero on type errors, so it slots into CI as a `typecheck` script.
- 40c9256: Diagnostics now carry a `docsUrl` pointing at the page that explains them. Every mapped error/warning (config, frontmatter, meta, sources, links, deployment, …) prints a `docs: https://useblume.dev/docs/…` line, so a failing build links straight to the fix.
- 037191b: Config and frontmatter validation errors now point at a line and column, not just the file. `diagnosticsFromZod` locates the offending key in the source text (narrowing key-by-key so a nested field lands under its parent), so a bad `blume.config.ts` field or a mistyped frontmatter value reports e.g. `at content/docs/guide.mdx:4:3`.
- c0c998b: Warn early when an enabled feature needs a runtime secret that isn't set, so it surfaces at `blume dev`/`build` instead of failing at the first request in production. Covers Ask AI (`AI_GATEWAY_API_KEY`, or the provider's `apiKeyEnv`) and Mixedbread search (`MIXEDBREAD_API_KEY`). It's a warning, not a hard failure, since the value may live only in the deploy environment.
- afafbc1: Add an integration **fixture matrix** (`test/fixtures.test.ts`) that exercises whole projects through the core pipeline — nested navigation, broken links, invalid frontmatter (with line/column), a custom `.astro` page, a React island, and static-vs-server feature gating — so the pieces keep working together, not just in isolation.
- d81aebc: Add a dev-only hydration-mismatch hint. When React reports an island hydration mismatch, Blume follows it with a friendly pointer explaining the usual causes (non-serializable props, non-deterministic render) and linking to the islands guide. It's guarded by `import.meta.env.DEV`, so it's tree-shaken out of production builds.
- 085ed4d: Unexpected (non-`BlumeError`) failures now print a stable internal-error report — a fixed `BLUME_INTERNAL` code, the message, a trimmed stack, and an environment dump (Blume/Node/platform) with a link to file an issue — instead of a bare stack trace. Wired into `prepare`, `validate`, and `doctor`, plus a top-level backstop for async failures that escape a command (e.g. in `blume dev`).
- 18fb645: Catch unknown MDX components with a friendly warning before the build hits Astro's cryptic "Expected component X to be defined" error. When an `.mdx` page uses a `<Tag>` that isn't a built-in, an island, or a `components.ts` override, Blume warns with the page it's on and how to fix it — `blume add <name>` when a registry item matches, otherwise how to register or add it. Code blocks, inline code, and quoted text are ignored to avoid false positives.
- 7ae1937: Blume now warns when a navigation icon name (in `blume.config.ts`, folder meta, or a page's `sidebar.icon`) isn't in its icon set — a typo used to just render nothing. Image paths, URLs, and inline SVG icons are left alone. Surfaced by `blume dev`, `blume build`, and `blume doctor`.
- 7a8cd85: Blume now catches common navigation mistakes that used to fail silently:

  - **Missing target** — a tab/selector pointing at a route no page (content, custom `.astro`, or generated) serves.
  - **Duplicate labels** — two sidebar entries sharing a title at the same level.
  - **Hidden-in-sidebar** — a page marked `sidebar.hidden` that still appears in the sidebar (and therefore its prev/next pagination).

  Surfaced by `blume dev`, `blume build`, and `blume doctor`.

- f583299: Support custom `og:image` overrides on custom pages. `PageLayout`'s `ogImage` prop now resolves a root-relative path (a file in `public/`) against `deployment.site` to the absolute URL crawlers require; absolute URLs pass through unchanged. This lets a marketing home or landing page set a bespoke social image instead of the generated Open Graph card.
- 307e156: Add a Playwright end-to-end harness for the docs site (a real Blume project, so it doubles as the framework's browser coverage). `playwright.config.ts` builds and previews the site, and `e2e/site.spec.ts` drives navigation, the sidebar, theme toggle, the mobile drawer, the search dialog, code-copy, tabs, and a custom page. Run with `bun run test:e2e` (after `bunx playwright install`).
- e18dcc8: Redesign the generated Open Graph card. It now uses a light layout with a brand lockup (the configured `logo` SVG, painted to the foreground, or an accent tile with the site initial as a fallback), the page title as a balanced headline, the site description as a muted subtitle, and a footer showing the repository slug and site host. Titles and descriptions use `text-wrap: balance`.
- e66583c: Error reports now relativize `.blume/` stack frames. A frame pointing into the hidden generated runtime is shortened from its machine-absolute path to a project-relative `.blume/…` path tagged `(generated)`, so internal-error stacks stay readable and the user-source frames (custom pages, island/override wrappers, which keep their real paths) stand out.
- d59a1b0: Add accessibility and visual-regression coverage to the Playwright suite. `e2e/a11y.spec.ts` runs axe-core (WCAG 2 A/AA) on the home, docs index, and a content page, checks the skip link is first in the tab order, verifies dark-mode color contrast, and renders under reduced motion. `e2e/visual.spec.ts` captures light/dark screenshot baselines for regression diffing.

## 0.2.0

### Minor Changes

- 7a30708: Add a built-in `github-releases` content source that turns a repository's GitHub Releases into `type: changelog` entries, so your release notes become your changelog with no files to maintain. The generated `/changelog` timeline now also reads staged (non-filesystem) sources, and the CLI loads `.env`/`.env.local` (cascading to the repo root) before the content scan so remote sources can read tokens like `GITHUB_TOKEN`. Because a changelog is supplementary, a fetch failure with no cache (e.g. a CI build without a token) degrades to an empty timeline with a warning instead of failing the build, and the `/changelog` page is still generated so its nav tab resolves.

## 0.1.5

### Patch Changes

- 6eb10b8: Hide tab-owned groups from the root sidebar. On a route under no tab (or the root `/` tab), the sidebar showed every top-level group — including the folders that already have their own header tab — so a section like Adapters or API appeared both as a tab and as a sidebar group. Those tab-owned groups are now dropped from the un-scoped sidebar, leaving only the pages that don't belong to a tab (and any group emptied by this is dropped too). If hiding them would blank the sidebar, the full tree is shown, so a route is never left empty.

## 0.1.4

### Patch Changes

- a41a9d7: Insulate the `<Component>` live preview from the page's prose styles. The preview renders inside the content's `.prose` wrapper, so Tailwind Typography bled into the previewed component (heading sizes, link colors, list markers, paragraph spacing), making it look unlike its real rendering. The Preview pane now carries `not-prose`; the Code pane keeps prose so the highlighted source stays styled.
- a1155c4: Add a default 404 page. Blume now generates a not-found page at Astro's reserved `src/pages/404.astro` path, so static builds ship a `dist/404.html` and `blume dev` serves it for unmatched routes — previously an unknown URL fell back to Astro's unstyled default. The page renders through `PageLayout` (header + search, no sidebar), is centered and `noindex`, and its copy comes from new translatable `notFound` UI strings (`title`, `description`, `home`), overridable per locale via `i18n.ui`. Drop a `pages/404.astro` to replace it entirely: Blume skips the default when the project already owns `/404` (a custom page or a `404.md` content page), so the override never collides. The same default is written on `blume eject`.
- 875eac0: Navigation tabs now scope the sidebar to their section. Previously `navigation.tabs` rendered as header links but every page still showed one global sidebar; the `sidebarVariants` data the model carried was never consumed at render time. Now, when the current route falls under a tab's `path`, the sidebar shows only that tab's section (the folder at that path) — so a multi-section site (e.g. Adapters / API / AI tabs) drills each tab into its own pages, the way Fumadocs' root folders do. It needs no extra config beyond the tabs: each group carries its URL path, and the renderer picks the section matching the route, falling back to the full sidebar when no tab matches. Breadcrumbs and pagination follow the scoped tree.

## 0.1.3

### Patch Changes

- 46f539c: Let `<Component>`'s `examples` config be a glob, not just a directory. When it contains glob magic (`*`, `?`, `[]`, `{}`, or `!`), only matching files are discovered and a `<Component path>` key is relative to the glob's static prefix. This lets a shadcn-style registry that colocates each component's source (named exports, no default) with its example (default export) be targeted directly — e.g. `examples: "registry/<pkg>/**/examples/*"` previews just the examples instead of sweeping in the sources and failing the build with `"default" is not exported`. Also makes `blume eject` honor the configured `examples` directory, which it previously ignored.
- 84ef03c: Stop the search preflight from falsely warning `Search provider "orama" needs "@orama/orama", which isn't installed` on a successful build. The check resolved the provider SDK from the project root only, so under isolated linkers (Bun's `isolated` mode, pnpm) a SDK Blume ships — Orama, the default provider — looked missing even though the index built fine via the `.blume` deps link. It now also resolves from Blume's own package (the same dependency set the build uses), so a shipped SDK is recognized; a genuinely uninstalled peer (Algolia, Typesense, …) still warns.

## 0.1.2

### Patch Changes

- b6a2506: Surface a clear, actionable diagnostic for the split-layout Astro conflict that a symlink can't repair. When a hoisted install pulls a second Astro to the project root (e.g. a dependency with a type-only `astro@6`) that shadows Blume's, and `@astrojs/mdx` is hoisted away from Blume's own Astro, `ensureDepsLink` can't reconcile the split with one symlink and leaves it for a root `overrides`/`resolutions` pin. Previously it did so silently, and the build later crashed deep in Astro on a missing export (e.g. `chunkToString`) with no hint at the cause. `blume dev`/`build` now warn up front — naming the conflicting versions and telling you to pin Blume's Astro with a package.json `overrides` (npm/bun/pnpm) or `resolutions` (yarn) entry — and the warning clears itself once the pin is in place.
- 40c7bd7: Make `<Component>`'s examples directory configurable. `<Component path>` previously only resolved live previews (and their source) from a top-level `examples/` directory, so projects whose examples live elsewhere — e.g. a registry layout like `registry/<pkg>/…`, which also doubles as the shadcn payload — couldn't adopt it. Set `examples` in `blume.config.ts` to point at any directory under the project root (default `"examples"`); a `<Component path>` key is then relative to that directory. For example, with `examples: "registry/files-sdk"`, a file at `registry/files-sdk/file-list/basic.tsx` is `<Component path="file-list/basic" />`.
- b6a2506: Fix `blume build` failing under isolated package-manager linkers (Bun's `isolated` mode, pnpm) with `Cannot find package 'zod'` (or `shiki`, `sharp`, `@takumi-rs/core`, …) during static page generation. Astro's static build emits a self-contained SSR bundle to `dist/.prerender/` and runs it to render the HTML; that bundle leaves Blume's render-time dependencies external, so Node resolves them by walking up from `dist/.prerender/`. The earlier dependency-link fix only repaired resolution rooted at `.blume/`, and `dist/` is a separate tree an isolated linker never hoists Blume's deps into, so prerendering died. Blume now drops the same `node_modules` symlink beside the prerender bundle (removed again with `dist/.prerender/` once generation finishes, so nothing leaks into your published output), and forces Blume's render-time deps external on both build environments so an isolated linker doesn't bundle a symlinked store copy and strand one of its own transitive dependencies (e.g. `batchwork` via `@astrojs/markdown-satteri`) as an unresolvable import.

## 0.1.1

### Patch Changes

- 52fdcb4: Auto-detect an Apple touch icon by filename, the way favicons already work. Drop an `apple-icon.png` (or `.jpg`/`.jpeg`, or `apple-touch-icon.png`) in your project root or `public/` directory and Blume wires up `<link rel="apple-touch-icon">` for you — no config required. A file in `public/` is referenced by URL (the reliable path for iOS); there's no default, so no tag is emitted when the project ships none.
- ac174bd: Document and type the `blume:data` module that custom pages import. Export `BlumeData` (and its parts — `BlumeDataConfig`, `BlumeRoute`, `BlumeFeed`, `BlumeLogo`, `BlumeFavicon`, `BlumeBanner`, `BlumeDataI18n`, `UIStrings`) from `blume`, so a custom `.astro` page can `import type { BlumeData } from "blume"` instead of reading the generator to learn the shape. The generated runtime now declares `blume:data` with that type, and `buildRuntimeData` is annotated with it so the exported type and the emitted JSON can't drift. The custom-pages guide's data table is expanded to the full surface — `config` (now listing favicon/appleIcon/banner/theme/site/repoUrl/search/i18n/mcp/og/analytics/...), plus `navigation`, `navigationByLocale`, `routes`, `feeds`, `fontCssVars`, `ui`, and `uiByLocale`.
- 2a3acb7: Add a `CodeBlock` component and a `highlightCode` helper for themed code outside the Markdown pipeline. There was no way to highlight a string with Blume's configured Shiki theme except by writing a fenced code block, so showing code on a landing page or inside a custom component meant pulling in raw Shiki and hand-writing a `[data-theme="dark"]` swap. `CodeBlock` (usable in any MDX page, or imported from `blume/components/content/CodeBlock.astro`) renders a `code` string with the same themes, transformers, and light/dark swap as fenced code — `<CodeBlock lang="ts" code={source} />`. The underlying `highlightCode(code, lang)` is exported from `blume/markdown` for rendering to an HTML string directly. The `<Component>` source view now shares the same helper.
- fe75624: Add `<Component>` — render an example file from your project's `examples/` directory as a live, hydrated preview alongside its highlighted source, in tabs. Point it at a file with `<Component path="forms/login" />` (the path under `examples/`, without the extension); React, Vue, Svelte, and Astro examples are all supported.
- fe75624: Add `<Diff>` — render a git-style diff with `@pierre/diffs`, highlighted with the same Shiki theme as your code blocks and produced entirely at build time (no client JavaScript). Accepts two inline strings (`old`/`new`), two file paths (`before`/`after`), or a unified patch (an inline `patch` string or a `src` file).
- 0a147fb: Fix the Fumadocs `meta.json` → sidebar migration for the common flat-files-plus-separators layout. The Extract operator (`...folder`) is no longer kept as a literal `"...folder"` page slug; it now keeps the folder's place in the ordering and renders as a normal group. `---Section---` separators, which were previously dropped with a warning, are rebuilt as route-transparent Blume group folders: a section's flat pages move into a `(Section)/` folder (with a `meta.ts` preserving their order), a section that is a single folder is left in place, and links are reported for manual navbar placement. Routes are unchanged and per-folder `meta.ts` keeps working, since the migration reshapes the filesystem rather than emitting a global `navigation.sidebar` override.
- 6501d73: Repair `blume dev`/`build` when a hoisted install resolves the _wrong_ Astro. Previously `ensureDepsLink` only relinked Blume's deps when Astro was unresolvable from `.blume/` (isolated linkers, pnpm); if a sibling workspace pinned an older major (e.g. `astro@6` for a type-only import) and the package manager hoisted it to the project root, `.blume/` resolved that shadowing copy, `@astrojs/mdx@7` bound to it, and the build crashed on a missing export. The link decision now compares _which_ Astro resolves — Blume's own versus a shadowing one — and links Blume's dependency directory in whenever they differ, not just when Astro is missing. This only happens when Blume's deps are a co-located, consistent set (Astro beside the `@astrojs/mdx` that binds to it); a split layout, where the integration is hoisted away from a conflicting Astro, can't be fixed by one symlink and still needs a root `overrides`/`resolutions` pin, so it's left untouched rather than half-fixed.
- 49be339: Fix `blume dev`/`build` failing to resolve Astro and its integrations under isolated package-manager linkers (Bun's `isolated` mode, pnpm), which forced projects to redeclare Blume's dependencies by hand. The generated `.blume/` runtime now locates Blume's real dependency directory — whether nested under the package or installed as siblings in a virtual store — and symlinks it in, so the generated config's bare specifiers resolve without the project adding any deps. Stale or broken `.blume/node_modules` links are also detected and rebuilt.
- 6a06d82: Finish the Fumadocs migration teardown so the project builds as Blume without manual cleanup. After moving content and writing the config, `blume migrate fumadocs` now repoints the `dev`/`build`/`start` scripts at the Blume CLI (`blume dev`/`build`/`preview`) and drops the `fumadocs-mdx` postinstall, adds `.blume/` and `dist/` to `.gitignore`, and prints a "safe to delete" checklist of the leftover Next/Fumadocs files it found (`next.config.*`, `source.config.*`, `mdx-components.tsx`, `app/`, …) plus a reminder to remove the `next` tsconfig plugin and the `.next`/`.source` ignore lines. It also derives a better site title for monorepos: a generic package name like `web` (from `apps/web`) now falls back to the repository's directory name. (The script-rewrite, gitignore, and leftover-checklist helpers live in the shared migration toolkit for other migrators to adopt.)
- e6914c0: Generate Open Graph cards for custom pages, including the home. OG images were generated per content route only, so a custom landing page at `/` — the most-shared URL — got no `/og/index.png` and had to ship a static `public/og.png`. Blume now renders a card for every static, public custom page (skipping dynamic `[param]` routes and private `_partial`/`.well-known` segments): the home uses the site title with the site description as its eyebrow, and a deeper page is titled from its last path segment. `PageLayout` derives the page's `canonical` and `og:image` from `siteUrl` + `ogEnabled` automatically (explicit `ogImage`/`canonical` still override), so a custom page wired from `blume:data` gets a themed card with no extra work.
- e22d957: Add `PageLayout` for landing, marketing, and other full-width pages. `RootLayout` hard-codes the docs 3-column grid (sidebar + prose + TOC), so building a custom page like a landing page meant hand-rolling the entire document shell — re-importing the header/favicon/fonts, copying the theme + banner pre-paint scripts, wiring `fontCssVars`, and rebuilding the banner markup. `PageLayout` (import from `blume/components/layout/PageLayout.astro`) provides that shell — `<head>`, theme, fonts, favicon, banner, and header — then a single full-width `<slot />` for the body, plus an optional `footer` slot rendered after `<main>`. Props come straight from `blume:data`. The two layouts now share the theme/banner pre-paint scripts so they can't drift, and the bundled docs landing page is built on `PageLayout`.
- 9434520: Ship compiled `.d.ts` declarations for the public API so you can type-check your own Blume project. Previously the `blume` and `blume/schema` exports pointed straight at `src/*.ts`, so the moment a consumer's `tsc`/`tsgo` touched a file importing `blume` (`blume.config.ts`, every `meta.ts`, `components.ts`) it followed into Blume's source and surfaced errors it couldn't resolve — `.ts` import extensions (TS5097), `node:fs`, and migrator internals — forcing you to exclude your own config from type-checking. The build now emits declarations to `dist/types/`, and the exports map resolves the `types` condition to them while the runtime still resolves to source. `defineConfig`/`defineMeta` now type-check and autocomplete in editors without the source leaking.
- 482bd71: Wire the project's tsconfig `paths` into the generated runtime's Vite aliases, so `@/`-style imports resolve in `blume dev`/`build`. The generated `.blume/` is its own Astro project with its own tsconfig and never inherited the project's, so shadcn-style imports like `@/lib/utils` in custom pages, islands, and components failed to resolve and had to be rewritten to relative paths. Blume now reads `compilerOptions.paths` (and `baseUrl`) from the project's `tsconfig.json`/`jsconfig.json` — tolerating JSONC and following a relative `extends` to the file that declares them — and emits each mapping as a `resolve.alias` entry (longest prefix first), so those components port over unchanged. Reading is best-effort: an unparseable or alias-less config simply yields no aliases.
- 83aab31: Fix `blume validate` false-flagging valid heading anchors. Heading anchor ids were derived for the manifest with a hand-rolled slugifier that collapsed consecutive dashes (`--` → `-`) and didn't disambiguate repeated headings, while the renderer assigns ids with `github-slugger`. So a link like `/api/copy#the-read--write-fallback` (matching the real rendered id) was reported broken, and a link to a repeated heading's `#setup-1` had no match. Heading extraction now uses the same per-document `github-slugger` as the renderer, so the manifest's anchor ids — and the on-page table-of-contents links built from them — match the rendered heading ids exactly. (`slugify` still handles content/route slugs.)
- f412250: `blume validate` now treats configured redirects as valid link targets. A content link to a path that only exists as a `redirects` entry (e.g. `/providers`, which redirects to `/providers/openai`) was flagged as a broken link, even though it resolves at runtime. Link validation now accepts any link whose target matches a configured `redirect.from`, removing the false positive.
- 5e35945: Fix `blume dev`/`build` crashing with "Function yaml.safeLoad is removed in js-yaml 4" when a workspace resolves js-yaml 4 for gray-matter. Front-matter parsing now routes through an explicit js-yaml `load`/`dump` engine instead of gray-matter's removed `safeLoad` default.

## 0.1.0

### Patch Changes

- 2aa1da0: First alpha release (v0.0.1) for testing.
