---
changelog:
  category: Release
  version: blume@2.2.0
date: '2026-10-07T06:08:31Z'
seo:
  description: >-
    blume migrate now moves sites from VitePress, VuePress, Docus, MkDocs
    (including Material for MkDocs and Zensical projects), mdBook, Fern,
    GitBook, Redocly…
title: blume@2.2.0
type: changelog
---
## Minor Changes

- 3ba8dc7: `blume migrate` now moves sites from VitePress, VuePress, Docus, MkDocs (including Material for MkDocs and Zensical projects), mdBook, Fern, GitBook, Redocly, ReadMe, Docsify, Jekyll (Just the Docs), and GitHub wikis. Name the source (`npx blume migrate gitbook --claude`) or let Blume detect it from the project's files; a GitHub wiki is always named. The bundled `blume-migrate` skill has a mapping reference for each one, plus codemods for VitePress, Docus, MkDocs, Fern, ReadMe, Docsify, Jekyll, and GitHub wikis that make the mechanical rewrites before the agent starts. Three helper scripts ship with it: `operation-routes.mjs` maps each OpenAPI endpoint to its Blume route for redirects, `pin-heading-ids.mjs` keeps a migrated site's old heading anchors working, and `include-excerpts.mjs` generates the partial-file code excerpts (mdBook anchors, VitePress regions, line ranges) that `<include>` can't select.
- 0c4eed3: Blume now requires Node.js 22.19 or newer, up from 22.12. Blume uses `undici` to send remote API spec fetches through `HTTP_PROXY` or `HTTPS_PROXY` when either is set. That dependency is now undici 8, which requires Node.js 22.19 or newer. `blume doctor` warns when the running Node.js is older than that.
- be943aa: Add a `<Changelog />` component, so a hand-written `/changelog` page can put an introduction or a feed link above the generated release list instead of rebuilding the list by hand. Place the tag in `changelog/index.mdx` with `mode: center` to match the generated page's layout. It lists the same releases the generated page does, GitHub Releases entries included, and downlevels to that list in `/changelog.md`, llms-full.txt, MCP `get_page`, and search. The generated page now renders the same component.

## Patch Changes

- d68591a: Headings in an API spec's descriptions no longer add `<h1>`s to reference pages. A spec's `info.description` is often a document of its own under `# Introduction` and `# Authentication` headings, so the overview page had several `<h1>`s besides its title. Headings in `info.description` and in an operation's description now move one level down (`#` renders as `<h2>`), and headings in a tag's description two (`#` renders as `<h3>`, under the tag's section).
- ca9dc01: An API reference operation without a `summary` is now labeled in the sidebar by its method and path (`GET /pets`), as its page is already titled. Before, the label was the path alone, so `GET /pets` and `POST /pets` shared one, and an unedited spec failed `blume validate --strict` with `BLUME_NAV_DUPLICATE_LABEL`. AsyncAPI operations without a title or summary get their action and channel (`SEND user/signup`) the same way.
- ca9dc01: An API reference operation's URL now only has to be unique within its tag. Before, an operation whose slug another tag's operation already used got its method added to the URL, so a `list` operation under a second tag lived at `/reference/stores/list-get`; it now lives at `/reference/stores/list`. Two operations with the same slug in one tag still get the method added to the second. The old URLs keep working: Blume redirects each one (and its `.md` and `.mdx` copies) to the new one with a 301, unless a page or a redirect you configured is already at that URL. `<Operation id>` values don't change. In a `graphql()` reference, a type page whose name matches a root field moves the same way (`/graphql/objects/pet-object` to `/graphql/objects/pet`).
- ca9dc01: API reference operations now follow the spec's order in both the sidebar and the overview page: paths in the order the spec lists them, and each path's methods in the order they're written. Before, the sidebar sorted a tag's operations alphabetically by label, and the overview listed each path's methods in a fixed order (GET before POST, and so on). A `meta.ts` in a tag's folder that sets `pages` still decides the sidebar order.
- ca9dc01: API reference tag folders now split camelCase and PascalCase tag names into words, the way operation slugs already did: the tag `InboxesThreads` is served at `/reference/inboxes-threads/…` instead of `/reference/inboxesthreads/…`. The old URLs keep working: Blume redirects each one (and its `.md` and `.mdx` copies) to the page's new URL with a 301, unless a page or a redirect you configured is already at that URL. This applies to `openapi()`, `asyncapi()` (including untagged operations grouped by channel address), and `graphql()` references.
- 6db6cdd: `blume audit --only` and `--skip` now narrow the counts the report prints to the checks they leave in: the audit count in the summary, the `audits` count in `--json`, and the count beside each skipped tier. Before, `blume audit --only redirects` filtered the findings but still printed the counts for every check, so the filter looked ignored. A skipped tier the filter leaves no checks in isn't listed.
- 9b13c53: `blume audit` now checks where a pattern redirect sends its bare path. A pattern ending in `/*` or `/:name*` also matches the path without that segment: `/mcp/*` → `/user-api/*` sends `/mcp` to `/user-api`, and every host gets that rule. When `/user-api` isn't a page, the audit now reports `BLUME_AUDIT_REDIRECT_BROKEN` for the pattern and names `/mcp`. A pattern that sends its bare path to itself, like `/a/*` → `/a`, is reported as `BLUME_AUDIT_REDIRECT_LOOP`. The audit also stops following a redirect chain that grows a segment at each hop, which used to keep it running forever, and reports the chain as a loop.
- caf4d21: A `meta.ts` under a content root outside the project folder, like `content: { root: "../docs" }` or a sibling package in a monorepo, can now `import { defineMeta } from "blume"`. Before, Blume looked for `blume` from the meta file's own folder upward. A project that installs `blume` in its own `node_modules` failed every such file with `BLUME_META_LOAD_FAILED: Cannot find module 'blume'`. Blume now resolves `blume` and its subpaths (`blume/sources`, `blume/schema`, …) to the running Blume package wherever the file lives. The same fix applies to files that `blume.config.ts` imports from outside the project.
- 000f0ac: `blume validate` now says why a link to a partial is broken. A file whose name, or a folder's, starts with `_` isn't published, so a link like `[setup](./_setup.md)` reported only that no page resolves to `/_setup`, although the file is right there. The `BLUME_BROKEN_LINK` fix now names the file as a partial and suggests splicing it in with `<include>`, renaming it, or adding `"!**/_*"` to `content.exclude`.
- c2e2a02: `<Card img="./cover.png">` now finds an image next to the page, like `![](./cover.png)` does. The card rendered `img` as written, so the browser resolved a relative path against the page's URL and the image 404'd, while `blume validate` never checked it. A card's relative `img` is now published with the page and points at its served copy, a card in an included partial resolves its image next to the partial, and the raw `.mdx` copy agents read points at the same URL. `blume validate` checks `img` like any image embed and reports a missing file as `BLUME_BROKEN_ASSET`.
- 07ac505: A `<CodeBlock>` with no `code` prop, like a fenced block wrapped in `<CodeBlock>…</CodeBlock>` the way Fern and Mintlify write code, no longer fails the build with a bare `TypeError` that named no file or line. Without `code`, `CodeBlock` now renders its children as written, so the wrapped fence shows as it would on its own, and the page's Markdown copy and `llms-full.txt` unwrap it the same way.
- 1633bf9: Code blocks support `// [!code error]` and `// [!code warning]` notation comments, which tint a line red or amber. Before, those comments stayed in the code as written. Like the other notations, the comment is removed from the rendered block, and the new `--blume-code-error` and `--blume-code-warning` tokens (each with a `-border` partner) restyle the lines.
- 8b05bef: `blume dev`, `blume build`, and `blume check` warn about code fences that don't render as written. A language Shiki doesn't know, like ` ```requirements `, rendered as plain text with only a `[Shiki]` console line that named no page; it now warns as `BLUME_UNKNOWN_CODE_LANGUAGE` with the file and line. A fence option from another docs tool did nothing without a word: MkDocs' `hl_lines="2 3"` and `linenums="1"`, `showLineNumbers`, Mintlify's `lines`, Fern's `wordWrap`, and `filename="…"`. Each now warns as `BLUME_CODE_FENCE_OPTION` and gives the Blume spelling (`{2,3}`, `lineNumbers`, `wrap`, `title="…"`). On a Rust block, rustdoc's and mdBook's `ignore`, `no_run`, `should_panic`, `compile_fail`, `edition2015` through `edition2024`, `noplayground`, and `editable` warn the same way, with a note to remove them, since Blume never compiles, tests, or runs code. None of these words becomes part of the block's title, so ` ```js app.js showLineNumbers ` is titled `app.js`.
- 3f5ae80: A code block's title is every word after its language. Before, only the first word counted, so ` ```javascript Install the client ` was titled "Install" and the rest of the line was dropped. Keywords like `lineNumbers`, line ranges, and `key="value"` options still stay out of the title. A title in brackets, like ` ```ts [file.ts] ` from Docus or a VitePress code group, now shows without its brackets, and a line range written against them, as in Docus' ` ```ts [file.ts]{2} `, highlights its lines and stays out of the title.
- bb14a3a: An untitled code block in a `<CodeGroup>` is labeled by its language, like "Python" or "TypeScript", instead of "Tab 1", "Tab 2". Groups still sync by label, so picking "Python" in one group picks it in the others. Code block headers name more languages the same way, like "Python", "Go", and "Rust" in place of their lowercase ids.
- 1e1bb42: `openapi()` and `graphql()` now warn about `codeSamples` ids Blume doesn't generate a sample for, instead of leaving them out of every operation page without a sign. `BLUME_OPENAPI_UNKNOWN_CODE_SAMPLE` (`BLUME_GRAPHQL_UNKNOWN_CODE_SAMPLE` for GraphQL) names the ids and lists the ones Blume accepts. ReadMe's `cplusplus` is now accepted as an alias for `cpp`. Its `objectivec` has no generated sample, so it warns.
- 194787d: The copy button on a ` ```console ` or ` ```shellsession ` block copies only the commands, without their prompts or output. Before, copying `$ npm i blume` and the lines it printed put all of it on the clipboard, so pasting it into a terminal ran `$` and the output as commands. A command ending in `\` keeps its continuation lines, and a block with no prompt copies as written.
- a81a303: `content.exclude` (and a `filesystem()` source's `exclude`) now adds to the default `["**/_*", "**/.*"]` instead of replacing it. Before, setting `exclude: ["drafts/**"]` silently published every `_`-prefixed partial and dot-file as a page. To publish `_`-prefixed or dot-files on purpose, list the default you want dropped with a `!`: `exclude: ["!**/_*"]` publishes underscore files, and `"!**/.*"` dot-files. A config that already lists the two defaults beside its own patterns works as before.
- 714fafc: A file or folder named with a day- or month-first date, or a year and month, now keeps its whole name in its URL. Blume read the first number as an ordering prefix, so `changelog/12-05-2022.mdx` published at `/changelog/05-2022` and `changelog/2024-01.mdx` at `/changelog/01`, with no warning, and two files that differed only in that number collided on one route. Now `12-05-2022` (`D-M-YYYY` or `M-D-YYYY`) and `2024-01` (`YYYY-MM`) stay whole, like an ISO date (`2024-01-05`) already did, so those pages publish at `/changelog/12-05-2022` and `/changelog/2024-01`. Each old URL redirects to the new one with a 301, its Markdown copies included, unless another page now lives there or a configured redirect already starts there.
- d136cd5: `blume dev`, `blume build`, and `blume check` warn when a `:::` container's closing line has text after its colons. Inside a `:::warning`, a `::: card` line closes the callout, so `card` was dropped from the page and the text after it fell outside the callout, with no warning. `BLUME_DIRECTIVE_CLOSING_TEXT` names the file and line and suggests a longer outer fence (`::::warning` … `::::`) to keep the line inside the callout. A callout opener written with a space, like `::: tip` from VitePress, VuePress, or Docusaurus v2, isn't a directive and shows as text. Blume now warns about it as `BLUME_DIRECTIVE_SPACED_NAME` and gives the unspaced spelling, with a title moved into brackets (`:::tip[Title]`).
- 3f8beb7: `blume dev`, `blume build`, and `blume check` warn when a callout opener has text after its name. `:::tip Some title` opens a `tip` callout but drops `Some title`, so the callout rendered untitled with no warning. `BLUME_DIRECTIVE_OPENING_TEXT` names the file and line and gives the bracketed spelling that keeps the title, `:::tip[Some title]`.
- 8b18782: A backslash-escaped `<` in an `.mdx` page no longer counts as a component tag. Text like `Promise\<App>` or `\<Not set>` renders as literal text, but `blume dev` and `blume build` still warned that `<App>` or `<Not>` isn't a known component (`BLUME_UNKNOWN_COMPONENT`), and an escaped `\<Component path="…">` was reported as a missing example. `blume validate` likewise no longer checks the link in an escaped `\<a href="…">` or `\<Card href="…">`, or accepts a fragment that only an escaped `\<a id="…">` provides. Real tags are still caught, including one after an escaped backslash (`\\<Foo />`).
- ca181e4: Code fence languages are no longer case-sensitive: ` ```JSON ` and ` ```Dockerfile ` highlight like ` ```json ` and ` ```dockerfile `, where before they fell back to plain text with only a console line. A line range written against the language, like ` ```js{2} ` (the VitePress and VuePress spelling), now highlights its lines and keeps the language, instead of the whole block falling back to plain text.
- f6f9bfe: A relative link to a file next to a page now works. `[the spec](./spec.pdf)`, a reference definition like `[spec]: ./spec.pdf`, and an HTML element naming such a file (the `src` of an `<img>`, `<video>`, `<source>`, or `<audio>`, and the `href` of an `<a>` or a component, as raw HTML in `.md` or an element in `.mdx`) all shipped as written, so the browser resolved them against the page's URL and the file, never published, 404'd. The file is now published with the page and the built link points at that copy, keeping a suffix like `#page=2`, in the page's Markdown copy and `llms-full.txt` too. A partial's link moves with it when the partial is included from another folder. `blume validate` accepts these links, and no longer warns that an `<img src>` naming a file beside the page isn't published.
- 059c83f: `blume dev`, `blume build`, and `blume doctor` warn about folder meta that does nothing. A `meta.ts` `pages` entry that names no page or folder in its group was ignored without a word; `BLUME_META_UNKNOWN_PAGE` now names the file, the entry, and its line, and suggests the slug it most likely means (`"quickstart"` for `"01-quickstart.mdx"`, or a near miss). A `meta.ts` outside every `include` glob of its content source is never read, which left a reference's tag-folder `meta.ts` silently ignored on a site whose `include` lists only its own folders. When such a file sits in a sidebar group's folder, `BLUME_META_OUTSIDE_INCLUDE` now says so and suggests an `include` glob that reaches it.
- b04f1b7: A `meta.ts` in an OpenAPI, AsyncAPI, or GraphQL reference's tag folder now changes only the fields it sets. Before, it replaced the group's generated meta entirely, so a `meta.ts` that only renamed a tag group also dropped the group's place in the spec's tag order, and the group moved to the end of the sidebar. The generated title and order now fill in whatever the file leaves out, for `meta.ts` and `meta.$.ts` alike.
- 983ac72: GitHub alerts render as callouts in `.mdx`. A quote that opens with `[!NOTE]`, `[!TIP]`, `[!IMPORTANT]`, `[!WARNING]`, or `[!CAUTION]` becomes a note, tip, note, warning, or danger callout, with any text after the marker as its title; before, it rendered as a plain quote that showed the marker as text. In an `.md` page, which renders no components, the quote stays a quote, and `blume dev`, `blume build`, and `blume check` now warn about it as `BLUME_MD_GITHUB_ALERT` so you can rename the page to `.mdx`.
  
  Callout and accordion titles keep their inline formatting. A ``:::note[Use **bold** and `code`]`` title, a `<Callout title>`, and an `<AccordionItem>` or `<Expandable>` `title` now render their code spans, emphasis, and links instead of flattening them to plain text or showing the Markdown as written. A title without any of that renders exactly as before, and an accordion keeps the id its title always gave it.
- f50c9c6: Heading anchors no longer pick up stray dashes or badge text. A heading that ends or starts with a component, an image, or raw HTML slugged the space beside it into its id (`## Maintainers <Badge/>` anchored as `#maintainers-`), and one ending in a symbol after a space kept a trailing dash (`## Features ✨` as `#features-`). Those ids now drop the dash: `#maintainers`, `#features`. A `<Badge>`'s text is no longer part of a heading's id or its table of contents entry, so `## Install <Badge>beta</Badge>` anchors as `#install` instead of `#install-beta` and lists as "Install". A fragment can't be redirected, so links to the old anchors land at the top of the page; `blume validate` reports the ones in your own pages.
  
  A heading that holds its own empty anchor, like GitBook's `## Title <a href="#x" id="x"></a>` or a wiki's `<a name="x"></a>`, now takes that anchor's `id` (or `name`) as its id and keeps a single self-link. Before, the anchor nested inside the heading's link in `.md` (invalid HTML, with an id ending in a dash) and stopped the self-link from rendering in `.mdx`. A raw `<a>` link in an `.md` heading no longer gets the self-link wrapped around it either.
  
  The spaced `{ #id }` and kramdown `{: #id }` heading markers that MkDocs writes, and an attribute list that sets the id beside classes or attributes (`{ #id .wide }`), stay in the heading's text in `.md`, where only the unspaced `{#id}` pins an anchor. `blume dev`, `blume build`, and `blume check` now warn about them as `BLUME_MD_CURLY_ANCHOR`, naming the file and line and the `{#id}` or `[#id]` spelling to use, instead of silently giving the heading an id like `setup--setup`.
- 5c1974b: Hiding the home page with `sidebar.hidden` (or `hidden`) to drop its sidebar row no longer takes it out of the sitemap, site search, `llms.txt`, the site skill, the MCP server's page list, or the JSON API. The site's root URL always serves the home page, so it stays listed, as a hidden folder `index` page that its group row links already did. This applies to each locale's and archived version's home page too.
- 2a23248: The top-level `noindex: true` frontmatter shorthand now emits the page's `<meta name="robots" content="noindex">` and drops its canonical and structured data, exactly like `seo: { noindex: true }`. Before, it only took the page out of the sitemap, so `blume audit` reported the page as indexable but missing from the sitemap. The top-level `hidden` shorthand now also keeps a changelog entry off the `/changelog` index, like `sidebar.hidden`.
  
  Hiding a folder's `index` page with `sidebar.hidden` (or `hidden`) to drop its duplicate sidebar row no longer takes the page out of the sitemap, site search, `llms.txt`, the site skill, the MCP server's page list, or the JSON API. The group row still links the landing page, so it stays listed like any other page; a hidden page nothing in the sidebar links is still left out.
  
  After upgrading, the first build re-reads every page instead of reusing pages cached by the previous Blume version.
- a05c4d6: An image in an included partial now resolves wherever the partial is spliced, however its path is written. Only a plain path was rebased onto the including page, so `![](<./my diagram.png>)`, `![](./a.png 'Title')`, or `![]( ./a.png )` kept the partial's path and broke the page that included it (the build failed to resolve the image, or the agent-facing Markdown pointed nowhere). Every form a Markdown image's destination can take is now rebased and written back in its own form, and colocated images in those forms are served to agents the same way. `blume validate` reads those link forms too: a link with spaces around its destination, or a title in single quotes or parentheses, used to go unchecked.
- 153a87d: Link reference definitions now cross `<include>`, as if the partial were written inline. A reference in a page (`[PEP 508]`) to a definition in an included partial (`[pep 508]: https://…`), or a reference in a partial to a definition in the page or in another partial, stayed literal text, so a partial holding a site's shared link definitions linked nothing. Each now resolves, in `.md` and `.mdx` alike. A partial's definition of a file beside it moves with the partial, like its links. When the page and a partial both define a label, each uses its own.
- a675a48: A redirect from `/index` to `/` no longer breaks the home page's Markdown copy. A redirect that moves a page also redirects its `.md` and `.mdx` copies, and `/index.md` is the home page's own copy, so that redirect turned `/index.md` into a redirect to itself: every redirect file looped on it, and a static build replaced the copy with a redirect page, while `build`, `validate`, and `audit` all passed. A redirect now never takes over a Markdown copy Blume serves. One you write from a copy yourself, like `/guide.md` or `/index.md`, warns `BLUME_REDIRECT_MATCHES_PAGE`, since agents lose that copy.
- ed17ce5: `BLUME_NAV_INDEX_TITLE_MISMATCH` no longer says the page's `<title>` shows its frontmatter `title` when `seo.title` replaces it, and its fix no longer suggests leaving an intentional difference alone, which `blume validate --strict` fails on. It now names the ways to clear it: match the page's title and the folder's `meta.ts` title, set the hidden index page's `sidebar.label` to the folder title to keep a different heading on purpose, or show the index row again. A hidden index page whose `sidebar.label` matches its folder's title no longer warns.
- a01a348: With `lastModified: "git"`, renaming a page without changing its content, like `page.md` to `page.mdx` or a move to another folder under the content root, no longer resets its "Last updated" date to the rename. Blume now follows renames, as `git log --follow` does, and dates the page from the last commit that changed it. It still reads the dates with one `git log` per build. A rename that also edits the file dates the page at that commit.
- 87dc7dd: `llms.txt` and the generated site skill now summarize each page with its meta description, the same text as the page's `<meta name="description">`: `seo.description` when the page sets one, else `description`. Before, they read only `description`, so a page summarized only in `seo.description`, like a generated reference page or an OpenAPI operation page, was listed with no summary.
- 07a34bc: Under the `group` sidebar display, a group that is the only row at the top of the sidebar now starts open. A folder that wraps every page started collapsed on any page outside it, so a home page outside the folder showed a sidebar of one closed row. A group whose meta sets `collapsed: true` still starts closed.
- 560e316: `blume dev`, `blume build`, and `blume check` warn about a `:::` directive in a `.md` page. Directives render only in `.mdx`, so a `:::note` callout in a `.md` page showed its `:::` lines as text with no warning. `BLUME_MD_DIRECTIVE` names the file and line and says to rename the page to `.mdx`, or, for a name that isn't a callout type, lists the callout types. Code blocks and inline code are skipped.
- 08eda5d: An HTML comment in a `.md` partial no longer shows on an `.mdx` page that includes it. The partial is read as MDX there, which has no HTML comments, so `<!-- a note -->` rendered as visible text (with its dashes turned into en dashes). Its comments are now spliced as MDX comments (`{/* a note */}`), so they stay hidden, while a comment shown in a code block or inline code stays as written.
- 2efc0b4: `blume dev`, `blume build`, and `blume check` warn about two things in `.mdx` that used to pass `blume check` and then break the page:
  
  - `BLUME_MDX_ATTRIBUTE_LIST`: an attribute list like `{ width="300" }` or `{: .note }` after an image or paragraph. MDX reads the braces as JavaScript, so the build failed at render time with a bare `ReferenceError` that named only the route. The warning names the file and line and suggests setting the attributes on a JSX element instead.
  - `BLUME_MDX_UNCLOSED_ELEMENT`: an HTML void element without a closing slash, like `<img …>` or `<br>`. In an `.mdx` page it fails to compile, and in a `.md` partial that an `.mdx` page includes it silently took in the rest of the partial as its children. The warning points at the partial's own line.
- 13f8e67: `blume dev`, `blume build`, and `blume check` warn about two kinds of JSX in `.mdx` that built green and did nothing:
  
  - `BLUME_MDX_EVENT_HANDLER`: a JavaScript event handler on an HTML element, like `<button onClick={() => open()}>`. The page is static HTML, so the handler never ran. The warning names the file and line and points to islands, which do run in the browser.
  - `BLUME_UNKNOWN_PROP`: a built-in component with no children given props it doesn't take, like VitePress's `<Badge type="tip" text="beta" />`, which rendered an empty badge. The warning lists the props the component takes and suggests writing the content between the tags. A component you replace in `components.ts` isn't checked.
- e8b4e3f: `mdxRemote({ url, files })` no longer warns `BLUME_MISSING_SECRET` for `GITHUB_TOKEN` when the `url` is on `raw.githubusercontent.com`. A public repository's raw files need no token, so the warning fired on every build of a site that needed nothing set. Only the `github` form, which lists files through GitHub's rate-limited API, still declares the variable. When GitHub refuses a remote file with a 401, 403, or 404 while `GITHUB_TOKEN` is unset, the warning for the skipped file now says so.
  
  A remote page whose file opens with a `# Heading` no longer shows two `<h1>`s. When that first heading matches the page's front matter `title`, or the front matter sets no `title`, Blume drops it and uses its text as the title. A first heading that differs from `title` stays.
- 05a1d47: A build that fails in an `.mdx` page now says where. A `{…}` expression that reads a name nothing defines (`{user.name}`, a `{{name}}` on a site without that variable, an attribute list like `{ width="300" }`) failed with a bare `ReferenceError` that named only the route; it's now `BLUME_MDX_UNDEFINED_NAME`, at the expression's file, line, and column, with how to show the braces as text. A compile error in a partial an `.mdx` page includes named only the page, with no line; it's now reported at the partial's own line.
- 05a1d47: An `.mdx` page that MDX can't parse (an HTML comment, an element left open, a stray `{`) used to pass `blume validate`, `blume doctor`, and `blume check`, and only failed `blume build` once Astro compiled it. Blume now parses every `.mdx` page when it reads the project, so all of them report it as `BLUME_MDX_SYNTAX`, an error at the line and column where MDX stopped, with how to fix it, and `blume build` stops before it compiles anything. `blume build --no-strict` now leaves such a page out and builds the rest of the site, as it does a page with invalid front matter; before, the build still failed on it. `blume dev` keeps the page, so opening it shows the error. A partial that only breaks once an `.mdx` page includes it is a `BLUME_MDX_SYNTAX` warning at the partial's line.
- fdb7115: The `blume-migrate` skill's Mintlify codemod keeps the brand icons Blume renders. It dropped every brand icon as having no Lucide equivalent, but Lucide still ships `facebook`, `github`, `gitlab`, `instagram`, `linkedin`, `slack`, `twitter`, and `youtube`, so an `icon: github` page lost an icon that would have rendered. The codemod now keeps those under the same name and still drops the brands Lucide lacks (`discord`, `x-twitter`, `docker`, and the like, plus `apple`, whose Lucide icon is the fruit), and the skill's Mintlify reference says the same.
- bb126b6: A page whose file git ignores no longer shows an **Edit on GitHub** link. Generated pages, like a TypeDoc reference written into a gitignored folder under the content root, are never committed, so the link opened a GitHub 404. Blume asks git which page files it ignores with one `git check-ignore` call per build and drops the link from those pages. A file that a `.gitignore` rule matches but git already tracks keeps its link, and outside a git repository every page keeps it, as before.
- e83d42f: Open Graph cards render again for a font whose family name has a word starting with a digit, like the curated `source-sans-3` (Source Sans 3) and `source-serif-4` (Source Serif 4). The card handed the family name to the renderer unquoted, which reads it as CSS and rejects a word like `3`, so setting one of those fonts in `theme.fonts` failed the build whenever cards rendered. The card now quotes the name, and you no longer need to set `seo.og.fonts` to work around it.
- 8fbb6d3: Try it and the generated code samples in an OpenAPI reference now send an `Accept` header naming the first JSON media type among the operation's responses (a success response's first). Before, they sent none, and frameworks like Laravel treat such a request as a browser's: on a stock Laravel API, sending a request without a token returned a 500 ("Route [login] not defined.") instead of the documented 401. An `Accept` header parameter the operation declares still takes precedence.
- ca9dc01: `blume dev`, `blume build`, and `blume validate` now warn when two operations in an OpenAPI spec share an `operationId`. Blume keeps both pages by adding the method to the second one's id (and to its URL when the two share a tag), which used to happen without a sign. `BLUME_OPENAPI_DUPLICATE_OPERATION_ID` names both operations and where the second one ended up.
- 223ad49: Request samples and the Try it prefill in an OpenAPI reference now leave out `readOnly` properties wherever the example comes from. A model-level `example`, which TypeSpec writes, was copied as written, so every request sample sent the model's server-generated `id`. Response examples likewise leave out `writeOnly` properties. The Try it form no longer lists `readOnly` fields either, matching the request body's schema table, which already hid them.
- d68591a: `blume dev`, `blume build`, and `blume validate` now warn when an OpenAPI spec's server is a local address (`localhost`, `127.0.0.1`, `0.0.0.0`, or `[::1]`), which frameworks write when the spec is exported on a dev machine. Blume published it as the server Try it sends requests to and the code samples call, which readers can't reach. `BLUME_OPENAPI_LOCAL_SERVER` names the spec and the URL, and suggests listing the public URL in `servers` or setting it with an overlay.
- 223ad49: An OpenAPI body or response with several named `examples` now shows all of them, each labeled by its `summary` or its key. Before, an operation page showed only the first one, unnamed. A response gets a tab per example after its status (`200 · A cat`), a webhook payload a tab per example after its media type, and a request body an **Examples** block under its schema.
- d68591a: `blume dev`, `blume build`, and `blume validate` now warn when a spec that declares OpenAPI 3.1 or later uses `nullable`, which 3.1 removed. Blume shows those values as nullable, but validators and SDK generators that follow 3.1 read them as never null. `BLUME_OPENAPI_NULLABLE` names where, as JSON Pointers into the spec, and suggests `type: [string, "null"]`.
- 18c8ea9: An OpenAPI operation page's Markdown copy (`<route>.md`), `llms-full.txt`, and the MCP server's `get_page` now include the operation's request body and responses. Before, they carried only the endpoint, so the examples a spec records (rswag's, say) and the request bodies a generator infers (Scramble's) never reached agents. The copy lists the request body's media type and top-level properties with its example, every named one included, and each response's status and description with the examples the spec records, plus a sampled example for the first success response that records none. A webhook's body is listed as its payload.
- 223ad49: A property written as a `$ref` with keywords beside it, which OpenAPI 3.1 allows and ASP.NET Core writes for every enum-typed property, now shows its own `description` instead of the referenced schema's. An `example`, `examples`, or `default` beside a `$ref` is now used in request samples, the Try it prefill, and response examples, where the referenced schema's value used to win.
- ca9dc01: An OpenAPI tag's `x-displayName` now labels its sidebar group and its section on the overview page. Before, Blume ignored it and showed the raw tag name. The tag's URL still comes from its name, so adding an `x-displayName` doesn't move any page.
- e1607f1: API operation pages now end like every other docs page: the "Was this page helpful?" rating, a `PageFooter` component override, and the previous and next links render below the operation, aligned with its description column. Before, operation pages skipped the whole page-end area, so the rating and `PageFooter` never showed on them.
- 2efc0b4: `blume dev`, `blume build`, and `blume check` warn about syntax from other docs tools that a page shows as written. Each warning names the file and line, skips code and comments, and says what to write instead:
  
  - `BLUME_TEMPLATE_TAG`: a Liquid or Markdoc tag (`{% include note.html %}`) or a Liquid output (`{{ site.title }}`) in a `.md` page. A `{{name}}` that could be a Blume variable isn't reported.
  - `BLUME_WIKILINK_UNSUPPORTED`: a wiki link (`[[Home]]`, `[[Text|Page]]`) that names one of the site's pages, or any with a `|`, with the Markdown link that replaces it. Obsidian vault sources still turn wiki links into links.
  - `BLUME_MDC_SYNTAX`: a Nuxt Content (MDC) block component (`::callout` … `::`) or inline component (`:badge[New]{color="primary"}`).
  - `BLUME_MD_ATTRIBUTE_LIST`: a kramdown or MkDocs attribute list in a `.md` page, like `{: .note }` or `{: #intro }` on its own line or after a block, or `{ width="300" }` after an image. A heading's `{#id}` marker is left to `BLUME_MD_CURLY_ANCHOR`.
- aece109: A pinned heading id now keeps `--`, `---`, `...`, and quotes as written. Smart punctuation, which turns `--` into a dash and curls quotes in a heading's text, also rewrote the id in its `[#…]` or `{#…}` marker, so `## Read and write [#read--write]` anchored as `#read–write` (with an en dash): links to `#read--write` missed the heading, and `blume validate` reported them as `BLUME_BROKEN_ANCHOR`. The id is now used exactly as written, on a page and in a partial it includes, so the anchor of such a heading changes from the dashed or curled form to the one in its marker.
- a56ff73: `blume preview` now answers a static build's redirects the way the host files the build writes do. Pattern redirects like `/old/*` → `/new/:splat` used to 404, because only the exact redirects had redirect pages in `dist/`. Every redirect, exact or pattern, now gets its configured status, including the Markdown copies a moved page takes with it.
  
  `blume dev` and `blume preview` also handle trailing slashes the way static hosts do. A slashed page URL like `/guide/` used to 404 in both. It now redirects to `/guide` and keeps the query string. A folder of HTML shipped in `public/`, like `public/demo/index.html`, is served at `/demo/`, and `/demo` redirects there. Before, `blume preview` served it only at `/demo`, so the relative links inside it broke, and `blume dev` served it at neither URL.
- e10a457: `blume preview` serves a page again when a redirect points its old `.html` URL at it, like `/guide.html` to `/guide` after a migration from VitePress, VuePress, or MkDocs. The redirect page for `/guide.html` lands in a `guide.html/` folder, and the preview server took that folder for the page, so `/guide` showed the redirect page and refreshed to itself forever. A redirect from an `.html` URL also no longer adds redirects for `/guide.html.md` and `/guide.html.mdx`, URLs that never had a Markdown copy.
- cfdf7a0: Copying a `<Prompt>` (or opening it in Cursor) keeps the punctuation its body was written with. Smart punctuation curled the body's quotes and joined its dashes and dots like the rest of the page's prose, so `lang="ts"` copied as `lang=“ts”` and `--force` as `–force`, which broke code in the prompt. Quotes, dashes, and ellipses the body's author typed as such still copy as typed.
- d68591a: `blume dev`, `blume build`, and `blume validate` now warn when a project ships `public/openapi.json`. That file takes over `/openapi.json`, where Blume publishes the OpenAPI description of the site's JSON docs API, so the description wasn't generated, while `/.well-known/api-catalog` still listed `/openapi.json` as that API's description. `BLUME_PUBLIC_OPENAPI_JSON` names the file and suggests moving it to another path, like `public/specs/openapi.json`, or setting `agents.api: false`.
- 5a1dfb9: Under a `deployment.base`, a root-relative URL in raw HTML now gains the base like a Markdown link does. `<a href="/guide">`, `<img src="/logo.png">`, and any other element's `href` or `src`, as raw HTML in `.md` or an element in `.mdx`, shipped without the base, so they pointed outside the site, while `blume validate` passed them. A URL that already starts with the base, an external URL, and a `#fragment` are left as written, and a raw `href` still gains no `basePath`.
- f2c66a9: A redirect from a page's own `index.html` URL, like `{ from: "/guide/index.html", to: "/guide" }`, no longer fails a static build with `EISDIR`. Astro wrote the redirect page to `guide/index.html/index.html`, which needs the page's own `guide/index.html` file to be a folder. The build now writes no redirect page there, since the page itself is served at that URL. The redirect still goes into `_redirects` and `vercel.json`, so hosts that read them answer it with the redirect, and `blume dev` answers it too.
- ec6b278: A link to a Markdown file renamed from `.md` to `.mdx`, or back, now lands on that file's page even when the file has a `slug` or an ordering prefix. A link to `./01-setup.md` after the file became `01-setup.mdx`, or to `./setup.md` when `setup.mdx` sets its own `slug`, fell back to a relative link with the extension dropped, which no page serves, so the built link 404'd and `blume validate` reported `BLUME_BROKEN_LINK`. Blume now tries the same file with the other extension before that fallback, in the built page and in `blume validate` alike.
- 6751fbd: A root-relative link to a Markdown file, like `[Setup](/guides/setup.md)` (the way VitePress, Docsify, and TypeDoc write one), now lands on the page that file publishes, its `slug` and ordering prefix included, as a relative `./setup.md` link already did. It kept its path, which is also the URL of the page's Markdown copy, so readers landed on raw Markdown, and `blume validate` passed it. The link is read from the content root; under a `deployment.base`, one that starts with the base is read as including it. A link that names no file in your content keeps its path, so a link to a page's Markdown copy still works, and a raw `<a href>` keeps its path too. The page's Markdown copy and `llms-full.txt` get the same rewrite, and `blume validate` checks the link at that page.
- e061f74: A tab at `path: "/"` on a site with no root `index.mdx` now links to the first page in the sidebar, as tabs for other sections already did, instead of to `/`, which has no page. On an archived version's pages, the root tab still links back to the current docs, now under a `basePath` too, where it linked to the archived version's first page. `BLUME_NAV_MISSING_PAGE` now checks a tab where it links: its `href` when it has one, and its `path` otherwise. Before, it checked only `path`, so a root tab warned on every build, even with `href` set to a real page.
- 480d351: A `script()` analytics adapter with a root-relative `src`, like `script({ src: "/js/redirects.js" })` for a file in `public/`, now loads it under `deployment.base`. Before, the tag kept the path as written, so on a site served under a base the script 404ed unless the base was written into `src` by hand. A `src` that already starts with the base is left as written, and absolute and protocol-relative URLs pass through unchanged.
- 6b58575: Blume now requires `sharp` 0.35.5, which patches a memory vulnerability in its bundled librsvg (GHSA-wq5f-xc86-pv6w). The bug can lead to remote code execution on glibc-based Linux when sharp renders an SVG.
- f28a742: A colocated SVG whose size Astro can't read no longer fails the whole build. Astro reads an SVG's size from its `<svg>` tag, and only when that tag ends within the file's first 1,000 bytes with a width and height or a viewBox; a draw.io export puts its whole diagram in a `content` attribute on the tag, so one diagram failed `blume build` with `NoImageMetadata`. The page now shows that SVG as it is, served from `/blume-assets/content/…` like the copies agents read, and `blume dev`, `blume build`, and `blume validate` warn about it as `BLUME_SVG_UNOPTIMIZED` at the line that embeds it.
- 1ba74be: A `(group)` folder inside a tab section now keeps the order its `meta.ts` `pages` sets. The folder adds no URL segment, so it shares the tab's path, and Blume listed its loose pages above its subgroups as if it were the section's top level, even when every subgroup was a collapsible `group` or a `page` drill-in. Only the section's own top level lists its loose pages first now, as the folder meta docs describe.
- 1ca5a00: `blume dev`, `blume build`, and `blume check` warn when `theme.accent`, `theme.action`, `theme.background`, or a `seo.og.palette` color isn't a CSS color, like `"deep purple"` or a hex value without its `#`. Before, the config took any string and the site shipped it, so browsers ignored every style that used the color, with no warning. `BLUME_THEME_COLOR_INVALID` names the field, its value, and the line in `blume.config.ts` that sets it, and lists the accent presets and the color forms to use instead. It's a warning, so an existing build still passes unless it runs with `--strict`.
- 05a1d47: A project `tsconfig.json` whose `extends` or `references` doesn't resolve, such as a package that isn't installed or a framework's generated tsconfig that doesn't exist yet (Nuxt's `.nuxt/tsconfig.app.json`), failed `blume build` with Astro's `GenerateContentTypesError` and no hint that the build reads that file. `blume build`, `blume dev`, and `blume check` now report `BLUME_TSCONFIG_EXTENDS` at the line that names the target, and say to install, restore, or create it, or remove it from `extends` or `references`.
- 563da2d: `BLUME_UNKNOWN_COMPONENT` now names every page that uses an unknown component, with a count, instead of only the first one it found. A `<Widget>` on five pages used to report one route, so fixing that page brought the same warning back for the next.
- 97fe5db: A page without a frontmatter `title` that opens with a `# Heading` shows that heading once. The page took its title from the heading and rendered it as the page heading, but the heading stayed in the body too, so it appeared twice, one above the other, and `blume audit` reported two `<h1>` tags. The opening heading now gives the page its title and leaves the body, and the page heading takes its anchor, so links to `#the-heading` still land. `llms-full.txt` opens each page's section with its title, so a body that opens with a `# Heading` of the same text showed it twice there too; the section now drops the body's copy. A page with a `title`, a `#` heading after other content, and a page in `custom` or `frame` mode, which shows no title, keep their headings as before.
- 2849116: `blume validate` now says which page an `.html` link means. Blume serves pages at their routes, never at `.html` URLs, so a link like `./setup.html` or `/guides/setup.html` 404s unless `public/` has that file. `validate` reported it only as an asset missing from `public/` (`BLUME_BROKEN_ASSET`), and skipped it entirely when the project had no `public/` folder. The warning now names the page route to link instead, like `/guides/setup` for `setup.html` or `/guides` for `guides/index.html`.
- a9ec1fd: `blume validate` now checks the `src` of `<img>`, `<source>`, `<video>`, and `<audio>` elements, in raw HTML in `.md` pages and as elements in `.mdx` pages. Before, a broken `src` passed `validate --strict` and 404'd on the built site. A `src` ships exactly as written, so it's checked where the browser requests it: `./diagram.png` on `/guides/setup` must be served at `/guides/diagram.png`, from `public/`. A relative `src` that names a file beside the page is reported too, since nothing publishes that file; the `BLUME_BROKEN_ASSET` warning suggests Markdown image syntax (`![alt](./diagram.png)`), which does, or a `public/` path.
- d2dda96: `blume validate` now warns about a link that uses one of ReadMe's link schemes: `doc:`, `ref:`, `page:`, `changelog:`, or `blog:`. Only ReadMe resolves them, so anywhere else each ships as a dead link, and `validate` passed them. `BLUME_UNSUPPORTED_LINK_SCHEME` names the file and line; link the page by its path instead. Other schemes, like `mailto:`, `tel:`, or an app's `vscode:` deep link, are still left alone.
- 6a524b1: `blume validate` now reads link syntax the way the page renders it. A link destination in angle brackets, which Prettier writes for a URL with parentheses (`[x](<https://example.com/a(b>)`), was misread as a broken relative path (`BLUME_BROKEN_LINK`), and an image path with backslash-escaped parentheses (`![](image%20\(115\).png)`) was reported missing (`BLUME_BROKEN_ASSET`) though the build found it. Both are now read without the brackets and escapes. Links inside comments, which never render, are no longer checked: an HTML comment in a `.md` page, or a `{/* … */}` comment in an `.mdx` page. A code fence inside a block quote (`> ```ts`) now counts as code, so a quoted line in it like `>   [key: string]: string;` is no longer read as a link definition. A line whose `<` never closes, like `[1]: <src/x.ts - function x(`, isn't a link definition either, and when a label is defined twice, only the first definition, the one the page uses, is checked. A fragment link to an `<a name="…">` now resolves, like one to an element's `id`, instead of warning `BLUME_BROKEN_ANCHOR`.
- 40fbae3: `blume validate` now checks where each redirect leads. Only `blume audit` did, on the built site, so a redirect to a page that was never written passed `validate --strict` and 404'd once deployed. An exact internal `to` must now be a page, a file in `public/` or one Blume generates, or another redirect's `from`; for a pattern, something must be served under the literal part of `to` before its first capture (`/v2/` in `/v2/:slug*`). One that leads nowhere warns `BLUME_BROKEN_REDIRECT`, located at its `to` in `blume.config.ts`. Links to a redirect's `from` stay valid.
  
  `validate` also accepts links to API reference pages, like a `scalar()` page at `/reference`, which it reported as `BLUME_BROKEN_LINK` though the build serves them.
- a079099: `blume validate` now accepts links to the files Blume writes beside your pages. A link to `/llms.txt`, `/llms-full.txt`, `/sitemap.xml`, `/robots.txt`, an RSS feed, `/openapi.json`, `/skill.md`, or a `.well-known` file like `/.well-known/agent-skills/index.json` was reported as a missing asset (`BLUME_BROKEN_ASSET`), which failed `--strict`. Each now counts while the config has its feature on, so a link to `/llms.txt` with `agents.llmsTxt` off is still reported. A link to a `public/` folder with an `index.html`, like `/demo` or `/demo/` for `public/demo/index.html`, now resolves too, instead of failing as `BLUME_BROKEN_LINK`.
  
  A project with no `public/` folder no longer skips its asset checks with a `BLUME_ASSETS_UNCHECKED` note. It ships no public files, so a link to `/logo.png` there is reported as `BLUME_BROKEN_ASSET`, and images beside a page are checked as before.
  
  A `navigation.featured` link, header action, or call to action that points at a `public/` file (`/spec.pdf`) or a generated one (`/llms.txt`) no longer warns `BLUME_NAV_MISSING_PAGE` in `blume dev`, `blume build`, and `blume doctor`.
- 47865d3: A `vercel()` server build that fails its function-bundle check now removes `.vercel/output/config.json`. The adapter had already written that file, but Blume's pattern redirects, `Accept: text/markdown` routes, and base routes were never added. The output looked deployable, and `vercel deploy --prebuilt` would have shipped it without them. The same happens when the build can't move the routes under `deployment.base`. The function bundles stay in place so you can inspect them.
- 07ac505: `<YouTube>` embeds a playlist URL as the playlist. A `url` like `https://www.youtube.com/embed/videoseries?list=…` used to embed a video with the id `videoseries`, which doesn't exist, and `https://www.youtube.com/playlist?list=…` rendered nothing. Both now embed the playlist, the page's Markdown copy links to it, and a playlist pasted into a Notion video block becomes the embed too.
