---
changelog:
  category: Release
  version: blume@2.1.0
date: '2026-09-30T16:40:02Z'
seo:
  description: >-
    API references can generate code samples in 18 languages: cURL, Python,
    JavaScript, Node.js, TypeScript, PHP, Go, Java, Ruby, PowerShell, Swift, C#,
    .NET, C…
title: blume@2.1.0
type: changelog
---
## Minor Changes

- 99119e7: API references can generate code samples in 18 languages: cURL, Python, JavaScript, Node.js, TypeScript, PHP, Go, Java, Ruby, PowerShell, Swift, C#, .NET, C, C++, Kotlin, Rust, and Dart. List them in `codeSamples` on `openapi()` or `graphql()` in the order you want them, by id or by a common alias such as `golang`, `c#`, or `ts`. Each sample quotes values by its own language's string rules and updates live with the Try it form. `node` and `typescript` now generate their own samples (axios, and typed `fetch`) instead of repeating the JavaScript one.
  
  Operations also render the spec's own `x-codeSamples` (or `x-code-samples`) as tabs ahead of the generated samples, for SDK snippets written by hand or by Speakeasy or Stainless. Set `codeSamples: false` to show only those.
- f4854ef: Add a bot check for the assistant. `ai.assistant.captcha` takes an adapter from `blume/captcha`, `turnstile({ siteKey })` or `hcaptcha({ siteKey })`, run invisibly: the provider's script loads with a reader's first question, the panel sends a fresh token with each question, and the generated route verifies it (with `TURNSTILE_SECRET_KEY` or `HCAPTCHA_SECRET_KEY`) before the model runs. A failed check answers `403`, which the panel shows as a translated "we couldn't check that you're human"; a missing secret answers with the assistant's "not configured" notice, and `blume build` warns. With an external `endpoint`, the token is sent as `captcha` in the request body for that backend to verify. `useAssistant` takes the `captcha` settings and a `verifyMessage` too.
- 253ad9e: The assistant can now search the docs and read whole pages itself. Beyond the pages retrieved up front, the model gets a `search_docs` tool and a `read_page` tool, and can search, read, and search again before it answers, within one question and at most five steps. The tools run on your server against the assistant's snapshot and keep to the reader's language and docs version. When nothing matches a question up front, the assistant now searches instead of saying it doesn't know. They're on by default, except for `openai()` with a `baseUrl`, whose model may not support tool calling. Set `ai.assistant.tools` to change either.
- 6653f72: The assistant can now call OpenAI, Anthropic, Gemini, and Grok directly. `openai({ model })`, `anthropic({ model })`, `gemini({ model })`, and `grok({ model })` from `blume/ai` send each question to the provider's own API with your key, through its AI SDK package (`@ai-sdk/openai`, `@ai-sdk/anthropic`, `@ai-sdk/google`, or `@ai-sdk/xai`, installed as needed). They read `OPENAI_API_KEY`, `ANTHROPIC_API_KEY`, `GEMINI_API_KEY`, and `XAI_API_KEY`, turn the docs tools on, and send `reasoning` as each model's own control. `openai()` also takes a `baseUrl` for any OpenAI-compatible endpoint, which it calls through `@ai-sdk/openai-compatible` with the docs tools off, as `openaiCompatible()` did. `openaiCompatible()` still works and now returns the same adapter.
- aca5ac9: Add content variables. Define values under `variables` in `blume.config.ts`, like `{ version: "2.1.0", "api-url": "https://api.example.com" }`, and `{{version}}` anywhere in a page reads the value: in prose, headings, links, code, and component props, in `.md` and `.mdx` pages and the files they include. Search, the `.md` mirrors, and `llms-full.txt` show the value too. An undefined name in prose fails the build at its line (`BLUME_UNDEFINED_VARIABLE`); inside code it's left as written. Sites that define no variables are unaffected.
- 9ac2262: Add cookie consent. `consent` in `blume.config.ts` takes an adapter from `blume/consent`, and with it set every `analytics` adapter waits for the reader: its tags stay in the page as inert `text/plain` until the reader allows analytics, then run in order, once per page load, and Vercel Web Analytics sends nothing until then. `native()` is Blume's own banner, a card with Accept and Decline side by side and an optional `policy` link, translated in every built-in language and shown in `blume dev` too. `osano({ customerId, configId })` and `ethyca({ privacyCenter, propertyId, notice })` load Osano or Ethyca's Fides instead, and follow the answers readers give there. The site footer gains a Cookie settings link that reopens the banner or the manager's preferences (any element with `data-blume-consent-open` works in a custom footer), and a reader who takes consent back gets a reload so analytics stops. The "Was this page helpful?" rating, which reports through analytics too, only shows for readers who allowed analytics. Your own scripts can wait the same way with `type="text/plain" data-blume-consent="analytics"`, or listen for the `blume:consent` event.
- c3e1cd3: Add directory listings. `directory` in a folder's `meta.ts`, or on a group in an explicit `navigation.sidebar`, lists the group's pages below the content of its own page (a folder's `index` page, or the group's `root`): `card` as a grid of cards with each page's icon and description, `accordion` as rows with each subgroup a section that opens to its pages, and `none`, the default, as nothing. Nested groups inherit the nearest setting, so one `directory` in the content root's `meta.ts` gives every section a listing, and any folder can set its own. The values match Mintlify's `directory`.
- 0036165: Add components for documenting endpoints by hand. `<ParamField>` is a request parameter, named by where it goes (`<ParamField query="limit" type="integer" default={20}>`, or `path`, `header`, `body`), and `<ResponseField name="id" type="string">` is a field of the response, with optional `pre` and `post` labels. Both take `type`, `required`, `deprecated`, and `default`, hold their description as content, and nest an object's fields inside an `<Expandable>`. They render like the OpenAPI reference's rows, and a page's Markdown copy lists each field with its type and flags. `<RequestExample>` and `<ResponseExample>` hold code blocks as tabs (or a language menu with `dropdown`); on a wide screen they pin to a column beside the page, request above response, and stay in view while the reader scrolls, the way an OpenAPI operation's examples do. A page with `api` frontmatter (`api: POST /v1/users`) documents one endpoint without a spec: it shows the method and path at the top, and its `<ParamField>`s build a Try it panel and request samples in cURL, JavaScript, and Python, the same ones an OpenAPI operation gets, with nested fields as the properties of a JSON body. `authMethod` and `playground` frontmatter tune a page, and a new top-level `api` config sets the `server` a path joins, the default `auth`, and the `playground`, whose `proxy: true` routes sends through the built-in CORS proxy for those origins. The names and props match Mintlify's, so migrated pages keep them as written.
- 72f7cf5: Slim the header down to icon buttons, making room in it. Search is now an icon button beside the theme toggle and the assistant, in place of the search field, and each icon button names itself in a tooltip on hover and keyboard focus: "Search (⌘K)" (`Ctrl K` off Apple devices), "Toggle theme", and "Open assistant". The GitHub link leaves the header for the new site footer, which is now the one place the site links its repository and social profiles; `navigation.repo` works as before and applies there. The header's tabs now sit at its center on wide screens. The version and language pickers lose their border to match the icon buttons, the header's right-hand controls sit closer together, and the language switcher drops its globe icon and always shows the language's name. Its menu now widens to fit, so a long language name no longer runs into the "Not translated" note beside it. A `Header` override is unaffected. The "Open assistant" tooltip is a new `assistant.open` UI string, translated in every built-in language pack.
- f8bd737: Pass props to an `<include>`: every attribute other than `lang` and `meta` becomes a value the included file reads with `{{name}}`, like `<include plan="Pro" feature="SSO">./_snippets/upgrade.mdx</include>`. Props reach the partial's own nested includes and take precedence over a site-wide variable with the same name.
- 0758eaa: OpenAPI references now document webhooks and callbacks. Each webhook in a 3.1 spec's `webhooks` gets its own page, filed under its first tag or a Webhooks group. The page shows its payload schema, the responses your endpoint should send, and an example payload in place of the Try it panel. It lists only the security the webhook itself declares, since the spec's root `security` guards calls to the API, not the requests it sends. Webhook pages are in search, `llms.txt`, and the MCP server, and their Markdown marks them as requests the API sends. An operation's `callbacks`, inline or `$ref`'d from `components.callbacks`, render in a Callbacks section on its page with their URL expression, method, request body, and responses. Specs with webhooks no longer log a warning that they're missing from the reference.
- 2153f6b: Add page layout modes. `mode` in a page's frontmatter sets what it shows around its content: `wide` drops the table of contents and lets the content take its width, `center` drops the sidebar too and centers a wider column, `custom` keeps only the header for a landing page written in MDX, and `frame` is `custom` with the sidebar. `custom` and `frame` pages render no title, description, breadcrumbs, page-end links, or site footer, so they bring their own heading. Every mode stays in search and keeps its Markdown copy. The values match Mintlify's; its full-page `assistant` mode has no equivalent.
- ecbfb0e: Add narration: a "Listen to this page" player that reads each page aloud and highlights the sentence being read. Turn it on with `narration: true` to use the reader's browser voices, which need no key and work on any host. Or pass `narration: { provider: gateway({ model: "openai/tts-1-hd", voice: "alloy" }) }` to generate neural audio at build: one cached clip per sentence, shipped as static files, with browser voices as the fallback in `blume dev` or when the key is missing.
  
  The player announces callouts, steps, tabs, and collapsible sections with short spoken cues, and skips code, tables, media, and type tables. It opens a closed section or switches to a hidden tab when narration reaches it, and keeps the sentence in view until the reader scrolls away. It offers speeds from 0.8× to 2×, and stops when the reader opens another page. Set `narration: false` in a page's frontmatter to leave it out, or add `data-blume-narration="skip"` to keep any element out of narration.
- aea605b: Add pattern redirects. A redirect's `from` can now cover many paths, written the way Mintlify and `vercel.json` write them: `:name` matches one segment (`/blog/:slug`), `:name*` as the last segment matches the rest of the path (`/beta/:slug*` sends `/beta/a/b` to `/v2/a/b` and `/beta` to `/v2`), and `*` ending a segment matches the rest from there (`/old/article-*`). `to` reads the captures (`/v2/:slug*`, `/new/article-*`). Every host gets them in its own syntax: `_redirects` splats, `vercel.json` sources, and the routing config of a Vercel or Netlify server build, while the Node and Cloudflare servers and `blume dev` match them directly. Exact redirects win over a pattern that covers the same path. A pattern that also matches a page fails the build (`BLUME_REDIRECT_MATCHES_PAGE`), since hosts disagree on which answers, and links into a pattern count as valid in `blume validate` and `blume audit`.
- 09efac4: Add rate limiting to the server routes a reader can call: the assistant, the API playground proxy, and Mixedbread search. Each reader, by IP address, gets 30 requests per route every 10 minutes by default, and past that the route answers `429 Too Many Requests` with a `Retry-After` header, which the assistant turns into a translated "try again in a few minutes". `rateLimit` in `blume.config.ts` takes an adapter from `blume/ratelimit`: `memory()`, the default, counts in the server's memory (exact on `node()`, per instance on serverless hosts); `upstash()` shares the count through Upstash Redis's REST API (`UPSTASH_REDIS_REST_URL` and `UPSTASH_REDIS_REST_TOKEN`); and `cloudflare()` counts with Workers rate limiting, declaring the binding in the built Worker's config. Each takes `requests` and `window` (seconds). A request whose address the host can't tell is let through, and so is one a shared store fails to count. `rateLimit: false` turns it off.
- 0dd30af: Add a site footer: one row, as tall as the header, below the content on every page. Your repository link moves there from the header (see the header changes), as the first of the footer's icons, and `footer` in `blume.config.ts` adds the rest: `links`, a list of `{ label, href }` shown on one side, and `socials`, profile URLs keyed by platform (`x`, `discord`, `linkedin`, `youtube`, `website`, and more) shown as brand icons on the other, both in the order written. A `github` social replaces the repository link, and `navigation.repo` still hides it or points it elsewhere. Custom pages built on `PageLayout` show the footer too, unless they fill the layout's `footer` slot. A `Footer` layout override still replaces it, and `blume add footer` copies the built-in into your project to edit. A site with no repository, links, or profiles has no footer.

## Patch Changes

- ade0397: Add an Ask button to code blocks when the assistant is on. It opens the assistant with the block attached as a chip above the input; the question goes to the model with the code as a fenced block (up to 6,000 characters), and an empty question asks it to explain the code. The conversation shows the attached code under the question, and the button's label and the chip's text are translated in every built-in language. Code groups get one button in their tab strip, which asks about the tab that's showing.
- 57db488: Add a support handoff to the assistant. `ai.assistant.support` takes a `mailto:` address, a URL, or a page on your site, and once there's a conversation the panel shows a translated "Contact support" link: an email starts with the conversation as its body, and a link gets the conversation's id as a `thread` query parameter. The assistant's `ask`, `ask_answer`, and `ask_error` analytics events now carry the same `thread`, and `useAssistant` returns it.
- 0654705: Add `blume skill`, which writes your docs site's agent skill with a coding agent. `blume skill --claude` (or `--codex`) opens the agent on the new `blume-write-skill` skill. The agent writes a `SKILL.md` grounded in the docs that teaches an agent to use the product: setup, core concepts, common tasks, and gotchas, each linked to its page. The file goes under `agents.skills`, named after the site so it replaces the generated skill at `/skill.md`. Run it again to refresh the skill after big docs changes.
- c3e1cd3: Add `wrap` and `expandable` to code fences. `wrap` after the language (` ```ts wrap `) wraps one block's long lines instead of scrolling them, the way `markdown.code.wrap` does for every block. `expandable` collapses a long block to its first lines under a fade, with a Show more toggle that opens it in full, no inner scroll, and Show less to close it again; a block under 16 lines renders as usual. Both compose with a title, line numbers, and each other, and the toggle's labels are translated in every built-in language.
- a7c8ceb: Fix header controls overlapping on narrow screens. With many controls on (tabs, a version selector, the language switcher, search, and the icon buttons), the version and language pills squeezed on top of each other on phones, and the tab bar ran into them on tablets. Below the desktop breakpoint, selectors, the version selector, and the language switcher now sit at the top of the navigation drawer, full width. The remaining header controls keep their size, and only the site title truncates to make room.
- 553a58f: Apply OpenAPI Overlays to a spec before it renders. List `overlays` beside `spec` in `openapi()` or `scalar()`, or on each source, as local paths or `http(s)` URLs, applied in order. Overlay Specification 1.0 and 1.1 are supported: `update`, `copy`, and `remove` actions, with targets as RFC 9535 JSONPath. Overlays apply to the spec as written, before it's upgraded to OpenAPI 3.1, and everything built from the spec sees the result. An overlay that fails stops its reference from rendering, the way an unreadable spec does, so it can't publish what it was meant to hide.
- d6190ee: OpenAPI overlays can now add a property named `constructor` (it used to fail with a merge error), and a `__proto__` key in an overlay no longer reaches `Object.prototype`. Parsing a long run of digits in a theme color no longer takes quadratic time.
- c3e1cd3: Add `pagination: false` to page frontmatter, which leaves the previous and next links off that page. The page keeps its place in the order, so its neighbors still link to it. The Mintlify migration maps `hideFooterPagination: true` to it.
- 7f9fa4a: Add `<View>` for pages written for several audiences, such as one per programming language. Each distinct `title` becomes an option in a picker above the page's content, and only the chosen view's blocks show. Prose outside any `<View>` shows in every view, and headings in hidden views drop out of the table of contents. The reader's pick is remembered across pages and written to the URL as `?view=`, a link to a heading opens its view, and the choice applies before the page paints. The agent-facing Markdown includes every view under its name.
- 0a29cb2: Translate the banner, header links, and footer per locale. The banner's `content` and link `text`, `navigation.actions`, `navigation.cta`, and `navigation.featured` labels, and `footer` link labels now accept a map of locale code to text, like tab labels do. Each page shows its locale's entry and falls back to the default locale's. Internal footer links now move into the reader's locale whenever that locale serves the page, as header links already did.
- 4e4b258: Add related pages. `related` in a page's frontmatter lists up to ten pages to suggest at its foot, shown as cards under a translated "Related pages" heading: a root-relative path shows that page's title and description (its translation, on a translated page), a `{ Title: link }` entry names the card itself, and an absolute URL links off the site. Paths are checked like the page's other links, so `blume validate` reports one that matches no page. The key matches Mintlify's.
- 8f040cd: Add `i18n.routeByBrowserLanguage`, which sends visitors who land on the default language's home page to the home page in their browser's preferred language. Blume tries each language the browser prefers, in order, matching an exact locale code and then the base language, and a visitor whose first match is the default language stays. Picking a language with the switcher stops the routing for that reader, and only visitors arriving from outside the site are routed. The redirect runs in the browser before the page paints, so it works on static hosts. It's off by default.
- 169966c: Report search to your analytics. Once a query settles (a second without typing, or the reader picks a result or closes the dialog), the search dialog sends a `search` event with the `query`, its number of `results`, and the `path`, so queries with no results are easy to find. Picking a result sends `search_select` with the `query`, the result's `position`, and its `url`. Both go through every analytics adapter with an event API and as `blume:track` events.
- 0f8a46e: Rank pages with `search.boost` and `search.keywords` frontmatter. `boost` multiplies a page's search relevance: above 1 moves it up, below 1 moves it down. The default Orama search, FlexSearch, the MCP server, and the assistant apply it exactly. Pagefind weighs a boosted page's text more heavily, Algolia and Typesense sort by it among close matches, and Orama Cloud re-sorts a wider set of results. `keywords` are extra terms a page is found by, beyond its own text. Blume 1 accepted `search.boost` without reading it, so a page that still sets it now ranks by it. The Mintlify migration moves `keywords`, `boost`, and `searchable: false` into the `search` block.
- aa2babe: Add `seo.metatags` for tags written into every page's head: site-verification tokens, `theme-color`, an app banner, and anything else Blume has no setting for. Open Graph families render with `property` and everything else with `name`. A tag Blume writes itself, such as `description`, `robots`, `og:image`, or a `twitter:` card tag, is refused, and the build names the setting that controls it.
- 7dee91b: Publish an agent skill for every site. The build writes a `SKILL.md` named after the site's title, served at `/skill.md` and listed in the skills discovery index, `llms.txt`, and the AI catalog. It's built from what Blume already knows, with no model call: how to read any page as Markdown, `llms.txt`, the MCP server, the API references, the changelog, and a map of the docs with each page's description. It needs `deployment.site` for its absolute links. A skill of the same name in `agents.skills` replaces it, and `agents.skillMd: false` turns it off.
- 7a58b53: Meet WCAG AA contrast with the accent colors, and check a site's theme colors in `blume audit`. Each accent preset now has a darker light-mode shade and a lighter dark-mode shade, so accent text and button labels reach 4.5:1 in both modes. Blue is the default, so this changes sites that set no accent too. Labels on accent and action fills are white while white reaches AA on that color, and dark otherwise. A light accent, and every preset in dark mode, now gets dark labels instead of unreadable white ones. A new `accessibility` category in `blume audit` measures a custom `theme.accent`, `theme.action`, or `theme.background` against the same bar in both modes, and says when it can't read a color.
- 0c0acd5: Tell the translation agent to keep frontmatter valid YAML by quoting a translated value that contains a colon or starts with a quote or backtick, and name the YAML error and its line and column when `blume translate` rejects a page whose frontmatter doesn't parse.
- f50ef8a: The `BLUME_WIKILINK_UNRESOLVED` and `BLUME_WIKILINK_AMBIGUOUS` diagnostics now link to the Obsidian source page instead of the content sources overview.
- aecd4b9: Add written feedback. `feedback: { comments: true }` offers a box after the "Was this page helpful?" rating where the reader can say what worked or what's missing. A comment is sent as a `feedback_comment` event, with the rating, path, title, and the comment (up to 1,000 characters), through every analytics adapter with an event API and as a `blume:track` event. The box's label and button are UI strings, translated in every built-in language. `feedback: true` and `false` work as before, and the `Feedback` layout slot now receives a `comments` prop.
