blume 2.2.2 → 2.2.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 +18 -0
- package/THIRD-PARTY-NOTICES.md +51 -0
- package/dist/cli/{chunk-kmswjxj9.js → chunk-0b1a8xqz.js} +3 -3
- package/dist/cli/{chunk-rtvgmg2j.js → chunk-0qx6vwwf.js} +2 -2
- package/dist/cli/{chunk-qcyxkj44.js → chunk-3ps63r91.js} +43 -17
- package/dist/cli/chunk-3ps63r91.js.map +15 -0
- package/dist/cli/{chunk-b680sv30.js → chunk-402xaaxn.js} +10 -8
- package/dist/cli/chunk-402xaaxn.js.map +13 -0
- package/dist/cli/{chunk-k45sq16q.js → chunk-49xa4yf2.js} +39 -23
- package/dist/cli/{chunk-k45sq16q.js.map → chunk-49xa4yf2.js.map} +3 -3
- package/dist/cli/{chunk-j4nfgn2j.js → chunk-51wr1wre.js} +1 -1
- package/dist/cli/{chunk-9jqmq5hv.js → chunk-531qbwn1.js} +6 -6
- package/dist/cli/{chunk-r5nm9w0c.js → chunk-5bp65pv4.js} +107 -37
- package/dist/cli/{chunk-r5nm9w0c.js.map → chunk-5bp65pv4.js.map} +11 -10
- package/dist/cli/{chunk-yg8ckc3g.js → chunk-6nvcjzts.js} +2 -2
- package/dist/cli/{chunk-x7bh6d9j.js → chunk-6v5w8vzd.js} +4 -4
- package/dist/cli/{chunk-f260jdrn.js → chunk-8cshtem6.js} +5 -5
- package/dist/cli/{chunk-ydqzkhjn.js → chunk-8h5ceyk0.js} +6 -6
- package/dist/cli/{chunk-e1n41w9m.js → chunk-8v90n67z.js} +3 -3
- package/dist/cli/{chunk-etckvbrh.js → chunk-98me5yas.js} +14 -14
- package/dist/cli/{chunk-hktq4j5r.js → chunk-9k7gg4ga.js} +16 -10
- package/dist/cli/{chunk-hktq4j5r.js.map → chunk-9k7gg4ga.js.map} +5 -5
- package/dist/cli/{chunk-34wap3ed.js → chunk-9qmc57zw.js} +3 -3
- package/dist/cli/{chunk-beb0z983.js → chunk-a1xmzxk2.js} +16 -16
- package/dist/cli/{chunk-gwy55cx3.js → chunk-ajw85dgf.js} +2 -2
- package/dist/cli/{chunk-69rjf786.js → chunk-ansqj08e.js} +5 -5
- package/dist/cli/{chunk-q1wwrxzw.js → chunk-b0vwhmdf.js} +3 -3
- package/dist/cli/chunk-b0vwhmdf.js.map +10 -0
- package/dist/cli/{chunk-nqna8dzp.js → chunk-b5gztt4j.js} +17 -17
- package/dist/cli/{chunk-fnxgtjeq.js → chunk-bc4jz2m4.js} +1 -1
- package/dist/cli/{chunk-aafhwx74.js → chunk-d4h1y312.js} +1 -1
- package/dist/cli/{chunk-080mfch6.js → chunk-et7zwqkg.js} +15 -15
- package/dist/cli/{chunk-487b95qy.js → chunk-f29q3j13.js} +41 -25
- package/dist/cli/{chunk-487b95qy.js.map → chunk-f29q3j13.js.map} +3 -3
- package/dist/cli/{chunk-c5bjxq8v.js → chunk-f72388jn.js} +2 -2
- package/dist/cli/{chunk-t2h8pwy1.js → chunk-g0hpwbyx.js} +4 -4
- package/dist/cli/{chunk-40vk8kgf.js → chunk-j102xmnk.js} +7 -7
- package/dist/cli/{chunk-ppa6mg4d.js → chunk-m54bz9ng.js} +6 -6
- package/dist/cli/{chunk-h0qwdbem.js → chunk-mt6njy68.js} +2 -2
- package/dist/cli/{chunk-2ssh60qh.js → chunk-n091sgam.js} +6 -6
- package/dist/cli/{chunk-1ch3ct3r.js → chunk-p33bf2nb.js} +14 -14
- package/dist/cli/{chunk-4zxh25hc.js → chunk-saxq5jjr.js} +2 -2
- package/dist/cli/{chunk-x60e15st.js → chunk-sbt1s5j2.js} +17 -17
- package/dist/cli/{chunk-gxhcf2hs.js → chunk-tckd6qwv.js} +2 -2
- package/dist/cli/{chunk-p834rkcn.js → chunk-tf4134kk.js} +5 -5
- package/dist/cli/{chunk-zxd5jrgx.js → chunk-vxdjss7f.js} +76 -34
- package/dist/cli/{chunk-zxd5jrgx.js.map → chunk-vxdjss7f.js.map} +4 -4
- package/dist/cli/{chunk-9h8mh2tk.js → chunk-x2pgg22t.js} +1 -1
- package/dist/cli/{chunk-hn3g2b89.js → chunk-xhrwrzrh.js} +9 -9
- package/dist/cli/index.js +18 -18
- package/dist/types/ai/ask-context.d.ts +8 -0
- package/dist/types/ai/link-text.d.ts +5 -0
- package/dist/types/ai/relative-links.d.ts +26 -6
- package/dist/types/core/config-input.d.ts +41 -1
- package/dist/types/core/data.d.ts +14 -0
- package/dist/types/core/last-modified.d.ts +33 -18
- package/dist/types/core/links.d.ts +16 -0
- package/dist/types/core/schema.d.ts +17 -1
- package/dist/types/core/types.d.ts +10 -0
- package/dist/types/deploy/sitemap.d.ts +9 -0
- package/dist/types/search/documents.d.ts +3 -1
- package/dist/types/search/fold.d.ts +33 -0
- package/dist/types/search/orama-index.d.ts +4 -4
- package/dist/types/seo/jsonld.d.ts +33 -0
- package/docs/configuration/search.mdx +2 -2
- package/docs/configuration/theming.mdx +13 -1
- package/docs/content/frontmatter.mdx +1 -1
- package/docs/content/i18n.mdx +1 -1
- package/docs/content/sources/remote-mdx.mdx +1 -1
- package/docs/discoverability/open-graph.mdx +16 -1
- package/docs/discoverability/sitemap-and-robots.mdx +12 -0
- package/docs/discoverability/structured-data.mdx +44 -0
- package/package.json +4 -3
- package/skills/blume-migrate/references/docus.md +1 -2
- package/skills/blume-migrate/references/docusaurus.md +1 -1
- package/skills/blume-migrate/references/vitepress.md +2 -2
- package/skills/blume-migrate/references/vuepress.md +1 -1
- package/src/ai/ask-context.ts +52 -14
- package/src/ai/ask-data.ts +4 -1
- package/src/ai/ask-tools.ts +17 -5
- package/src/ai/link-text.ts +6 -0
- package/src/ai/llms.ts +1 -7
- package/src/ai/markdown.ts +8 -4
- package/src/ai/relative-links.ts +155 -48
- package/src/ai/site-skill.ts +1 -4
- package/src/astro/generate.ts +7 -0
- package/src/astro/templates.ts +36 -20
- package/src/components/layout/Header.astro +18 -10
- package/src/components/layout/PageActions.astro +1 -0
- package/src/components/layout/PageLayout.astro +31 -13
- package/src/components/layout/ReferenceLayout.astro +6 -1
- package/src/components/layout/RootLayout.astro +13 -1
- package/src/components/layout/head-scripts.ts +5 -1
- package/src/components/layout/locale-redirect.ts +38 -19
- package/src/components/layout/search/types.ts +12 -4
- package/src/core/config-input.ts +40 -1
- package/src/core/config.ts +5 -2
- package/src/core/data.ts +14 -0
- package/src/core/last-modified.ts +80 -34
- package/src/core/links.ts +43 -2
- package/src/core/manifest.ts +2 -0
- package/src/core/project-graph.ts +83 -43
- package/src/core/schema.ts +48 -9
- package/src/core/types.ts +10 -0
- package/src/deploy/sitemap.ts +127 -49
- package/src/search/documents.ts +34 -11
- package/src/search/fold.ts +175 -0
- package/src/search/orama-index.ts +40 -49
- package/src/seo/jsonld.ts +100 -23
- package/src/theme/chrome-icons.ts +2 -1
- package/src/theme/entry.ts +4 -1
- package/dist/cli/chunk-b680sv30.js.map +0 -13
- package/dist/cli/chunk-q1wwrxzw.js.map +0 -10
- package/dist/cli/chunk-qcyxkj44.js.map +0 -14
- /package/dist/cli/{chunk-kmswjxj9.js.map → chunk-0b1a8xqz.js.map} +0 -0
- /package/dist/cli/{chunk-rtvgmg2j.js.map → chunk-0qx6vwwf.js.map} +0 -0
- /package/dist/cli/{chunk-j4nfgn2j.js.map → chunk-51wr1wre.js.map} +0 -0
- /package/dist/cli/{chunk-9jqmq5hv.js.map → chunk-531qbwn1.js.map} +0 -0
- /package/dist/cli/{chunk-yg8ckc3g.js.map → chunk-6nvcjzts.js.map} +0 -0
- /package/dist/cli/{chunk-x7bh6d9j.js.map → chunk-6v5w8vzd.js.map} +0 -0
- /package/dist/cli/{chunk-f260jdrn.js.map → chunk-8cshtem6.js.map} +0 -0
- /package/dist/cli/{chunk-ydqzkhjn.js.map → chunk-8h5ceyk0.js.map} +0 -0
- /package/dist/cli/{chunk-e1n41w9m.js.map → chunk-8v90n67z.js.map} +0 -0
- /package/dist/cli/{chunk-etckvbrh.js.map → chunk-98me5yas.js.map} +0 -0
- /package/dist/cli/{chunk-34wap3ed.js.map → chunk-9qmc57zw.js.map} +0 -0
- /package/dist/cli/{chunk-beb0z983.js.map → chunk-a1xmzxk2.js.map} +0 -0
- /package/dist/cli/{chunk-gwy55cx3.js.map → chunk-ajw85dgf.js.map} +0 -0
- /package/dist/cli/{chunk-69rjf786.js.map → chunk-ansqj08e.js.map} +0 -0
- /package/dist/cli/{chunk-nqna8dzp.js.map → chunk-b5gztt4j.js.map} +0 -0
- /package/dist/cli/{chunk-fnxgtjeq.js.map → chunk-bc4jz2m4.js.map} +0 -0
- /package/dist/cli/{chunk-aafhwx74.js.map → chunk-d4h1y312.js.map} +0 -0
- /package/dist/cli/{chunk-080mfch6.js.map → chunk-et7zwqkg.js.map} +0 -0
- /package/dist/cli/{chunk-c5bjxq8v.js.map → chunk-f72388jn.js.map} +0 -0
- /package/dist/cli/{chunk-t2h8pwy1.js.map → chunk-g0hpwbyx.js.map} +0 -0
- /package/dist/cli/{chunk-40vk8kgf.js.map → chunk-j102xmnk.js.map} +0 -0
- /package/dist/cli/{chunk-ppa6mg4d.js.map → chunk-m54bz9ng.js.map} +0 -0
- /package/dist/cli/{chunk-h0qwdbem.js.map → chunk-mt6njy68.js.map} +0 -0
- /package/dist/cli/{chunk-2ssh60qh.js.map → chunk-n091sgam.js.map} +0 -0
- /package/dist/cli/{chunk-1ch3ct3r.js.map → chunk-p33bf2nb.js.map} +0 -0
- /package/dist/cli/{chunk-4zxh25hc.js.map → chunk-saxq5jjr.js.map} +0 -0
- /package/dist/cli/{chunk-x60e15st.js.map → chunk-sbt1s5j2.js.map} +0 -0
- /package/dist/cli/{chunk-gxhcf2hs.js.map → chunk-tckd6qwv.js.map} +0 -0
- /package/dist/cli/{chunk-p834rkcn.js.map → chunk-tf4134kk.js.map} +0 -0
- /package/dist/cli/{chunk-9h8mh2tk.js.map → chunk-x2pgg22t.js.map} +0 -0
- /package/dist/cli/{chunk-hn3g2b89.js.map → chunk-xhrwrzrh.js.map} +0 -0
|
@@ -14,6 +14,7 @@ theme: {
|
|
|
14
14
|
accent: "teal", // a named preset or any CSS color
|
|
15
15
|
radius: "md", // none | sm | md | lg
|
|
16
16
|
mode: "system", // system | light | dark
|
|
17
|
+
toggle: true, // show the light/dark toggle
|
|
17
18
|
fonts: { // self-hosted Google Fonts
|
|
18
19
|
display: "inter",
|
|
19
20
|
body: "inter",
|
|
@@ -51,7 +52,18 @@ Text on an accent or action fill, like a button label or a step number, is white
|
|
|
51
52
|
- **`system`** (default) — follow the reader's OS preference
|
|
52
53
|
- **`light`** / **`dark`** — default to one scheme
|
|
53
54
|
|
|
54
|
-
A toggle in the header
|
|
55
|
+
A toggle in the header lets readers switch, and their choice is remembered across visits. Dark mode is applied with a `data-theme="dark"` attribute on the `<html>` element.
|
|
56
|
+
|
|
57
|
+
To ship a single scheme, set `toggle: false`. The header drops the toggle, and every page renders in `mode`, even for readers who picked the other scheme before the toggle was removed:
|
|
58
|
+
|
|
59
|
+
```ts blume.config.ts lineNumbers
|
|
60
|
+
theme: {
|
|
61
|
+
mode: "dark",
|
|
62
|
+
toggle: false,
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
With `mode: "system"`, `toggle: false` follows each reader's OS setting and gives them no way to override it.
|
|
55
67
|
|
|
56
68
|
### Fonts
|
|
57
69
|
|
|
@@ -23,7 +23,7 @@ Frontmatter is YAML, so quote a value that holds a colon followed by a space, li
|
|
|
23
23
|
authors: {
|
|
24
24
|
type: "string | string[] | object[]",
|
|
25
25
|
description:
|
|
26
|
-
"Post author(s) for blog/changelog content — a name, or objects with a name plus optional avatar/url and any extra fields.
|
|
26
|
+
"Post author(s) for blog/changelog content — a name, or objects with a name plus optional avatar/url and any extra fields. Credited as Person nodes in the page's JSON-LD; other fields are preserved as-is.",
|
|
27
27
|
},
|
|
28
28
|
slug: {
|
|
29
29
|
type: "string",
|
package/docs/content/i18n.mdx
CHANGED
|
@@ -186,7 +186,7 @@ i18n: {
|
|
|
186
186
|
Localized SEO is handled for you — no per-page metadata to write:
|
|
187
187
|
|
|
188
188
|
- `<html lang>` and `dir` are set from the active locale.
|
|
189
|
-
- `hreflang` alternates link every real translation of a page, plus an `x-default` pointing at the default locale.
|
|
189
|
+
- `hreflang` alternates link every real translation of a page that search engines may index (a `noindex`, hidden, or canonical-elsewhere translation is left out), plus an `x-default` pointing at the default locale.
|
|
190
190
|
- Canonical URLs are locale-correct, and JSON-LD carries `inLanguage`.
|
|
191
191
|
|
|
192
192
|
Set [`deployment.site`](/docs/deployment) so these can be emitted as absolute URLs.
|
|
@@ -35,7 +35,7 @@ mdxRemote({
|
|
|
35
35
|
|
|
36
36
|
`include` (default `["**/*.{md,mdx}"]`) filters the files by their path under `path`, the way a `filesystem()` source's `include` does: a pattern starting with `!` excludes what it matches (`["**/*.mdx", "!drafts/**"]`). With `github`, the source lists the repository's whole tree at `ref` and keeps the files under `path`. If nothing is under `path` at that ref, the source reads no pages and warns with `BLUME_SOURCE_PATH_MISSING`. GitHub caps the listing (100,000 entries or 7 MB for the whole repository, whatever `path` is), and past it the source warns with `BLUME_SOURCE_TRUNCATED`: list the files with `url` and `files` instead.
|
|
37
37
|
|
|
38
|
-
A relative link between remote files (`[Errors](./02-errors.mdx)`) lands on the page the linked file publishes at, with its ordering prefix dropped from the route, and `blume validate` checks it the same way.
|
|
38
|
+
A relative link between remote files (`[Errors](./02-errors.mdx)`) lands on the page the linked file publishes at, with its ordering prefix dropped from the route. It does on the rendered page, in the page's [Markdown copy](/docs/discoverability/markdown#raw-markdown) and `llms-full.txt`, and in what the assistant reads, and `blume validate` checks it the same way.
|
|
39
39
|
|
|
40
40
|
A private repo's token is read from the `GITHUB_TOKEN` environment variable. It is never inlined into your config or generated output, and it is only ever sent to GitHub's own hosts (`api.github.com`, `raw.githubusercontent.com`), never to a custom `url` base. A public repo works without it. With `github`, the adapter declares the variable, so `blume dev` and `blume build` warn `BLUME_MISSING_SECRET` when it's unset: the file listing goes through GitHub's API, which allows 60 unauthenticated requests an hour. A raw `url` reads files only, so nothing warns up front; when GitHub refuses a file with a 401, 403, or 404 while `GITHUB_TOKEN` is unset, the warning for the skipped file says the variable isn't set.
|
|
41
41
|
|
|
@@ -3,7 +3,7 @@ title: Open Graph images
|
|
|
3
3
|
description: Generated 1200×630 social cards for every page — branding, card layers, fonts for non-Latin scripts, and per-page overrides.
|
|
4
4
|
---
|
|
5
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:
|
|
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 or when a [default image](#default-image) replaces the cards. Set `enabled` to override that either way:
|
|
7
7
|
|
|
8
8
|
```ts blume.config.ts lineNumbers
|
|
9
9
|
seo: {
|
|
@@ -11,6 +11,21 @@ seo: {
|
|
|
11
11
|
}
|
|
12
12
|
```
|
|
13
13
|
|
|
14
|
+
## Default image
|
|
15
|
+
|
|
16
|
+
Set `seo.og.image` to share one image across every page instead of a generated card, or give each locale its own:
|
|
17
|
+
|
|
18
|
+
```ts blume.config.ts lineNumbers
|
|
19
|
+
seo: {
|
|
20
|
+
og: {
|
|
21
|
+
image: { en: "/og/default-en.png", fr: "/og/default-fr.png" },
|
|
22
|
+
// Or image: "https://example.com/og/default.png"
|
|
23
|
+
},
|
|
24
|
+
}
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
A page's `seo.image`, or a custom page's `ogImage` prop, wins over the default. A locale missing from the map takes the default locale's image. The default covers custom pages rendered with `PageLayout` and the generated changelog index too, so Blume stops generating cards once it's set, unless `enabled` is `true`. Root-relative paths resolve under the deployment base. Blume declares image dimensions only for generated cards. Whichever image a page ends up with also goes into its [JSON-LD](/docs/discoverability/structured-data) as the page's `image`.
|
|
28
|
+
|
|
14
29
|
## Brand the generated card
|
|
15
30
|
|
|
16
31
|
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.
|
|
@@ -15,6 +15,18 @@ seo: {
|
|
|
15
15
|
}
|
|
16
16
|
```
|
|
17
17
|
|
|
18
|
+
### Locale alternates
|
|
19
|
+
|
|
20
|
+
Opt into alternate links on a multilingual site's sitemap:
|
|
21
|
+
|
|
22
|
+
```ts blume.config.ts lineNumbers
|
|
23
|
+
seo: {
|
|
24
|
+
sitemap: { alternates: true },
|
|
25
|
+
}
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Each translated page lists its translations, itself included, as `xhtml:link` elements, plus an `x-default` link when the default locale has the page. These are the same `hreflang` links the page head carries: a `noindex`, hidden, or canonical-elsewhere translation is in neither. A page in fewer than two listed locales gets no alternate links. URLs include the deployment base. `sitemap: true` and `sitemap: {}` keep alternates off; `sitemap: false` turns the sitemap off.
|
|
29
|
+
|
|
18
30
|
Ship your own `public/sitemap.xml` to take over — Blume never overwrites a file you place in `public/`.
|
|
19
31
|
|
|
20
32
|
## Robots
|
|
@@ -48,6 +48,50 @@ seo: {
|
|
|
48
48
|
|
|
49
49
|
`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`.
|
|
50
50
|
|
|
51
|
+
## Shared entity identifiers
|
|
52
|
+
|
|
53
|
+
When docs live inside another site (`example.com/docs`), point at the entities that site already defines instead of defining them again:
|
|
54
|
+
|
|
55
|
+
```ts blume.config.ts lineNumbers
|
|
56
|
+
seo: {
|
|
57
|
+
jsonLd: {
|
|
58
|
+
websiteId: "https://example.com/#website",
|
|
59
|
+
organizationId: "https://example.com/#organization",
|
|
60
|
+
},
|
|
61
|
+
}
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Every page names `websiteId` as its `isPartOf`, and Blume stops emitting its own WebSite node, whose name and URL would describe the docs instead of the site. Set `website: true` to emit it anyway under that id, or `website: false` to omit the docs' own WebSite node without sharing an id. `organizationId` becomes every page's publisher. With [`seo.organization`](#site-identity) also set, the Organization node uses that id and carries only the fields you configure: the docs' title and URL don't stand in for its name and URL. By default, Blume emits the WebSite node and uses the site URL plus `#website` and `#organization`. Identifiers must be absolute URLs, so they work without a site URL too.
|
|
65
|
+
|
|
66
|
+
## Page author and publisher
|
|
67
|
+
|
|
68
|
+
Reference existing entities on every page:
|
|
69
|
+
|
|
70
|
+
```ts blume.config.ts lineNumbers
|
|
71
|
+
seo: {
|
|
72
|
+
jsonLd: {
|
|
73
|
+
author: { "@id": "https://example.com/#author" },
|
|
74
|
+
publisher: { "@id": "https://example.com/#publisher" },
|
|
75
|
+
},
|
|
76
|
+
}
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
These references apply to the WebPage or article node, and `publisher` replaces the organization as its publisher. Blume doesn't define the entities they point at. A page's own [`authors`](/docs/content/frontmatter) front matter wins over `author`: each one is credited as a `Person`.
|
|
80
|
+
|
|
81
|
+
## Publication date from Git
|
|
82
|
+
|
|
83
|
+
Date pages by the commit that added their content file:
|
|
84
|
+
|
|
85
|
+
```ts blume.config.ts lineNumbers
|
|
86
|
+
seo: {
|
|
87
|
+
datePublished: "git",
|
|
88
|
+
}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Front matter `date` or `changelog.date` still wins. Git dates the rest, through the same `git log` as [`lastModified`](/docs/configuration#last-modified). Renames are followed only when the content is unchanged, so a new page never inherits the history of a similar one deleted in the same commit. Pages from non-filesystem sources and uncommitted files get no date from git. A shallow clone dates no page and warns instead, since its oldest commit is where the clone was cut. Fetch full history in CI (`VERCEL_DEEP_CLONE=true` on Vercel, `fetch-depth: 0` for `actions/checkout`).
|
|
92
|
+
|
|
93
|
+
The resolved [Open Graph image](/docs/discoverability/open-graph) is also included as the page node's `image`, whether it comes from frontmatter, a site default, or a generated card.
|
|
94
|
+
|
|
51
95
|
## Custom pages
|
|
52
96
|
|
|
53
97
|
A custom [`.astro` page](/docs/advanced/custom-pages) rendered through `PageLayout` emits the same graph, so a marketing home page isn't the one URL without structured data. A custom page has no content file to date, so pass `lastModified` to give it a `dateModified`. Pass `structuredDataEnabled={config.structuredData}` to keep it in sync with this setting, or `structuredDataEnabled={false}` to turn it off for that page alone.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "blume",
|
|
3
|
-
"version": "2.2.
|
|
3
|
+
"version": "2.2.4",
|
|
4
4
|
"description": "The open-source docs framework for humans and agents.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"agents",
|
|
@@ -36,7 +36,8 @@
|
|
|
36
36
|
"docs",
|
|
37
37
|
"skills",
|
|
38
38
|
"AGENTS.md",
|
|
39
|
-
"CHANGELOG.md"
|
|
39
|
+
"CHANGELOG.md",
|
|
40
|
+
"THIRD-PARTY-NOTICES.md"
|
|
40
41
|
],
|
|
41
42
|
"type": "module",
|
|
42
43
|
"types": "./dist/types/index.d.ts",
|
|
@@ -116,7 +117,7 @@
|
|
|
116
117
|
"@astrojs/mdx": "^8.0.2",
|
|
117
118
|
"@astrojs/node": "^11.1.6",
|
|
118
119
|
"@astrojs/react": "^7.0.0",
|
|
119
|
-
"@astrojs/vercel": "^11.0.
|
|
120
|
+
"@astrojs/vercel": "^11.0.14",
|
|
120
121
|
"@clack/prompts": "^1.8.0",
|
|
121
122
|
"@iconify-json/lucide": "^1.2.140",
|
|
122
123
|
"@iconify/types": "^2.0.0",
|
|
@@ -87,7 +87,7 @@ Read `app/app.config.ts` (`app.config.ts` at the project root on Nuxt 3), `nuxt.
|
|
|
87
87
|
| `toc.bottom.links` | `navigation.featured` (`to` → `href`). Report the move |
|
|
88
88
|
| `navigation.sub` | `navigation.tabs` (see Navigation). `'aside'` anchors have no Blume form; report |
|
|
89
89
|
| `docus.locale` (not `en`) | a one-locale `i18n` block |
|
|
90
|
-
| `docus.colorMode` | `theme.mode`.
|
|
90
|
+
| `docus.colorMode` | `theme.mode`. A forced mode also takes `theme.toggle: false` |
|
|
91
91
|
| `assistant.faqQuestions` | `ai.assistant.suggestions` (`[{ label }]`; flatten categories) |
|
|
92
92
|
| `ui.colors.primary` | `theme.accent`. Nuxt UI paints shade 500 in light mode and 400 in dark: a Blume preset of the same name, else `{ light, dark }` with those Tailwind values. **Not declared means Docus's emerald:** the site turns Blume's default color unless you offer `theme.accent: "green"`; report it either way |
|
|
93
93
|
| `--ui-radius: 0` in CSS | `theme.radius: "none"` |
|
|
@@ -275,7 +275,6 @@ Report each of these, and anything else you drop:
|
|
|
275
275
|
- Nuxt UI overrides and theme CSS that doesn't map
|
|
276
276
|
- a custom `seo.titleTemplate`, `seo.schema`, and the TOC title
|
|
277
277
|
- logo extras, and socials with no Blume platform
|
|
278
|
-
- a forced color mode (the toggle stays)
|
|
279
278
|
- `navigation.sub: 'aside'` anchors
|
|
280
279
|
- a header button that showed only on the landing page (now site-wide)
|
|
281
280
|
- **Navigation:** text-order sorting (`10.` before `2.`), and pages that sorted after a folder.
|
|
@@ -20,7 +20,7 @@ Read `themeConfig`, `presets`, and `plugins`:
|
|
|
20
20
|
| `themeConfig.navbar.items` (doc items) | `navigation.tabs` (for section links) |
|
|
21
21
|
| `themeConfig.navbar.items` (external/utility links — Blog, Discord…) | **`navigation.actions`** (`[{ label, href }]`, plain header links) to keep them in the header, or **`navigation.featured`** (`{ label, href, icon? }`, pinned above the sidebar on every route) if they should survive on phones; the GitHub link → the `github` config instead (it renders in the footer) |
|
|
22
22
|
| `themeConfig.navbar.items` (a `className`-styled button — "Get started", "Sign up") | **`navigation.cta`** (`{ label, href }`, the header's one filled button; a route the docs don't serve must be an absolute URL) |
|
|
23
|
-
| `themeConfig.colorMode.defaultMode` | `theme.mode` (`respectPrefersColorScheme: true` → `"system"`) |
|
|
23
|
+
| `themeConfig.colorMode.defaultMode` | `theme.mode` (`respectPrefersColorScheme: true` → `"system"`); `colorMode.disableSwitch: true` → `theme.toggle: false` |
|
|
24
24
|
| `themeConfig.prism.theme` / `.darkTheme` | `markdown.code.theme: { light, dark }` (map Prism theme names to Shiki themes, e.g. `github`/`github-dark`) |
|
|
25
25
|
| `themeConfig.metadata` / `themeConfig.image` | per-page `seo` frontmatter / `seo.og`; report what doesn't fit |
|
|
26
26
|
| `url` + `baseUrl` | **`url` → `deployment.site`**, unless the target host is Vercel or Netlify, which Blume auto-detects (see SKILL.md — GitHub Pages, a common Docusaurus host, is not one of them); `baseUrl` (when not `/`) → `deployment: { base: "/…" }`, or the `base` option of a host adapter (`vercel({ base })`) when the site also needs one |
|
|
@@ -48,7 +48,7 @@ Read the whole module and the helpers it imports (sidebars, nav, per-locale conf
|
|
|
48
48
|
| `rewrites` | **move each file to the path its rewrite produced**, so the URL stays put (`slug` only where moving is impossible). VitePress relative links are already written against the rewritten paths |
|
|
49
49
|
| `srcDir` / `srcExclude` | `content.root` / `content.exclude`, which adds to the defaults `**/_*` and `**/.*` |
|
|
50
50
|
| `outDir`, `assetsDir`, `cacheDir`, `ignoreDeadLinks`, `metaChunk`, `mpa` | drop |
|
|
51
|
-
| `appearance` | `'dark'
|
|
51
|
+
| `appearance` | `'dark'` → `theme.mode: "dark"`; `'force-dark'` → `theme: { mode: "dark", toggle: false }`; `false` → `theme: { mode: "light", toggle: false }`; `'force-auto'` → `theme.toggle: false`; `true` → omit |
|
|
52
52
|
| `lastUpdated: true` | `lastModified: "git"` |
|
|
53
53
|
| `sitemap.hostname` | `deployment.site`, unless the host is Vercel or Netlify (auto-detected). `sitemap.transformItems` → drop |
|
|
54
54
|
| `markdown.theme` | `markdown.code.theme: { light, dark }`; VitePress's default (`github-light`/`github-dark`) is Blume's, so omit it unless changed |
|
|
@@ -201,4 +201,4 @@ The old URL list is `<old build>/**/*.html` (or the live `sitemap.xml`). Blume s
|
|
|
201
201
|
|
|
202
202
|
## Dropped — report these
|
|
203
203
|
|
|
204
|
-
`titleTemplate`, the footer `message` (and any link inside `copyright`),
|
|
204
|
+
`titleTemplate`, the footer `message` (and any link inside `copyright`), per-page `outline`/`aside`, one-sided `prev`/`next`, page-level toggles (`editLink`, `footer`, `navbar`, `pageClass`), `head` entries beyond meta tags and analytics, build hooks, Vite and custom markdown-it plugins, unsupported social links, line-number offsets and the site-wide switch, snippet regions and include ranges (now split files), copied out-of-root snippets, data-driven and unregistered theme components (and any content they hid), `VPTeamMembers`, data loaders and dynamic routes, hero styling, the DocSearch index (with its credentials), old-site anchors that were already broken (and any left unpinned under `--only-linked`), version-dropdown links, and `::: note`/`important`/`caution` text that 1.x showed literally.
|
|
@@ -70,7 +70,7 @@ Read the whole config, including the modules it imports. VuePress sites often ke
|
|
|
70
70
|
| `nextLinks: false` **and** `prevLinks: false` (v1) | `pagination: false` on every page: there's no site-wide switch. Only one of them → drop (report) |
|
|
71
71
|
| `search: false` | `search: false` |
|
|
72
72
|
| `algolia` (`apiKey`, `indexName`, `appId`), in `themeConfig` or, in v1, any `themeConfig.locales[path]` | omit `search` (Orama) and report the credentials. A v1 `algolia` block is almost always **DocSearch**: Algolia's crawler fills the index (the app is `BH4D9OD16A` when `appId` is missing, and `algoliaOptions.facetFilters` often marks a shared index). Its records aren't Blume's, and Blume's sync, given an admin key, replaces the whole index on every build. Never point `algolia()` at it; a new index of your own is a separate opt-in |
|
|
73
|
-
| **v2** `colorMode: 'dark'` / `'light'` | `theme.mode`. `colorModeSwitch: false` →
|
|
73
|
+
| **v2** `colorMode: 'dark'` / `'light'` | `theme.mode`. `colorModeSwitch: false` → `theme.toggle: false` |
|
|
74
74
|
| **v2** `hostname` | `deployment.site`, unless the target host is Vercel or Netlify |
|
|
75
75
|
| `locales` | see "i18n" |
|
|
76
76
|
| `smoothScroll`, `searchMaxSuggestions`, `searchPlaceholder`, `repoLabel`, `editLinkText`, **v2** `contributors`, `externalLinkIcon`, `home`, `notFound`, `backToHome`, and the other UI labels | drop. A translated UI string → `i18n.ui` |
|
package/src/ai/ask-context.ts
CHANGED
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
import { normalizeRoute } from "../core/base-path.ts";
|
|
2
2
|
import { nextFenceState } from "../core/code-fences.ts";
|
|
3
3
|
import type { FenceState } from "../core/code-fences.ts";
|
|
4
|
+
import {
|
|
5
|
+
accentInsensitive,
|
|
6
|
+
foldLatinTerm,
|
|
7
|
+
stripSoftHyphens,
|
|
8
|
+
} from "../search/fold.ts";
|
|
4
9
|
import { buildOramaIndex, queryOramaIndex } from "../search/orama-index.ts";
|
|
5
10
|
import type { OramaDoc } from "../search/orama-index.ts";
|
|
11
|
+
import { linkText } from "./link-text.ts";
|
|
6
12
|
|
|
7
13
|
/** A chat message as posted by the assistant island (`{ role, content }`). */
|
|
8
14
|
export interface AskMessage {
|
|
@@ -47,7 +53,7 @@ const EXCERPT_CHARS = 2000;
|
|
|
47
53
|
const CONTEXT_BUDGET = 10_000;
|
|
48
54
|
/**
|
|
49
55
|
* Smallest excerpt worth injecting. A long page pushed under a tiny residual
|
|
50
|
-
* budget would get a full `## Title
|
|
56
|
+
* budget would get a full `## [Title](/route)` heading over a fragment of a few
|
|
51
57
|
* dozen characters — a section the model is invited to cite but that grounds
|
|
52
58
|
* nothing. Short pages that fit whole are still injected below this floor.
|
|
53
59
|
*/
|
|
@@ -196,7 +202,7 @@ const hasSegmenter = (
|
|
|
196
202
|
): segmenter is typeof Intl.Segmenter => typeof segmenter === "function";
|
|
197
203
|
|
|
198
204
|
const segmentQuery = (query: string): string[] => {
|
|
199
|
-
const lowered = query.normalize("NFC").toLowerCase();
|
|
205
|
+
const lowered = stripSoftHyphens(query).normalize("NFC").toLowerCase();
|
|
200
206
|
if (!hasSegmenter(Intl.Segmenter)) {
|
|
201
207
|
return lowered.match(TERM) ?? [];
|
|
202
208
|
}
|
|
@@ -210,9 +216,15 @@ const segmentQuery = (query: string): string[] => {
|
|
|
210
216
|
return pieces;
|
|
211
217
|
};
|
|
212
218
|
|
|
213
|
-
/**
|
|
219
|
+
/**
|
|
220
|
+
* Lowercase word tokens of `text`, cut at the same boundaries as a query.
|
|
221
|
+
* Latin words fold the way the search index folds them, so the section a page
|
|
222
|
+
* was retrieved for (Prüfung, for the question `prufung`) is the one excerpted.
|
|
223
|
+
*/
|
|
214
224
|
const tokenize = (text: string): string[] =>
|
|
215
|
-
segmentQuery(text).flatMap((piece) =>
|
|
225
|
+
segmentQuery(text).flatMap((piece) =>
|
|
226
|
+
(piece.match(TERM) ?? []).map((term) => foldLatinTerm(term))
|
|
227
|
+
);
|
|
216
228
|
|
|
217
229
|
/** Distinct, meaningful lowercase terms from a query (drops stopwords). */
|
|
218
230
|
const queryTerms = (query: string): string[] =>
|
|
@@ -222,11 +234,35 @@ const queryTerms = (query: string): string[] =>
|
|
|
222
234
|
|
|
223
235
|
/**
|
|
224
236
|
* The grounding preamble. The model is told to answer strictly from the injected
|
|
225
|
-
* excerpts and to cite the pages it used as Markdown links
|
|
226
|
-
*
|
|
237
|
+
* excerpts and to cite the pages it used as Markdown links, so citations render
|
|
238
|
+
* as real links in the panel. Each excerpt is headed by the very link to cite
|
|
239
|
+
* (`## [Title](/route)`), so a model that copies the heading copies a link.
|
|
227
240
|
*/
|
|
228
241
|
const BASE_INSTRUCTION =
|
|
229
|
-
"You are a helpful documentation assistant for this project. Answer the user's question using ONLY the documentation excerpts below. Each excerpt
|
|
242
|
+
"You are a helpful documentation assistant for this project. Answer the user's question using ONLY the documentation excerpts below. Each excerpt opens with a heading that links to its page, `## [Page Title](/route)`. If the answer is not covered by the excerpts, say you don't know and suggest where in the docs to look — do not invent details. Always cite the pages you drew from with the link in the heading that opens their excerpt, written exactly as it is there: [Page Title](/route).";
|
|
243
|
+
|
|
244
|
+
/** `encodeURIComponent` leaves parentheses as they are, so they're mapped here. */
|
|
245
|
+
const PARENTHESES = new Map([
|
|
246
|
+
["(", "%28"],
|
|
247
|
+
[")", "%29"],
|
|
248
|
+
]);
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* An excerpt's heading: the Markdown link the model cites the page with. A
|
|
252
|
+
* bracket or backslash in the title, or whitespace or a parenthesis in the
|
|
253
|
+
* route, would break the link, and a `%` in the route would read back as an
|
|
254
|
+
* escape (`read_page` decodes them), so those are escaped; the rest of the
|
|
255
|
+
* route stays as the reader sees it (`/ja/ポイント`).
|
|
256
|
+
*/
|
|
257
|
+
export const excerptHeading = (
|
|
258
|
+
doc: Pick<OramaDoc, "route" | "title">
|
|
259
|
+
): string => {
|
|
260
|
+
const target = doc.route.replaceAll(
|
|
261
|
+
/[%\s()]/gu,
|
|
262
|
+
(char) => PARENTHESES.get(char) ?? encodeURIComponent(char)
|
|
263
|
+
);
|
|
264
|
+
return `## [${linkText(doc.title)}](${target})`;
|
|
265
|
+
};
|
|
230
266
|
|
|
231
267
|
/**
|
|
232
268
|
* Appended when the endpoint gives the model the docs tools (see
|
|
@@ -272,7 +308,7 @@ const isFollowUp = (message: string): boolean => {
|
|
|
272
308
|
/**
|
|
273
309
|
* The texts that retrieve for the latest question, in rank order.
|
|
274
310
|
*
|
|
275
|
-
* The question itself always leads, verbatim:
|
|
311
|
+
* The question itself always leads, verbatim: the index's tokenizer and BM25
|
|
276
312
|
* weighting see the whole sentence (version numbers, single-character CJK
|
|
277
313
|
* words, `--flags`, and the bigrams a ja/zh index depends on all survive), and
|
|
278
314
|
* a short question that names its subject ("Does it support i18n?") is never
|
|
@@ -350,13 +386,15 @@ export const relevantExcerpt = (
|
|
|
350
386
|
return `${prefix}${slice}${suffix}`;
|
|
351
387
|
};
|
|
352
388
|
|
|
353
|
-
// Case-insensitive matching via regex rather than `indexOf` on
|
|
354
|
-
// copy: length-changing
|
|
355
|
-
// every index in the copy, sliding the excerpt window off
|
|
356
|
-
//
|
|
389
|
+
// Case- and accent-insensitive matching via regex rather than `indexOf` on
|
|
390
|
+
// a folded copy: length-changing mappings (Turkish İ → "i" + U+0307, ß →
|
|
391
|
+
// "ss") would shift every index in the copy, sliding the excerpt window off
|
|
392
|
+
// the match.
|
|
357
393
|
const positions: number[] = [];
|
|
358
394
|
for (const term of queryTerms(query)) {
|
|
359
|
-
for (const match of trimmed.matchAll(
|
|
395
|
+
for (const match of trimmed.matchAll(
|
|
396
|
+
new RegExp(accentInsensitive(term), "giu")
|
|
397
|
+
)) {
|
|
360
398
|
positions.push(match.index);
|
|
361
399
|
}
|
|
362
400
|
}
|
|
@@ -770,7 +808,7 @@ export const createAskContext = (
|
|
|
770
808
|
Math.min(excerptChars, budget)
|
|
771
809
|
);
|
|
772
810
|
budget -= body.length;
|
|
773
|
-
sections.push(
|
|
811
|
+
sections.push(`${excerptHeading(doc)}${label}\n${body}`);
|
|
774
812
|
};
|
|
775
813
|
|
|
776
814
|
if (current) {
|
package/src/ai/ask-data.ts
CHANGED
|
@@ -14,7 +14,10 @@ import type { AskData } from "./ask-context.ts";
|
|
|
14
14
|
* from the docs instead of declining.
|
|
15
15
|
* The reader is an AI agent, so `<Visibility>` resolves for the agents audience
|
|
16
16
|
* (web-only content removed, agents-only unwrapped) and components downlevel to
|
|
17
|
-
* Markdown, both matching llms-full.txt.
|
|
17
|
+
* Markdown, both matching llms-full.txt. Its page links point at the pages
|
|
18
|
+
* they mean, as there, but as the routes this snapshot keys those pages by:
|
|
19
|
+
* without the `deployment.base`, like the excerpt headings, since the chat
|
|
20
|
+
* panel mounts every link it renders under the base.
|
|
18
21
|
*/
|
|
19
22
|
export const buildAskData = async (project: BlumeProject): Promise<AskData> => {
|
|
20
23
|
const documents = await buildSearchDocuments(project, {
|
package/src/ai/ask-tools.ts
CHANGED
|
@@ -3,7 +3,12 @@ import { jsonSchema, tool } from "ai";
|
|
|
3
3
|
import { normalizeRoute } from "../core/base-path.ts";
|
|
4
4
|
import { queryOramaIndex } from "../search/orama-index.ts";
|
|
5
5
|
import type { OramaDoc } from "../search/orama-index.ts";
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
askIndex,
|
|
8
|
+
excerptHeading,
|
|
9
|
+
readerScope,
|
|
10
|
+
sectionExcerpt,
|
|
11
|
+
} from "./ask-context.ts";
|
|
7
12
|
import type { AskData, AskPage, ReaderScope } from "./ask-context.ts";
|
|
8
13
|
|
|
9
14
|
/**
|
|
@@ -37,7 +42,7 @@ const PAGE_CHARS = 20_000;
|
|
|
37
42
|
|
|
38
43
|
/** A result block, headed the way the injected excerpts are, for citing. */
|
|
39
44
|
const block = (doc: OramaDoc, body: string): string =>
|
|
40
|
-
|
|
45
|
+
`${excerptHeading(doc)}\n${body}`;
|
|
41
46
|
|
|
42
47
|
/**
|
|
43
48
|
* Search the docs for `query` within the reader's scope: the most relevant
|
|
@@ -66,7 +71,9 @@ export const searchDocs = async (
|
|
|
66
71
|
|
|
67
72
|
/**
|
|
68
73
|
* The route a model asked for, as the snapshot keys it: a full URL on the
|
|
69
|
-
* site becomes its path,
|
|
74
|
+
* site becomes its path, a query string or fragment is dropped, and
|
|
75
|
+
* percent-escapes are decoded (a cited route escapes whitespace,
|
|
76
|
+
* parentheses, and `%`; see `excerptHeading`).
|
|
70
77
|
*/
|
|
71
78
|
const routeOf = (data: AskData, requested: string): string => {
|
|
72
79
|
let path = requested.trim();
|
|
@@ -74,6 +81,11 @@ const routeOf = (data: AskData, requested: string): string => {
|
|
|
74
81
|
path = path.slice(data.site.replace(/\/$/u, "").length) || "/";
|
|
75
82
|
}
|
|
76
83
|
path = path.replace(/[?#].*$/u, "");
|
|
84
|
+
try {
|
|
85
|
+
path = decodeURI(path);
|
|
86
|
+
} catch {
|
|
87
|
+
// Malformed percent sequence — look it up as written.
|
|
88
|
+
}
|
|
77
89
|
return normalizeRoute(path.startsWith("/") ? path : `/${path}`);
|
|
78
90
|
};
|
|
79
91
|
|
|
@@ -106,7 +118,7 @@ export const createAskTools = (data: AskData) => {
|
|
|
106
118
|
return {
|
|
107
119
|
read_page: tool({
|
|
108
120
|
description:
|
|
109
|
-
"Read one documentation page in full, by its route (the
|
|
121
|
+
"Read one documentation page in full, by its route (the link target in the heading that opens a page's excerpt, such as /docs/getting-started).",
|
|
110
122
|
execute: ({ route }) => Promise.resolve(readPage(data, byRoute, route)),
|
|
111
123
|
inputSchema: jsonSchema<{ route: string }>({
|
|
112
124
|
additionalProperties: false,
|
|
@@ -122,7 +134,7 @@ export const createAskTools = (data: AskData) => {
|
|
|
122
134
|
}),
|
|
123
135
|
search_docs: tool({
|
|
124
136
|
description:
|
|
125
|
-
"Search this project's documentation. Returns the most relevant excerpt of each matching page, headed by
|
|
137
|
+
"Search this project's documentation. Returns the most relevant excerpt of each matching page, headed by a Markdown link to the page.",
|
|
126
138
|
execute: ({ query }) => searchDocs(data, scope, query),
|
|
127
139
|
inputSchema: jsonSchema<{ query: string }>({
|
|
128
140
|
additionalProperties: false,
|
package/src/ai/llms.ts
CHANGED
|
@@ -13,6 +13,7 @@ import { AI_CATALOG_PATH, hasAiCatalog } from "./ai-catalog.ts";
|
|
|
13
13
|
import { API_CATALOG_PATH, hasApiCatalog } from "./api-catalog.ts";
|
|
14
14
|
import { API_PAGES_PATH, OPENAPI_PATH } from "./api/paths.ts";
|
|
15
15
|
import { downlevelComponents } from "./component-markdown.ts";
|
|
16
|
+
import { linkText } from "./link-text.ts";
|
|
16
17
|
import { relativeLinkRewriter } from "./relative-links.ts";
|
|
17
18
|
import { projectComponentSerializers } from "./serializers.ts";
|
|
18
19
|
import {
|
|
@@ -33,13 +34,6 @@ const pageUrl = (route: string, site?: string, base = ""): string => {
|
|
|
33
34
|
return encodeURI(site ? absoluteUrl(site, path) : path);
|
|
34
35
|
};
|
|
35
36
|
|
|
36
|
-
/**
|
|
37
|
-
* A title as Markdown link text. An unescaped bracket in it — `[Beta] Webhooks`
|
|
38
|
-
* — closes or nests the text early, and the line stops being a link.
|
|
39
|
-
*/
|
|
40
|
-
const linkText = (title: string): string =>
|
|
41
|
-
title.replaceAll(/[\\[\]]/gu, String.raw`\$&`);
|
|
42
|
-
|
|
43
37
|
/** Inputs only the build has: the published skills, collected once per build. */
|
|
44
38
|
export interface LlmsIndexOptions {
|
|
45
39
|
/**
|
package/src/ai/markdown.ts
CHANGED
|
@@ -7,7 +7,7 @@ import { discoverPages, discoverPagesSync } from "../core/custom-pages.ts";
|
|
|
7
7
|
import matter from "../core/frontmatter.ts";
|
|
8
8
|
import type { BlumeProject } from "../core/project-graph.ts";
|
|
9
9
|
import { readExpandedEntryText } from "../core/sources/read.ts";
|
|
10
|
-
import type { RouteManifestEntry } from "../core/types.ts";
|
|
10
|
+
import type { PageRecord, RouteManifestEntry } from "../core/types.ts";
|
|
11
11
|
import { advertisedConfig, servesMcp } from "./agent-surface.ts";
|
|
12
12
|
import { buildChangelogIndexMarkdown } from "./changelog-markdown.ts";
|
|
13
13
|
import { downlevelComponents } from "./component-markdown.ts";
|
|
@@ -64,8 +64,10 @@ export const buildRawMarkdown = async (
|
|
|
64
64
|
const components = projectComponentSerializers(project);
|
|
65
65
|
const rewriteLinks = relativeLinkRewriter(project);
|
|
66
66
|
|
|
67
|
-
const readRoute = async (
|
|
68
|
-
|
|
67
|
+
const readRoute = async (
|
|
68
|
+
route: RouteManifestEntry,
|
|
69
|
+
page: PageRecord | undefined
|
|
70
|
+
): Promise<string> => {
|
|
69
71
|
if (page) {
|
|
70
72
|
return await readExpandedEntryText(project, page);
|
|
71
73
|
}
|
|
@@ -74,7 +76,8 @@ export const buildRawMarkdown = async (
|
|
|
74
76
|
|
|
75
77
|
const entries = await Promise.all(
|
|
76
78
|
project.manifest.routes.map(async (route) => {
|
|
77
|
-
|
|
79
|
+
const page = pageById.get(route.id);
|
|
80
|
+
let text = await readRoute(route, page);
|
|
78
81
|
if (route.sourcePath) {
|
|
79
82
|
text = rewriteRelativeAssets({
|
|
80
83
|
deployBase: project.config.deployment.options.base,
|
|
@@ -87,6 +90,7 @@ export const buildRawMarkdown = async (
|
|
|
87
90
|
// agent fetched resolves it to, and `/install` gains the base and the
|
|
88
91
|
// page's locale the rendered link has — remote content included.
|
|
89
92
|
text = rewriteLinks(text, {
|
|
93
|
+
entryId: page?.entryId,
|
|
90
94
|
route: route.path,
|
|
91
95
|
sourcePath: route.sourcePath,
|
|
92
96
|
});
|