blume 1.6.2 → 1.6.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/dist/cli/index.js +307 -68
  3. package/dist/cli/index.js.map +28 -23
  4. package/dist/types/ai/component-markdown.d.ts +14 -0
  5. package/docs/01-quickstart.mdx +2 -2
  6. package/docs/02-deployment.mdx +5 -5
  7. package/docs/{07-faq.mdx → 08-faq.mdx} +7 -7
  8. package/docs/advanced/blog.mdx +3 -3
  9. package/docs/advanced/changelog.mdx +2 -2
  10. package/docs/advanced/custom-pages.mdx +4 -4
  11. package/docs/advanced/meta.ts +1 -1
  12. package/docs/configuration/ask-ai.mdx +179 -0
  13. package/docs/configuration/index.mdx +8 -7
  14. package/docs/configuration/meta.ts +1 -2
  15. package/docs/configuration/search.mdx +1 -1
  16. package/docs/configuration/theming.mdx +1 -1
  17. package/docs/content/components.mdx +1 -1
  18. package/docs/content/i18n.mdx +7 -1
  19. package/docs/content/index.mdx +1 -1
  20. package/docs/content/navigation.mdx +2 -2
  21. package/docs/content/syntax.mdx +1 -1
  22. package/docs/discoverability/agent-discovery.mdx +196 -0
  23. package/docs/discoverability/index.mdx +48 -0
  24. package/docs/discoverability/json-api.mdx +58 -0
  25. package/docs/discoverability/llms-txt.mdx +68 -0
  26. package/docs/discoverability/markdown.mdx +76 -0
  27. package/docs/discoverability/mcp.mdx +64 -0
  28. package/docs/discoverability/meta.ts +18 -0
  29. package/docs/discoverability/metadata.mdx +82 -0
  30. package/docs/discoverability/open-graph.mdx +113 -0
  31. package/docs/discoverability/rss.mdx +24 -0
  32. package/docs/discoverability/sitemap-and-robots.mdx +95 -0
  33. package/docs/discoverability/structured-data.mdx +51 -0
  34. package/docs/index.mdx +5 -5
  35. package/docs/reference/eval.mdx +1 -1
  36. package/docs/reference/meta.ts +1 -1
  37. package/docs/reference/translate.mdx +1 -0
  38. package/package.json +18 -18
  39. package/src/ai/component-markdown.ts +17 -2
  40. package/src/ai/llms.ts +3 -10
  41. package/src/ai/markdown.ts +3 -10
  42. package/src/ai/openapi-components.ts +123 -0
  43. package/src/ai/serializers.ts +24 -0
  44. package/src/astro/templates.ts +42 -12
  45. package/src/audit/checks/links.ts +1 -8
  46. package/src/audit/checks/llms.ts +5 -4
  47. package/src/audit/redirects.ts +4 -3
  48. package/src/audit/run.ts +6 -8
  49. package/src/audit/url.ts +33 -0
  50. package/src/cli/commands/validate.ts +1 -0
  51. package/src/components/content/Component.astro +65 -68
  52. package/src/components/content/Tabs.astro +24 -9
  53. package/src/components/content/example-pane.ts +6 -0
  54. package/src/components/layout/LocaleLinks.astro +42 -0
  55. package/src/components/layout/PageLayout.astro +5 -3
  56. package/src/components/layout/ReferenceLayout.astro +5 -0
  57. package/src/components/layout/RootLayout.astro +110 -39
  58. package/src/components/layout/search-locale.ts +13 -0
  59. package/src/components/openapi/ApiOverview.astro +7 -39
  60. package/src/components/openapi/ApiTagOperations.astro +2 -1
  61. package/src/components/openapi/AsyncApiOperation.astro +3 -2
  62. package/src/components/openapi/GraphqlOperation.astro +3 -2
  63. package/src/components/openapi/Operation.astro +3 -2
  64. package/src/core/i18n.ts +13 -2
  65. package/src/core/links.ts +33 -1
  66. package/src/core/locale-links.ts +163 -0
  67. package/src/core/sources/normalize.ts +57 -7
  68. package/src/markdown/package-commands.ts +27 -3
  69. package/src/openapi/graphql.ts +29 -0
  70. package/src/openapi/model.ts +69 -0
  71. package/src/openapi/render-mdx.ts +3 -2
  72. package/src/openapi/signature.ts +18 -0
  73. package/src/search/documents.ts +4 -9
  74. package/src/theme/code-block-padding.ts +0 -8
  75. package/src/theme/entry.ts +33 -27
  76. package/src/translate/anchors.ts +91 -0
  77. package/src/translate/validate.ts +8 -3
  78. package/docs/configuration/ai.mdx +0 -613
  79. package/docs/configuration/seo.mdx +0 -364
@@ -0,0 +1,113 @@
1
+ ---
2
+ title: Open Graph images
3
+ description: Generated 1200×630 social cards for every page — branding, card layers, fonts for non-Latin scripts, and per-page overrides.
4
+ ---
5
+
6
+ Blume can render a 1200×630 social card for every page at build time — no headless browser, thanks to [Takumi](https://takumi.kane.tw), so builds stay fast. On by default once [`deployment.site`](/docs/deployment) is set or auto-detected (the `og:image` URL has to be absolute to be useful to crawlers), and off otherwise. Set `enabled` to override that either way:
7
+
8
+ ```ts blume.config.ts lineNumbers
9
+ seo: {
10
+ og: { enabled: true }, // or false to opt out even with a site set
11
+ }
12
+ ```
13
+
14
+ ## Brand the generated card
15
+
16
+ Set a local SVG and color palette to match the generated card to your brand. The logo can live in `public/` or at the project root. Omit any palette value to keep its default.
17
+
18
+ ```ts blume.config.ts lineNumbers
19
+ seo: {
20
+ og: {
21
+ logo: "/logo/og.svg",
22
+ palette: {
23
+ accent: "#ff5410",
24
+ background: "#1d1d1d",
25
+ foreground: "#fff6f2",
26
+ muted: "#a6a19f",
27
+ border: "#323232",
28
+ },
29
+ },
30
+ }
31
+ ```
32
+
33
+ By default, each card is derived from your content and theme — the **page title** as the headline, the **page description** as the subtitle (the same text as its `og:description`, so `seo.description` wins over `description`), your **site title** as the eyebrow, and your theme **accent** for the mark. Images are served at `/og/<slug>.png`, mirroring each route, and are prerendered as static files even in server mode:
34
+
35
+ | Page route | Image URL |
36
+ | ---------------- | ----------------------- |
37
+ | `/` | `/og/index.png` |
38
+ | `/quickstart` | `/og/quickstart.png` |
39
+ | `/guides/deploy` | `/og/guides/deploy.png` |
40
+
41
+ Override the generated card for any page with `seo.image` — a file in `public/` or an external URL. It takes precedence over the generated card and works even when `og` is off, so you can mix custom images with generated ones:
42
+
43
+ ```yaml lineNumbers
44
+ ---
45
+ title: Pricing
46
+ seo:
47
+ image: /og/pricing-custom.png
48
+ ---
49
+ ```
50
+
51
+ :::note
52
+ Every palette color accepts any CSS color — hex, `oklch(…)`, `rgb(…)`, and so on. The accent also accepts a named preset (`blue`, `teal`, …), matching [`theme.accent`](/docs/configuration/theming#accent). A color the renderer can't parse fails the build rather than silently shipping a default-colored card.
53
+ :::
54
+
55
+ Emoji in a page title or site title render as [Twemoji](https://github.com/jdecked/twemoji) glyphs, fetched from a CDN while the card renders — so a build whose titles contain emoji needs network access. Each glyph is fetched once per build, however many pages use it.
56
+
57
+ ## Show, hide, or override card layers
58
+
59
+ Beyond the headline, the card carries three optional layers: the **brand mark** in the top-left (your logo, or an accent tile with the site title's initial), the **subtitle** under the headline (the page `description`, or your site `description` for pages without one), and a **footer** with your repo slug (from `github`) and the site's URL — the deployment site's host plus [`deployment.base`](/docs/deployment#subpath-deploys), so a GitHub Pages project site reads `user.github.io/repo`. Override any of them with a string of your own, or hide one with `false`:
60
+
61
+ ```ts blume.config.ts lineNumbers
62
+ seo: {
63
+ og: {
64
+ site: "docs.acme.com", // footer URL text, or false to hide it
65
+ description: false, // hide the subtitle on every card; a string replaces the site fallback
66
+ logo: false, // no brand mark at all — not even the initial tile
67
+ },
68
+ }
69
+ ```
70
+
71
+ ## Card fonts
72
+
73
+ By default the card renders in Takumi's built-in font, which covers only Latin glyphs — a title in another script (Japanese, Chinese, Korean, Arabic, …) would render as tofu, empty boxes.
74
+
75
+ **Set [`theme.fonts`](/docs/configuration/theming#fonts) and the card follows it.** When your config picks its own fonts, the generated cards automatically render the headline in your display font and the description and footer in your body font, so shared links match the site — including non-Latin coverage, with nothing to configure here. (Families from non-Google providers are skipped — the card renderer can only fetch from Google Fonts — but local font files work.)
76
+
77
+ To use different fonts on cards than on the site, or to add script coverage without touching the theme, set `og.fonts` explicitly — it always wins over the theme-derived fonts:
78
+
79
+ ```ts blume.config.ts lineNumbers
80
+ seo: {
81
+ og: {
82
+ fonts: [
83
+ "Noto Sans JP",
84
+ { name: "Inter", weight: [400, 700] },
85
+ { name: "Berkeley Mono", src: "./fonts/BerkeleyMono-Regular.woff2" },
86
+ ],
87
+ },
88
+ }
89
+ ```
90
+
91
+ Each entry is a Google Fonts family name, an object pinning its `weight` (a number, a list, or a variable range like `"100..900"`) and `style` (`"normal"`, `"italic"`, or both), or a local font file — `src` resolves from the project root, with optional `weight` and `style` when the file's own metadata shouldn't decide.
92
+
93
+ Google families are fetched at build — so a build that uses them needs network access — and the renderer only pulls the glyph subsets each title actually uses. Fallback is per-glyph, so adding a family only affects glyphs the other fonts can't draw.
94
+
95
+ An explicit `og.fonts: []` opts out entirely: cards keep the built-in font even when `theme.fonts` is set.
96
+
97
+ ## Custom page titles
98
+
99
+ A custom [`.astro` page](/docs/advanced/custom-pages) has no frontmatter to read, so its generated card is titled by humanizing the last URL segment of its route — `/getting-started` becomes "Getting Started", but `/cli` becomes "Cli". Name those cards explicitly with `og.titles`, keyed by route (`"/"` addresses the home, whose card otherwise carries the site title):
100
+
101
+ ```ts blume.config.ts lineNumbers
102
+ seo: {
103
+ og: {
104
+ titles: {
105
+ "/cli": "CLI",
106
+ },
107
+ },
108
+ }
109
+ ```
110
+
111
+ Entries only apply to custom pages — a content page's card always takes its headline from the page title, so retitle those in frontmatter instead.
112
+
113
+ `seo.image` is frontmatter, so it only covers Markdown and MDX content. To give a custom [`.astro` page](/docs/advanced/custom-pages) its own social image — a marketing home or landing page, and the way to give the home page alone a bespoke share image — pass the `ogImage` prop to `PageLayout`.
@@ -0,0 +1,24 @@
1
+ ---
2
+ title: RSS feeds
3
+ description: A feed per dated content type — blog and changelog by default — served at /<type>/rss.xml and advertised to feed readers automatically.
4
+ ---
5
+
6
+ Blume builds an RSS feed for each content type in `rss.types` — `blog` and `changelog` by default — that has pages, served at `/<type>/rss.xml`. See [Feeds](/docs/content#feeds) for authoring blog and changelog entries with dates.
7
+
8
+ ```ts blume.config.ts lineNumbers
9
+ seo: {
10
+ rss: {
11
+ enabled: true,
12
+ types: ["blog", "changelog"],
13
+ limit: 50,
14
+ },
15
+ }
16
+ ```
17
+
18
+ | Option | Default | Description |
19
+ | --------- | ----------------------- | ------------------------------------- |
20
+ | `enabled` | `true` | Generate feeds. |
21
+ | `types` | `["blog", "changelog"]` | Content types that each get a feed. |
22
+ | `limit` | `50` | Maximum items per feed, newest first. |
23
+
24
+ Blume injects `<link rel="alternate">` tags so browsers and feed readers discover the feeds automatically, and lists them in the [agent readability manifest](/docs/discoverability/agent-discovery#agent-readability) so agents find them too. Item links are absolute when [`deployment.site`](/docs/deployment) is set.
@@ -0,0 +1,95 @@
1
+ ---
2
+ title: Sitemap and robots
3
+ description: The sitemap.xml and robots.txt Blume writes for crawlers, and the Content-Signal line that tells AI crawlers how they may use your docs.
4
+ ---
5
+
6
+ Search crawlers and AI crawlers both start from the same two files at your site root. Blume writes both on every build, and the `robots.txt` carries the content-usage signals that AI crawlers look for, so this is where the search side and the agent side of discoverability meet.
7
+
8
+ ## Sitemap
9
+
10
+ Blume writes a `sitemap.xml` of every indexable page at build time. It needs an absolute [`deployment.site`](/docs/deployment) and lists every page except drafts, hidden, and [`noindex`](/docs/discoverability/metadata#per-page-overrides) pages. On a [versioned](/docs/content/versioning) site, archived pages whose canonical points at their live equivalent are left out too — the live page is the one to index. On by default:
11
+
12
+ ```ts blume.config.ts lineNumbers
13
+ seo: {
14
+ sitemap: true,
15
+ }
16
+ ```
17
+
18
+ Ship your own `public/sitemap.xml` to take over — Blume never overwrites a file you place in `public/`.
19
+
20
+ ## Robots
21
+
22
+ Blume writes a `robots.txt` that allows all crawlers, declares your [content signals](#content-signals), and adds a `Sitemap:` line pointing to the sitemap when one is available. On by default:
23
+
24
+ ```ts blume.config.ts lineNumbers
25
+ seo: {
26
+ robots: true,
27
+ }
28
+ ```
29
+
30
+ ```txt robots.txt
31
+ User-agent: *
32
+ Content-Signal: search=yes, ai-input=yes, ai-train=yes
33
+ Allow: /
34
+
35
+ Sitemap: https://docs.example.com/sitemap.xml
36
+ ```
37
+
38
+ ### Content signals
39
+
40
+ The `Content-Signal` line — the emerging content-usage convention — declares how AI crawlers may reuse your docs. Blume emits it **on by default with every signal set to `yes`**, matching its stance that docs are open to humans and agents alike:
41
+
42
+ - `search` — traditional and AI search indexing
43
+ - `aiInput` — grounding / RAG at answer time
44
+ - `aiTrain` — model training
45
+
46
+ Restrict any signal by setting it to `false`; the ones you leave out stay `yes`:
47
+
48
+ ```ts blume.config.ts lineNumbers
49
+ seo: {
50
+ contentSignals: {
51
+ aiTrain: false, // opt out of training, keep search + grounding
52
+ },
53
+ }
54
+ ```
55
+
56
+ ```txt robots.txt
57
+ User-agent: *
58
+ Content-Signal: search=yes, ai-input=yes, ai-train=no
59
+ Allow: /
60
+ ```
61
+
62
+ Set `contentSignals: false` to drop the declaration entirely:
63
+
64
+ ```ts blume.config.ts lineNumbers
65
+ seo: {
66
+ contentSignals: false,
67
+ }
68
+ ```
69
+
70
+ <TypeTable
71
+ type={{
72
+ "seo.contentSignals": {
73
+ type: "boolean | object",
74
+ description:
75
+ "Content-Signal declaration. true or omitted emits all signals as yes; false drops the line; an object sets signals individually.",
76
+ },
77
+ "contentSignals.search": {
78
+ type: "boolean",
79
+ description: "Allow use for search indexing (search). Default true.",
80
+ },
81
+ "contentSignals.aiInput": {
82
+ type: "boolean",
83
+ description:
84
+ "Allow use for AI grounding / RAG at answer time (ai-input). Default true.",
85
+ },
86
+ "contentSignals.aiTrain": {
87
+ type: "boolean",
88
+ description: "Allow use for AI model training (ai-train). Default true.",
89
+ },
90
+ }}
91
+ />
92
+
93
+ Content signals express a preference, not access control: they tell well-behaved crawlers how you'd like your content used, and it's on the crawler to honor them. The same policy is mirrored as `contentUsage` in the [agent readability manifest](/docs/discoverability/agent-discovery#agent-readability), so an agent that never reads `robots.txt` still sees it.
94
+
95
+ Ship your own `public/robots.txt` to take over.
@@ -0,0 +1,51 @@
1
+ ---
2
+ title: Structured data
3
+ description: The schema.org JSON-LD every page carries — WebSite, article, and breadcrumb nodes — plus the optional Organization and SoftwareApplication blocks that tell search engines and agents who you are.
4
+ ---
5
+
6
+ Blume emits [schema.org](https://schema.org) JSON-LD in every page's `<head>` so search engines understand your content. On by default:
7
+
8
+ ```ts blume.config.ts lineNumbers
9
+ seo: {
10
+ structuredData: true,
11
+ }
12
+ ```
13
+
14
+ Each page includes:
15
+
16
+ - a **WebSite** node for site identity,
17
+ - the page as an **article** — `BlogPosting` for blog posts, `TechArticle` for changelog and docs — with its description and publish date,
18
+ - a **BreadcrumbList** built from the navigation trail.
19
+
20
+ URLs are absolute when [`deployment.site`](/docs/deployment) is set. Pages marked [`seo.noindex`](/docs/discoverability/metadata#per-page-overrides) are skipped. When [`lastModified`](/docs/configuration#last-modified) is on, each page's date is also emitted as `dateModified`.
21
+
22
+ ## Site identity
23
+
24
+ The WebSite node says a site exists; it doesn't say _what_ it is or _who_ runs it — which is what AI agents read JSON-LD for before they recommend or cite you. Two optional blocks fill that in, both needing `deployment.site` (the nodes carry absolute identifiers):
25
+
26
+ ```ts blume.config.ts lineNumbers
27
+ seo: {
28
+ organization: {
29
+ name: "Acme", // defaults to the site title
30
+ email: "hello@acme.com",
31
+ telephone: "+1 555 0100",
32
+ address: { addressLocality: "Sydney", addressCountry: "AU" },
33
+ logo: "/logo.svg",
34
+ sameAs: ["https://github.com/acme", "https://x.com/acme"],
35
+ },
36
+ software: {
37
+ license: "MIT",
38
+ operatingSystem: "Node.js 22+",
39
+ price: 0, // emitted as an Offer; 0 marks it free
40
+ sameAs: ["https://www.npmjs.com/package/acme"],
41
+ },
42
+ }
43
+ ```
44
+
45
+ `organization` adds an **Organization** node to every page — the WebSite and article nodes cite it as `publisher` — with the email and telephone as a `ContactPoint` (`contactType` defaults to `"customer support"`) and the address as a `PostalAddress`, the two fields business-verification checks look for. `name` and `url` default to the site's; a root-relative `logo` is absolutized like any page URL.
46
+
47
+ `software` adds a **SoftwareApplication** node to the homepage: the product's name and description (defaulting to the site's), `applicationCategory` (default `"DeveloperApplication"`), operating system, license, an `Offer` when `price` is set, and the registry or repository URLs in `sameAs`. Pass `software: true` to take every default. The organization, when configured, is cited as its `publisher`.
48
+
49
+ ## Custom pages
50
+
51
+ A custom [`.astro` page](/docs/advanced/custom-pages) rendered through `PageLayout` emits the same `WebSite` graph, so a marketing home page isn't the one URL without structured data. Pass `structuredDataEnabled={config.structuredData}` to keep it in sync with this setting, or `structuredDataEnabled={false}` to turn it off for that page alone.
package/docs/index.mdx CHANGED
@@ -31,7 +31,7 @@ Blume builds on Astro and Vite and renders static HTML by default — fast, cach
31
31
 
32
32
  ### AI-ready out of the box
33
33
 
34
- Every Blume site speaks fluent machine. It emits [`llms.txt` and `llms-full.txt`](/docs/configuration/ai), serves any page's raw Markdown by appending `.md` to its URL, exposes a [JSON API described by OpenAPI](/docs/configuration/ai#json-api), and gives readers **Copy as Markdown** and **Open in chat** actions on every page. Add an optional in-page **Ask AI** assistant, or host an [**MCP server**](/docs/configuration/ai#mcp-server) so coding agents like Claude Code and Cursor can search and read your docs directly — no scraping, no hosted service. Your Markdown is the source of truth for both humans and models.
34
+ Every Blume site speaks fluent machine. It emits [`llms.txt` and `llms-full.txt`](/docs/discoverability/llms-txt), serves any page's raw Markdown by appending `.md` to its URL, exposes a [JSON API described by OpenAPI](/docs/discoverability/json-api), and gives readers **Copy as Markdown** and **Open in chat** actions on every page. Add an optional in-page **Ask AI** assistant, or host an [**MCP server**](/docs/discoverability/mcp) so coding agents like Claude Code and Cursor can search and read your docs directly — no scraping, no hosted service. Your Markdown is the source of truth for both humans and models.
35
35
 
36
36
  ### Zero configuration — even the template
37
37
 
@@ -45,9 +45,9 @@ Your [`blume.config.ts`](/docs/configuration) and every [`meta.ts`](/docs/conten
45
45
 
46
46
  - **Components** — callouts, cards, steps, tabs, accordions, badges, file trees, and parameter tables, usable in MDX with [no imports](/docs/content/components).
47
47
  - **Local search** — Orama works in dev and production; Pagefind is one flag away for large sites. No hosted index.
48
- - **AI** — [`llms.txt`, raw Markdown URLs, a JSON API with an OpenAPI description, Copy as Markdown, Open in chat, an Ask AI assistant, and a hosted MCP server](/docs/configuration/ai).
48
+ - **AI** — [`llms.txt`, raw Markdown URLs, a JSON API with an OpenAPI description, Copy as Markdown, Open in chat, and a hosted MCP server](/docs/discoverability), plus an optional [Ask AI assistant](/docs/configuration/ask-ai).
49
49
  - **Navigation** — inferred from files, refined with `meta.ts` or config.
50
- - **SEO** — metadata, Open Graph images, RSS feeds, and JSON-LD, [built in](/docs/configuration/seo).
50
+ - **SEO** — metadata, Open Graph images, RSS feeds, and JSON-LD, [built in](/docs/discoverability).
51
51
  - **Customization** — component overrides, React islands, custom pages, theme tokens, and a source-component registry via `blume add`.
52
52
  - **Eject** — `blume eject` produces a standalone Astro project that still uses the `blume` package.
53
53
 
@@ -64,8 +64,8 @@ The Blume CLI discovers your content, builds a content graph, and generates a hi
64
64
  <Card title="Components" href="/docs/content/components" icon="folder">
65
65
  Explore the built-in component library.
66
66
  </Card>
67
- <Card title="AI" href="/docs/configuration/ai" icon="lightbulb">
68
- Ship `llms.txt` and an Ask AI assistant.
67
+ <Card title="Discoverability" href="/docs/discoverability" icon="lightbulb">
68
+ Ship `llms.txt`, social cards, and an MCP server.
69
69
  </Card>
70
70
  <Card title="CLI" href="/docs/reference/cli" icon="rocket">
71
71
  Every `blume` command and flag.
@@ -26,7 +26,7 @@ blume eval 3 question(s) · Claude Code
26
26
 
27
27
  Each question runs through two agent sessions, using an agent CLI you already have installed — [Claude Code](https://claude.com/claude-code) by default, or [Codex](https://developers.openai.com/codex/cli) with `--agent codex`. Blume holds no API keys and calls no model itself.
28
28
 
29
- 1. **The reader** answers the question using _only_ your documentation. It runs in an empty directory with its file, shell, and web tools disabled, connected to a private [MCP server](/docs/configuration/ai) that serves your docs — the same `search_docs`/`get_page` tools a real agent uses against your deployed site. It cannot read your repo, so it experiences the docs exactly like a fresh user: what isn't written doesn't exist.
29
+ 1. **The reader** answers the question using _only_ your documentation. It runs in an empty directory with its file, shell, and web tools disabled, connected to a private [MCP server](/docs/discoverability/mcp) that serves your docs — the same `search_docs`/`get_page` tools a real agent uses against your deployed site. It cannot read your repo, so it experiences the docs exactly like a fresh user: what isn't written doesn't exist.
30
30
  2. **The judge** grades the answer against the facts you listed, with no tools at all. Paraphrase passes; a missing or contradicted fact fails — and so does "the documentation doesn't say."
31
31
 
32
32
  The MCP snapshot is built from your content sources directly, so there is no need to run `blume build` first, and nothing is deployed or uploaded anywhere.
@@ -1,7 +1,7 @@
1
1
  import { defineMeta } from "blume";
2
2
 
3
3
  export default defineMeta({
4
- order: 6,
4
+ order: 7,
5
5
  pages: ["frontmatter", "cli", "eval", "translate"],
6
6
  title: "Reference",
7
7
  });
@@ -44,6 +44,7 @@ The agent is never trusted with structure. Before writing, Blume checks each rep
44
44
 
45
45
  - The frontmatter is reconstructed from the source file's data, with only the six translatable values overlaid — keys the agent invented are dropped, keys it deleted are restored, and `slug`, `icon`, `order`, and dates are source-verbatim by construction.
46
46
  - The number of code fences must match the source, the body must be non-empty, and the frontmatter must parse.
47
+ - Every heading is pinned to its source heading's anchor id with a trailing [`[#id]` marker](/docs/content/syntax#custom-anchors), unless the translation already pins one, so `#fragment` links resolve identically in every language. Headings pair up positionally, so a translation whose heading structure doesn't match the source gets no pins.
47
48
 
48
49
  A reply that fails validation writes nothing — the item is reported as failed and the run moves on. Everything that succeeded stays stamped in the ledger, so a rerun retries only the failures.
49
50
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "blume",
3
- "version": "1.6.2",
3
+ "version": "1.6.4",
4
4
  "description": "Documentation that's fast, AI-ready, and zero-config.",
5
5
  "keywords": [
6
6
  "astro",
@@ -67,21 +67,21 @@
67
67
  },
68
68
  "dependencies": {
69
69
  "@astrojs/check": "^0.9.10",
70
- "@astrojs/markdown-satteri": "^0.4.0",
71
- "@astrojs/mdx": "^8.0.0",
70
+ "@astrojs/markdown-satteri": "^0.4.1",
71
+ "@astrojs/mdx": "^8.0.1",
72
72
  "@astrojs/node": "^11.1.5",
73
73
  "@astrojs/react": "^6.0.5",
74
74
  "@astrojs/vercel": "^11.0.10",
75
75
  "@asyncapi/converter": "^2.0.2",
76
- "@clack/prompts": "^1.7.0",
77
- "@iconify-json/lucide": "^1.2.129",
76
+ "@clack/prompts": "^1.8.0",
77
+ "@iconify-json/lucide": "^1.2.130",
78
78
  "@iconify/types": "^2.0.0",
79
- "@iconify/utils": "^3.1.5",
79
+ "@iconify/utils": "^3.1.7",
80
80
  "@modelcontextprotocol/sdk": "^1.30.0",
81
81
  "@orama/orama": "^3.1.18",
82
82
  "@pierre/diffs": "^1.4.1",
83
- "@scalar/astro": "^0.4.17",
84
- "@scalar/openapi-parser": "^0.29.0",
83
+ "@scalar/astro": "^0.4.18",
84
+ "@scalar/openapi-parser": "^0.29.1",
85
85
  "@scalar/openapi-types": "^0.9.5",
86
86
  "@shikijs/transformers": "^4.4.3",
87
87
  "@shikijs/twoslash": "^4.4.3",
@@ -89,14 +89,14 @@
89
89
  "@tailwindcss/vite": "^4.3.3",
90
90
  "@types/mdast": "^4.0.4",
91
91
  "@vercel/analytics": "^2.0.1",
92
- "ai": "^7.0.93",
93
- "astro": "^7.3.1",
92
+ "ai": "^7.0.94",
93
+ "astro": "^7.3.2",
94
94
  "babel-plugin-react-compiler": "^1.0.0",
95
95
  "chokidar": "^5.0.0",
96
96
  "citty": "^0.2.2",
97
97
  "consola": "^3.4.2",
98
98
  "cross-spawn": "^7.0.6",
99
- "dompurify": "^3.4.14",
99
+ "dompurify": "^3.4.15",
100
100
  "dotenv": "^17.4.2",
101
101
  "epub-gen-memory": "^1.1.2",
102
102
  "fast-xml-parser": "^5.11.1",
@@ -107,9 +107,9 @@
107
107
  "image-size": "^2.0.2",
108
108
  "jiti": "^2.7.0",
109
109
  "js-yaml": "^5.4.1",
110
- "katex": "^0.18.6",
110
+ "katex": "^0.18.7",
111
111
  "markdown-table": "^3.0.4",
112
- "marked": "^18.0.11",
112
+ "marked": "^18.0.12",
113
113
  "mdast-util-from-markdown": "^2.0.3",
114
114
  "mdast-util-gfm": "^3.1.0",
115
115
  "mdast-util-to-string": "^4.0.0",
@@ -117,7 +117,7 @@
117
117
  "mermaid": "^11.17.2",
118
118
  "micromark-extension-gfm": "^3.0.0",
119
119
  "nanotar": "^0.3.0",
120
- "node-html-parser": "^9.0.3",
120
+ "node-html-parser": "^9.0.4",
121
121
  "openapi-sampler": "^1.7.5",
122
122
  "p-limit": "^7.3.2",
123
123
  "p-map": "^7.0.7",
@@ -134,11 +134,11 @@
134
134
  "semver": "^7.8.5",
135
135
  "sharp": "^0.35.4",
136
136
  "shiki": "^4.4.3",
137
- "simple-icons": "^16.29.0",
137
+ "simple-icons": "^16.30.0",
138
138
  "string-width": "^8.2.2",
139
139
  "sucrase": "^3.35.1",
140
140
  "tailwindcss": "^4.3.3",
141
- "takumi-js": "^2.13.6",
141
+ "takumi-js": "^2.13.7",
142
142
  "tinyglobby": "^0.2.17",
143
143
  "twoslash": "^0.3.9",
144
144
  "typescript": "^6.0.3",
@@ -148,12 +148,12 @@
148
148
  "zod": "^4.5.4"
149
149
  },
150
150
  "devDependencies": {
151
- "@ai-sdk/openai-compatible": "^3.0.44",
151
+ "@ai-sdk/openai-compatible": "^3.0.45",
152
152
  "@mixedbread/sdk": "^0.77.0",
153
153
  "@notionhq/client": "^5.26.0",
154
154
  "@openrouter/ai-sdk-provider": "^3.0.0",
155
155
  "@oramacloud/client": "^2.1.4",
156
- "@sanity/client": "^8.5.0",
156
+ "@sanity/client": "^8.6.1",
157
157
  "@types/cross-spawn": "^6.0.6",
158
158
  "@types/html-escaper": "^3.0.4",
159
159
  "@types/node": "^22.20.1",
@@ -64,7 +64,7 @@ export type EvaluatedValue =
64
64
  | EvaluatedValue[]
65
65
  | { [key: string]: EvaluatedValue };
66
66
 
67
- const isString = <Value>(value: Value): value is Value & string =>
67
+ export const isString = <Value>(value: Value): value is Value & string =>
68
68
  typeof value === "string";
69
69
 
70
70
  const isNumber = <Value>(value: Value): value is Value & number =>
@@ -384,7 +384,7 @@ const linkText = (value: string): string =>
384
384
  * part of a balanced pair — so an href carrying either goes in the angle
385
385
  * bracket form, where only `<` and `>` are special.
386
386
  */
387
- const linkDestination = (href: string): string =>
387
+ export const linkDestination = (href: string): string =>
388
388
  /[\s()<>]/u.test(href)
389
389
  ? `<${href.replaceAll(/[<>]/gu, String.raw`\$&`)}>`
390
390
  : href;
@@ -471,6 +471,21 @@ const youtube: ComponentMarkdown = ({ props }) => {
471
471
  return `[${title}](https://www.youtube.com/watch?v=${videoId}${start})`;
472
472
  };
473
473
 
474
+ /**
475
+ * `text` as an inline code span whose delimiter outlengths any backtick run
476
+ * inside it, padded with a space when the content starts or ends with a
477
+ * backtick (CommonMark strips one such pair) — so a spec path or address
478
+ * carrying a backtick cannot close the span early.
479
+ */
480
+ export const inlineCode = (text: string): string => {
481
+ const runs = text.match(/`+/gu);
482
+ const longest = runs ? Math.max(...runs.map((run) => run.length)) : 0;
483
+ const delimiter = "`".repeat(longest + 1);
484
+ const padded =
485
+ text.startsWith("`") || text.endsWith("`") ? ` ${text} ` : text;
486
+ return `${delimiter}${padded}${delimiter}`;
487
+ };
488
+
474
489
  /** Fence `code` so its opening/closing run outlengths any backticks inside. */
475
490
  const fencedBlock = (lang: string, code: string): string => {
476
491
  const trimmed = code.replace(/(?<!\n)\n+$/u, "");
package/src/ai/llms.ts CHANGED
@@ -8,10 +8,8 @@ import type { NavNode, Navigation, PageRecord } from "../core/types.ts";
8
8
  import { buildRssFeeds } from "../deploy/rss.ts";
9
9
  import { API_CATALOG_PATH, hasApiCatalog } from "./api-catalog.ts";
10
10
  import { API_PAGES_PATH, OPENAPI_PATH } from "./api/paths.ts";
11
- import {
12
- downlevelComponents,
13
- exampleComponentSerializers,
14
- } from "./component-markdown.ts";
11
+ import { downlevelComponents } from "./component-markdown.ts";
12
+ import { projectComponentSerializers } from "./serializers.ts";
15
13
  import { AGENT_SKILLS_DIR, AGENT_SKILLS_INDEX_PATH } from "./skills.ts";
16
14
  import type { SkillArtifact } from "./skills.ts";
17
15
  import { applyAgentVisibility } from "./visibility.ts";
@@ -292,12 +290,7 @@ const buildFull = async (project: BlumeProject): Promise<string> => {
292
290
  const pages = eligiblePages(project, { versions: "current" }).toSorted(
293
291
  (a, b) => a.route.localeCompare(b.route)
294
292
  );
295
- // Downlevel `<Component>` to its example's source; a same-name user
296
- // `markdownComponents` entry is spread last and still wins.
297
- const components = {
298
- ...exampleComponentSerializers(project.examples ?? {}),
299
- ...config.ai.markdownComponents,
300
- };
293
+ const components = projectComponentSerializers(project);
301
294
 
302
295
  const sections = await Promise.all(
303
296
  pages.map(async (page) => {
@@ -5,11 +5,9 @@ import matter from "../core/frontmatter.ts";
5
5
  import type { BlumeProject } from "../core/project-graph.ts";
6
6
  import { readExpandedEntryText } from "../core/sources/read.ts";
7
7
  import type { RouteManifestEntry } from "../core/types.ts";
8
- import {
9
- downlevelComponents,
10
- exampleComponentSerializers,
11
- } from "./component-markdown.ts";
8
+ import { downlevelComponents } from "./component-markdown.ts";
12
9
  import { buildLlmsIndex } from "./llms.ts";
10
+ import { projectComponentSerializers } from "./serializers.ts";
13
11
  import { applyAgentVisibility } from "./visibility.ts";
14
12
 
15
13
  /** One route's raw-Markdown variants. */
@@ -55,12 +53,7 @@ export const buildRawMarkdown = async (
55
53
  ): Promise<Record<string, RawMarkdownEntry>> => {
56
54
  const pageById = new Map(project.graph.pages.map((page) => [page.id, page]));
57
55
 
58
- // Downlevel `<Component>` to its example's source. A user `markdownComponents`
59
- // entry of the same name is spread last, so it still wins.
60
- const components = {
61
- ...exampleComponentSerializers(project.examples ?? {}),
62
- ...project.config.ai.markdownComponents,
63
- };
56
+ const components = projectComponentSerializers(project);
64
57
 
65
58
  const readRoute = async (route: RouteManifestEntry): Promise<string> => {
66
59
  const page = pageById.get(route.id);