blume@1.6.0
Minor Changes
-
089f0d2: Native GraphQL API reference. Point the new top-level
graphqlconfig block at a schema — SDL text or an introspection JSON result, local or remote — and Blume generates one real page per root field (grouped as Queries, Mutations, and Subscriptions) plus one page per named type (Objects, Input Objects, Enums, Interfaces, Unions, and custom Scalars), all in the sidebar, search, llms.txt, and OG images like any hand-written doc. Operation pages show arguments with defaults and deprecations, a generated example operation with typed variables and a matching example response, live code samples, and the same Try it playground the OpenAPI reference ships — a plain JSON POST of{ query, variables }against the configuredendpoint, withplayground.proxysupport for CORS-restricted APIs. Type pages cross-link every type reference and list where each type is used. -
65e3853: Add
github.hostso docs hosted on a GitHub Enterprise instance get working repo links. Every repo-derived URL — the header mark, per-page edit links, the agent manifest'srepository, the OG card's slug, and<GithubInfo>— was built against a hardcodedgithub.com, so an Enterprise repo produced links into the public site.hostdefaults tohttps://github.com, must be an HTTP(S) URL, and is normalized to its origin so nothing lands mid-link. The REST base<GithubInfo>queries is derived from it (anapi.subdomain for an Enterprise Cloud data-residency tenant,/api/v3for Enterprise Server) or set outright withgithub.api, and ahostprop points one card at a different instance.<GithubInfo>withholds its token from a cleartext-HTTP API base rather than sending it over the wire, and warns once per base so a private repo's missing counts are explained. -
2fa67b8: Custom heading anchors and table-of-contents markers, matching Fumadocs' syntax so migrated content works verbatim. Append
[#custom-id]to any heading to pin its anchor id — the marker never renders, links keep working after a heading is reworded, and pinned anchors stay identical across translated locales, where auto-generated ids differ per language.[!toc]keeps a heading on the page but out of the table of contents;[toc]does the reverse, adding a TOC-only entry that renders as an invisible anchor target so its link still scrolls somewhere — useful for labeling sections built from components rather than prose. Markers chain in any order (## Heading [toc] [#id]), work in Markdown and MDX, are stripped from search indexing, andblume validateresolves anchor links against pinned ids (including case-sensitive ones). Marker parsing is always on: a heading that previously ended in literal marker-shaped text now parses as marked — wrap the bracket text in inline code (## Using `[toc]`) to render it literally. The one exception is CommonMark's own: a trailing bracket whose label has a link-reference definition on the page ([toc]: /url, including one nested in a block quote or list item) is a shortcut reference link and stays in the heading text; a bare[toc]:with no destination defines nothing. -
beec2d2: Add
navigation.actionsandnavigation.cta— plain links in the header, and the one filled button.navigation.featuredpins secondary links to the top of the sidebar and there was no header slot at all, so a docs site arriving from a hosted platform lost whatever its old top bar asked the reader to do.ctais singular by contract. Both hide below thesmbreakpoint, where the header has room for the logo and the drawer toggle and nothing else — exceptctaon a page with no navigation toggle, where it stays, since nothing else can surface it on a phone. -
beec2d2:
navigation.reponow takes a URL as well as a boolean.githubdrives the per-page edit link, the header mark and the agent manifest'srepositorytogether, so a project whose docs repo is private had to unset all three — and then had no way to show a mark at all. An absolute URL points it anywhere on GitHub, an organization for instance, whiletruekeeps deriving it fromgithubandfalsestill hides it. -
a669f80: The Notion source now targets
@notionhq/clientv5 and Notion's data source API: a database is read through its first data source. Upgrade the peer dependency withnpm install @notionhq/client@^5. -
9ec4de4: Add a built-in
obsidiancontent source. Point it at an Obsidian vault and Blume reads the notes in place — no export step, and nothing generated into your repo:export default defineConfig({ content: { sources: [{ type: "obsidian", vault: "vault", prefix: "notes" }], }, });
[[Wikilinks]]become route links, addressed by note name across the whole vault the way Obsidian addresses notes: custom link text ([[Note|label]], and[[Note\|label]]inside a table), heading anchors ([[Note#Install]],[[#Install]]), block references ([[Note#^id]]), full and partial paths ([[folder/Note]],[[Note.md]]), and a frontmatterslug. A name two notes share resolves to an exact path match first, then to the first note in vault order, and warns only when a link actually resolves through the collision.- Heading anchors resolve against the target note's real headings, matched the way Obsidian writes them (inline formatting stripped) and slugged by the same pass that fills the page manifest.
- Frontmatter keeps Blume's page meta plus any key declared in
frontmatter.extendor, for notes of that type, in a content type'sfrontmatter; every other Obsidian property (includingtags,aliases, andcssclasses) is dropped, so a vault written with the Properties UI builds cleanly. - Locale directories and version snapshots inside the vault publish under their locale and version like filesystem content.
- An unresolved wikilink degrades to plain text with a warning, and one to a missing heading keeps the page link; single-line
%%comments%%are stripped and a wikilink inside an HTML comment is left alone; fenced, indented, and inline code pass through verbatim; symlinked folders are followed; untitled notes take their filename as the title; dot-folders (.obsidian,.trash, plugin caches) and the usual never-content directories are skipped by both the scan and the dev watcher;blume version cutleaves a vault insidecontent.rootout of the snapshot. - Vault notes get git "Last updated" dates when the vault is inside the repository, and "Edit this page" links through
github.dirfor a vault beside a monorepo docs app.
blume initoffers the source and seeds a first note. Not yet lowered: callouts, embeds, multi-line comments, aliases as link targets, and backlinks.Also in this release:
frontmatter'sstringifyno longer re-parses a string body (a body opening with a---divider was read as a second front matter block), and git last-modified dating now skips the scan entirely when no local source exposes a content root — a custom staged source that setssourcePathwithout acontentRootis no longer dated — setcontentRooton the source to date its pages. -
20a5da5: Content includes:
<include>./path.mdx</include>splices another file into a page at build time — the same syntax Fumadocs ships, so migrated content works unchanged. Markdown/MDX targets are embedded as content (front matter stripped, nested includes with cycle detection, relative images rebased onto the including page); any other extension — or an explicitlangattribute — embeds the file as a fenced code block, withmetafor the fence meta string. Paths resolve relative to the including file, or from the content root with a leading/. Spliced content flows through every surface: headings join the page's table of contents and anchor index, search indexes the included text, the.md/.mdxmirrors and llms-full.txt serve the expanded document, broken links inside a partial are reported against the partial file, and editing a partial inblume devreloads the pages that include it. Underscore-prefixed files (_snippets/) are already excluded from routing, so partials need no configuration. Unresolvable statements surface asBLUME_INCLUDE_NOT_FOUND,BLUME_INCLUDE_CYCLE, andBLUME_INCLUDE_OUTSIDE_ROOTdiagnostics.
Patch Changes
- 68f48c1: Downlevel components in the Ask AI grounding corpus, the way the
.mdmirror,llms-full.txtand MCPget_pagealready do. Ask AI was built from the search documents' verbatim Markdown, so a page whose body is a<CardGroup>of<Card>s reached the model as JSX with no prose in it — and section landing pages, which are exactly that shape, rank first for section-level questions.prop={frontmatter.*}expressions now resolve here too. - fd08e26: Require Astro 7.2.3+, which renames the dev SSR entry to
virtual:astro:server-appso full reloads no longer re-request it with a spurious.jssuffix — and drop theserverAppResolvePluginshim that worked around the old id corrupting the dev server on content renames. - 1715b14: Give each AsyncAPI channel or operation that references the same component its own copy before traits merge. The aliases shared one object, so the first merge leaked into the second and stripped
traitsfrom the component table entry itself. - 0fe3d43: Darken the green and orange text that misses WCAG AA in light mode. Both drew at
-700over a 15% tint of their own hue, landing at 4.32:1 and 4.44:1 against the 4.5:1 bar for text that size, while every other hue already cleared it (teal 4.69:1, red 5.17:1, blue 5.71:1, purple 5.82:1, violet 6.00:1, yellow 6.02:1). At-800they reach 6.20:1 and 6.25:1, with the fill weight unchanged. This covers the GET and PUT method badges (and GraphQLQUERY), the matching badges in a reference's sidebar, 2xx and 4xx response status chips, and<Badge>'ssuccessandwarningvariants. Thedeprecatedlabel on an operation moves too: atorange-600on the page background it was 3.59:1, the worst of the set. Dark mode was already passing and is unchanged. - 0fe3d43: Darken the callout icons that miss the WCAG 1.4.11 non-text contrast bar in light mode. A callout's icon names its type, so it is meaningful UI rather than decorative:
check/successandwarningsat at 2.94:1 and 2.95:1 against their own tint and now take-700(4.52:1 and 4.67:1), andnote's icon was muted-foreground at 70% over the muted surface (2.68:1) and is now full strength (4.64:1). Dark mode was already passing. - 4c0fdfc: Keep camelCase word boundaries in API reference operation routes. An
operationIdwent through the generic slugger, which lowercases before it replaces non-slug characters, soprocessSlackInteractioncollapsed to/reference/integrations/processslackinteraction. Operation ids are usually camelCase because OpenAPI generators reuse them as SDK method names, and renaming them just to get readable URLs couples SDK naming to presentation. The key now hyphenates the boundaries first: a lower-case letter or digit followed by a capital, and the last capital of an acronym before a capitalized word, soprocessSlackInteractionbecomesprocess-slack-interactionandgetHTTPResponsebecomesget-http-response. Already kebab-case ids and the method-plus-path fallback are unchanged. GraphQL field and type names and AsyncAPI operation ids derive their routes through the same key, so a GraphQLaddPetmutation moves from/graphql/mutations/addpetto/graphql/mutations/add-petand an AsyncAPIpublishUserSignedupoperation frompublishusersigneduptopublish-user-signedup. - 0fe3d43: Darken the typed
<Card>icons and the stroked<Badge>variants that still missed the light-mode contrast bar. A card'scheck,warning, andnoteicons carried the same values the callout icons moved off (2.94:1, 2.95:1, and 2.68:1 against their tint) and now share the callout table. A stroked badge has no fill of its own and inherits whatever surface it sits on — inside anotecallout,successat-700was 4.47:1 — sosuccessandwarningnow take-800whether filled or stroked. - 68f48c1: Downlevel
<Card>and<CardGroup>to Markdown on the agent-facing surfaces. Both are Blume's own components and neither had a serializer, so a section landing page — the shapeblume initscaffolds and the Mintlify migrator emits — published raw JSX to/<route>.md,llms-full.txt, MCPget_pageand the Ask AI corpus. A card becomes its title as a link over its body, with thectalast; a group becomes everything it holds, block by block. Serializers inai.markdownComponentsgainchildBlocks()— every direct child of the element in order, each already downleveled — which is whatCardGroupis built on. - e823872: Harden two scanners against pathological input: HTML comments stripped before anchor detection are replaced with a space so their neighbors can't splice into new markup, and
github.dirplus source/meta route prefixes trim slashes linearly instead of with a regex that was quadratic on a run of/. - ce20984: Make room for the copy button in code blocks that have no language bar to hold it. Inside a component whose chrome is
not-prose—<Tabs>,<CodeGroup>,<Steps>,<Callout>,<Card>,<Accordion>and the rest — the bar is cleared but the button is not, and the block'spadding-topwas the 1rem chosen for a block with no chrome at all, so the button painted over the first line of code. The same applied to an untitled<CodeBlock>in plain prose, which renders no bar. Both now reserve the button's strip, as does a raw<pre><code>block written in prose, which gets a button but has no language bar either; the clearance is keyed on the docs layout that injects the button, so a custom page layout without it keeps the plain inset. - 35b5c6a: Support trailing
{#custom-id}heading markers as an equivalent of[#custom-id]— verbatim in.md, and as the escape\{#custom-id\}in.mdx, where a bare{…}is a JSX expression.blume checknow reports a bare{#id}in an.mdxpage (or in a partial it includes) asBLUME_MDX_CURLY_ANCHORinstead of leaving it to fail at compile time, andblume translateis told to preserve the marker. Fragment-link validation also recognizes ids on raw HTML elements (<a id="…">, including tags wrapped over several lines) as anchor targets, ignoring ids inside code, inline code, HTML comments,<Prompt>blocks, and component props. - ce20984: Drop the page-actions divider when the rail has no table of contents. The actions block carries its own top border as a separator under the contents, but the contents only render when a page has a heading within
toc.minHeadingLevel–toc.maxHeadingLevel— so on a page without one the block drew a rule across the top of an empty column. The rail now decides once whether an outline renders and passes that to the block (dividerprop, default on), so the two cannot disagree. - 1715b14: Keep a spaced Shiki line range (
```ts {1, 3-5}) as one fence-meta token. The range used to split on its whitespace, and the3-5}fragment was promoted to the code block's title. - ce20984: Cap the search dialog's section-filter row so the results keep the dialog. The dialog is a fixed height and the filter chips wrapped without a bound, so on a site with many sections the chips took the height and the results list got the remainder — measured on an 18-section corpus, the row wanted 266px of the 480 and left about one visible result. The row now shows two rows of chips plus the top of a third as the scroll cue, chips no longer wrap on long section names, the selected chip is scrolled into view when the chips reorder as the query narrows, and each chip exposes its selected state as
aria-pressed. - 6d4ffec: Load the font subsets your locales need. Remote fonts used to load only the Latin subset, so Vietnamese, Central European, Cyrillic, and Greek text fell back to the system font. Blume now derives subsets from
i18n.locales, and remote families accept asubsetsoption to pin the list. - 1715b14: Treat an empty
graphql.endpointlike an omitted one. The playground and code samples fall back to the endpoint placeholder instead of targeting an empty URL. - ceb7a9f: Fix the image lightbox becoming impossible to close after the first in-page navigation.
medium-zoominjected its stylesheet into<head>once, when its module first loaded, and the client router's head swap discarded it on the next navigation; with the transition rule gone, the close animation'stransitionendnever fired, so Escape, a backdrop click, and scrolling all left the zoomed image open (and thezoom-incursor disappeared) until a hard reload. The stylesheet now ships in the layout's page CSS, which the router carries across swaps, and the library loads as itspurebuild with no runtime injection. - 03f5b36: Upgrade js-yaml to v5. Front matter parses exactly as before: dates still become
Dateobjects and<<merge keys still resolve. - aee337e: JSON-LD can now say what a site is and who runs it.
seo.organizationadds an Organization node to every page — name and URL default to the site's, with a logo,sameAsprofiles, the email and telephone as aContactPoint, and the address as aPostalAddress— which the WebSite and article nodes cite aspublisher.seo.softwareadds a SoftwareApplication node to the homepage with the product's name, description, category, operating system, license, anOfferwhen a price is set (0for free software), and registry or repositorysameAslinks;software: truetakes every default. Both needdeployment.site, and custom pages built onPageLayoutorRootLayoutpick them up automatically. - dff7930: Scan link-reference definitions in linear time. A line of deeply nested block-quote or list containers that never reaches a
[label]:no longer backtracks exponentially while headings are extracted. - aee337e:
llms.txtnow tells agents when to use the site and where its machine-readable surface lives. A newai.llmsTxt.detailsoption inserts free-form Markdown after the title and summary — the llms.txt spec's details block — for "when to use" guidance, the install command, or the package name in the site's own words. The generated file also closes with two sections that need no configuration: Agent skills lists each skill published throughai.skillswith its description, and Agent resources links every artifact the build emits —llms-full.txt, the per-page.mdmirror, the MCP server and its discovery document, the skills index, the API catalog,agent-readability.json, and the sitemap — each only when it exists. - aee337e: The hosted MCP server now exposes every page as an MCP resource alongside its tools.
resources/listenumerates the pages at their served URLs (ablume:URI when nodeployment.siteis configured) with atext/markdowntype, title, and description;resources/readreturns the page's agent Markdown — the same outputget_pageserves — and accepts a listed URI, a bare route, or a.mdmirror URL. An unknown URI answers with the protocol's resource-not-found code. The server card advertises theresourcescapability, so clients and readiness scanners that attach context by URI can browse the docs without calling a tool. - 1715b14: Corrections to the
blume-migrateskill: a Docusaurus or Fumadocs folder withcollapsible: falsemaps to that folder'sdisplay: "flat"instead of being dropped, several Fumadocs GraphQL sources map tographql.sourcesrather than a singlespec, a NextradocsRepositoryBaseon a non-github.comorigin setsgithub.hostto that origin, and the favicon convention readsicon.{svg,png,ico}orfavicon.{svg,png,ico}. - 24ac208: Darken
--blume-muted-foregroundfromoklch(0.54 0 0)tooklch(0.53 0 0)and lighten thetiptint on callouts and cards fromaccent/10toaccent/6so muted body text clears WCAG AA on every tinted surface, not just on the page. At0.54muted text was 5.06:1 on the background but 4.38:1 inside adangercallout and 4.50:1 inside aninfoone; at0.53those rise to 4.57:1 and 4.69:1 and the page to 5.28:1.tiptints with the configurable accent, whose default is near black, so no text lightness alone could make it safe — even at0.53it sat at 4.25:1 over the 10% tint. At 6% it clears 4.5:1 against any accent (4.62:1 worst case on pure black). Dark-mode tokens are unchanged. - aee337e: The default 404 page now helps readers and agents recover instead of dead-ending. Under a "Where to look next" heading it links every top-level section (each tab's resolved target), the sitemap when a
deployment.sitemakes one possible, andllms.txtwhen it's enabled — so an agent that followed a stale URL lands on a real HTTP 404 whose body points at the site map and the docs index. The three new labels (notFound.suggestions,notFound.sitemap,notFound.llms) are translatable throughuilike the rest of the page's copy. - 43931fb: Localize the "Open in chat" prompt. The prompt handed to the chat provider ("Read
<url>so I can ask you questions about this page.") was hardcoded in English regardless of the site's locale. It now comes from the UI dictionary asactions.openInChatPrompt— with{url}replaced by the page's raw-Markdown URL at load time — so localized sites send a localized prompt, every shipped starter pack carries a translation, andi18n.uican override the wording like any other chrome string. - ce20984: Keep long unbreakable tokens inside the content column. Prose sat at
overflow-wrap: normal, so an API permission, a broker list, a bare URL used as its own link text or a module-qualified name in a generated reference painted past the column — and nothing between the paragraph and the viewport clips it, so the run landed in the document's scroll width and dragged the whole page sideways on a phone. Headings had their own opt-in; the property is now set once on the prose root and inherited, so running text, headings and inline code are covered by the same rule. Tables are unchanged: an auto-layout column always grows to fit its widest token, so a wide table still relies on its scroll wrapper. - 5559319: Close the site's dropdowns — the page actions (Export, Open in chat, Connect to MCP), the header language switcher and the nav selectors — when clicking outside them, pressing Escape, or moving keyboard focus out of the panel, instead of leaving the panel open until its own trigger is clicked again. Escape returns focus to the trigger when pressed from inside the menu, and an Escape aimed at the search dialog no longer closes a menu beneath it.
- ce20984: Give the API reference's sticky sample panel its own scroll region. A grid item's sticky containing block is its grid row, and the row is as tall as its tallest item — so expanding a request schema on the left made the left column tower over the panel and handed sticky an enormous travel budget, pinning the panel with everything below the viewport fold unreachable until the left column ended. Bounding it to the viewport turns the overhang into a scroll. The tradeoff runs the other way on an operation with a short left column and a long sample: the row is then only as tall as the capped panel, and the overhang is reached by scrolling the panel rather than the page; a playground response is scrolled into view when it lands below that fold. The scroll region is styled like the sidebar and the table of contents — thin, border-colored thumb, inset from the Request card's edge, stable gutter — wheel input chains to the page once the panel reaches its end, and the focus ring on the panel's first control is no longer clipped by the scroller's edge. Applies to the OpenAPI, AsyncAPI and GraphQL operation pages, which now share one
OperationPanelcomponent. - 1715b14: Give the API playground proxy a 30-second upstream deadline, shared across a redirect chain. The client already aborted its own request after 30 seconds, but that never reached the server-side fetch, so a documented API that accepted the connection and never answered held a server request slot until the platform killed it. A timed-out upstream is now the same JSON 502 as an unreachable one.
- 1715b14: Emit the request body in the playground's JavaScript and Python code samples as the raw editor text, never re-read as a JavaScript object literal or Python dict. The generated sample now sends byte-for-byte what the live request sends: a
__proto__key stays a key, an id past 2^53 keeps its digits, and1e400is not re-serialized asInfinity. - beec2d2: A protocol-relative
navigation.featuredhref (//host/path) no longer picks up a locale prefix on non-default locales, where it became/fr//host/path.localizePathnow draws the same line aswithBasePath: a leading slash alone doesn't make a route. - fe966ca: Redesign the default 404 page: a two-column layout with the title and home link on the left and a vertical "Where to look next" list of sections, sitemap, and llms.txt on the right, stacking on small screens.
- e2153d5: Restore live Markdown hot reload in migrated
.-rooted projects. Astro's content watcher now honors the docs collection's negated globs, so the generated dev config no longer hides Astro's cache dir from the watcher — the escape hatch that previously cost.mdbody edits a dev-server restart in that layout. - 2bfe840: Accept
@sanity/clientv8 as a peer dependency for the Sanity content source. - fc24f9d: Keep a Scalar-rendered API reference on the page's theme. Scalar decided light or dark on its own — from its own localStorage key or the OS setting — and never read Blume's
data-theme, so a reader whose stored preference disagreed with what their browser reports toprefers-color-scheme(Brave with its own dark color scheme over a light OS, for one) got a light navbar over a dark reference, with two toggles that didn't agree. The reference layout now pins Scalar's color mode to the current theme before it mounts, which also hides Scalar's own switch, and mirrors later flips of Blume's toggle onto the embed. Ascalar.forceDarkModeStateorscalar.darkModeset through the escape hatch is left to Scalar, as before. - dd1ed31: Add
search.indexing.includeCodeBlocksto index fenced code (body and title) in the source-built search indexes — the client index, hosted syncs, and the MCPsearch_docstool — while keeping the plain-text default..mdxpages are now parsed as MDX for indexing, and components are downleveled with the same serializers the agent surfaces use, so prose and fences inside<Steps>/<Tabs>and the text a<Card>or<TypeTable>shows are indexed instead of being folded into raw HTML. - 1715b14: Keep an explicit script subtag when a search locale carries a POSIX suffix.
az_Cyrl.UTF-8fell back toaz, which maximizes to Latin script, so segmentation stayed off and Cyrillic content produced no search tokens;sr_Latn.UTF-8did the reverse. The codeset and modifier suffix is now dropped before the tag is parsed. - 52ac092: Add a
syncKeyprop toTabs: only groups sharing the same key switch together, so unrelated groups that happen to share a tab title stay independent. Nested tab groups now also keep their own panels instead of having them adopted by an ancestor group. - d009a19: Keep the dark theme on the page during client-side navigation. The client router replaces the root element's attributes with the incoming page's, which dropped
data-themeuntil Blume re-applied it after the swap; in between, the router's scroll restoration computed the new page's styles with the light palette, so the sidebar, header tabs, language selector, search button, feedback buttons, previous/next links and page actions visibly animated from light to dark on every navigation. The theme is now stamped onto the incoming document before the swap, so it never drops. - ce20984: Emit
data-depthon each table-of-contents item. The heading's level was computed and then written only into an inlinepadding-inline-start, so a project wanting to style the rail by level had to match on that style string — which breaks silently if the indent ever changes. - 52ac092: Add
ts2jscode fences: mark atsortsxblock with thets2jsmeta keyword and it renders as synced TypeScript/JavaScript tabs, with the JavaScript variant generated automatically — type syntax and type-only imports stripped, formatting, comments, and JSX preserved. - 72c9abe: Parse the project's
tsconfig.jsonpath aliases with the TypeScript compiler API instead ofget-tsconfig. Blume'sget-tsconfigv4 and Astro's pinned v5 left two copies in every install, and a stale CInode_modulescache could drop Astro's nested copy, failingblume buildwithThe requested module 'get-tsconfig' does not provide an export named 'readTsconfig'. TypeScript already ships with Blume, so there is now one fewer dependency and alias resolution follows tsc exactly. - aee337e:
blume buildnow audits the Vercel function bundle after a server build and fails when the bundle is missing a package its server code imports. The Vercel adapter's dependency trace silently drops any bare import it can't resolve from the project root — under an isolated linker (pnpm, Bun's isolated mode) that includes Blume's own runtime dependencies, such as the MCP server's@modelcontextprotocol/sdk— and the deployed function then fails on every request withFUNCTION_INVOCATION_FAILEDwhile the build, CI, and warm-cache previews all stay green. The build now reports each missing package with the chunk that imports it and thenpm install -D …line that fixes it; a missing Blume dependency fails the build, a project's own missing external is a warning.