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.
- package/CHANGELOG.md +25 -0
- package/dist/cli/index.js +307 -68
- package/dist/cli/index.js.map +28 -23
- package/dist/types/ai/component-markdown.d.ts +14 -0
- package/docs/01-quickstart.mdx +2 -2
- package/docs/02-deployment.mdx +5 -5
- package/docs/{07-faq.mdx → 08-faq.mdx} +7 -7
- package/docs/advanced/blog.mdx +3 -3
- package/docs/advanced/changelog.mdx +2 -2
- package/docs/advanced/custom-pages.mdx +4 -4
- package/docs/advanced/meta.ts +1 -1
- package/docs/configuration/ask-ai.mdx +179 -0
- package/docs/configuration/index.mdx +8 -7
- package/docs/configuration/meta.ts +1 -2
- package/docs/configuration/search.mdx +1 -1
- package/docs/configuration/theming.mdx +1 -1
- package/docs/content/components.mdx +1 -1
- package/docs/content/i18n.mdx +7 -1
- package/docs/content/index.mdx +1 -1
- package/docs/content/navigation.mdx +2 -2
- package/docs/content/syntax.mdx +1 -1
- package/docs/discoverability/agent-discovery.mdx +196 -0
- package/docs/discoverability/index.mdx +48 -0
- package/docs/discoverability/json-api.mdx +58 -0
- package/docs/discoverability/llms-txt.mdx +68 -0
- package/docs/discoverability/markdown.mdx +76 -0
- package/docs/discoverability/mcp.mdx +64 -0
- package/docs/discoverability/meta.ts +18 -0
- package/docs/discoverability/metadata.mdx +82 -0
- package/docs/discoverability/open-graph.mdx +113 -0
- package/docs/discoverability/rss.mdx +24 -0
- package/docs/discoverability/sitemap-and-robots.mdx +95 -0
- package/docs/discoverability/structured-data.mdx +51 -0
- package/docs/index.mdx +5 -5
- package/docs/reference/eval.mdx +1 -1
- package/docs/reference/meta.ts +1 -1
- package/docs/reference/translate.mdx +1 -0
- package/package.json +18 -18
- package/src/ai/component-markdown.ts +17 -2
- package/src/ai/llms.ts +3 -10
- package/src/ai/markdown.ts +3 -10
- package/src/ai/openapi-components.ts +123 -0
- package/src/ai/serializers.ts +24 -0
- package/src/astro/templates.ts +42 -12
- package/src/audit/checks/links.ts +1 -8
- package/src/audit/checks/llms.ts +5 -4
- package/src/audit/redirects.ts +4 -3
- package/src/audit/run.ts +6 -8
- package/src/audit/url.ts +33 -0
- package/src/cli/commands/validate.ts +1 -0
- package/src/components/content/Component.astro +65 -68
- package/src/components/content/Tabs.astro +24 -9
- package/src/components/content/example-pane.ts +6 -0
- package/src/components/layout/LocaleLinks.astro +42 -0
- package/src/components/layout/PageLayout.astro +5 -3
- package/src/components/layout/ReferenceLayout.astro +5 -0
- package/src/components/layout/RootLayout.astro +110 -39
- package/src/components/layout/search-locale.ts +13 -0
- package/src/components/openapi/ApiOverview.astro +7 -39
- package/src/components/openapi/ApiTagOperations.astro +2 -1
- package/src/components/openapi/AsyncApiOperation.astro +3 -2
- package/src/components/openapi/GraphqlOperation.astro +3 -2
- package/src/components/openapi/Operation.astro +3 -2
- package/src/core/i18n.ts +13 -2
- package/src/core/links.ts +33 -1
- package/src/core/locale-links.ts +163 -0
- package/src/core/sources/normalize.ts +57 -7
- package/src/markdown/package-commands.ts +27 -3
- package/src/openapi/graphql.ts +29 -0
- package/src/openapi/model.ts +69 -0
- package/src/openapi/render-mdx.ts +3 -2
- package/src/openapi/signature.ts +18 -0
- package/src/search/documents.ts +4 -9
- package/src/theme/code-block-padding.ts +0 -8
- package/src/theme/entry.ts +33 -27
- package/src/translate/anchors.ts +91 -0
- package/src/translate/validate.ts +8 -3
- package/docs/configuration/ai.mdx +0 -613
- 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/
|
|
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,
|
|
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/
|
|
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="
|
|
68
|
-
Ship `llms.txt
|
|
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.
|
package/docs/reference/eval.mdx
CHANGED
|
@@ -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/
|
|
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.
|
package/docs/reference/meta.ts
CHANGED
|
@@ -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.
|
|
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.
|
|
71
|
-
"@astrojs/mdx": "^8.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.
|
|
77
|
-
"@iconify-json/lucide": "^1.2.
|
|
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.
|
|
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.
|
|
84
|
-
"@scalar/openapi-parser": "^0.29.
|
|
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
|
-
"astro": "^7.3.
|
|
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.
|
|
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.
|
|
110
|
+
"katex": "^0.18.7",
|
|
111
111
|
"markdown-table": "^3.0.4",
|
|
112
|
-
"marked": "^18.0.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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) => {
|
package/src/ai/markdown.ts
CHANGED
|
@@ -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
|
-
|
|
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);
|