blume 2.1.1 → 2.1.2
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 +16 -0
- package/dist/cli/{chunk-ncj7w596.js → chunk-0pmb4tr4.js} +22 -18
- package/dist/cli/{chunk-ncj7w596.js.map → chunk-0pmb4tr4.js.map} +3 -3
- package/dist/cli/{chunk-nq1d20w2.js → chunk-0w6hs479.js} +15 -15
- package/dist/cli/{chunk-ehweg4fr.js → chunk-124fvmzq.js} +1031 -903
- package/dist/cli/{chunk-ehweg4fr.js.map → chunk-124fvmzq.js.map} +20 -18
- package/dist/cli/{chunk-kma9f6sf.js → chunk-1f882900.js} +2 -2
- package/dist/cli/{chunk-e8xw9qkh.js → chunk-297ajdcv.js} +4 -4
- package/dist/cli/{chunk-e6atmn4h.js → chunk-32khwcb6.js} +7 -7
- package/dist/cli/{chunk-073rdvgb.js → chunk-36bxs9c7.js} +16 -16
- package/dist/cli/{chunk-mrcccgnh.js → chunk-53jqw8z0.js} +3 -3
- package/dist/cli/{chunk-ft9vfys6.js → chunk-58bt3hag.js} +4 -4
- package/dist/cli/{chunk-sds923fa.js → chunk-5cmts6dv.js} +3 -3
- package/dist/cli/{chunk-cfyg47zg.js → chunk-60hz265r.js} +4 -4
- package/dist/cli/{chunk-j134vrp2.js → chunk-74wfhv79.js} +1 -1
- package/dist/cli/{chunk-nrns10mj.js → chunk-9qzgg6mj.js} +3 -3
- package/dist/cli/{chunk-tphac74f.js → chunk-a57yh0h9.js} +5 -5
- package/dist/cli/{chunk-hebn0tpx.js → chunk-ave1wcbr.js} +1 -1
- package/dist/cli/{chunk-htrwgabr.js → chunk-bqxaqn5p.js} +6 -6
- package/dist/cli/{chunk-5vjtgs6e.js → chunk-bxwhtvw8.js} +5 -5
- package/dist/cli/{chunk-66w9j7p6.js → chunk-ckrcm08h.js} +5 -3
- package/dist/cli/{chunk-66w9j7p6.js.map → chunk-ckrcm08h.js.map} +3 -3
- package/dist/cli/{chunk-n0v7m8ry.js → chunk-desvm72z.js} +3 -3
- package/dist/cli/{chunk-3v3ffbrj.js → chunk-dhwtqnnd.js} +2 -2
- package/dist/cli/{chunk-gb420vvz.js → chunk-dpy8qdjf.js} +7 -4
- package/dist/cli/chunk-dpy8qdjf.js.map +17 -0
- package/dist/cli/{chunk-c1y538kd.js → chunk-dqhg54mr.js} +1 -1
- package/dist/cli/{chunk-42t4jxdx.js → chunk-dv2z0rzf.js} +1 -1
- package/dist/cli/{chunk-cm89scdv.js → chunk-es8h0v3j.js} +9 -11
- package/dist/cli/{chunk-cm89scdv.js.map → chunk-es8h0v3j.js.map} +3 -3
- package/dist/cli/{chunk-hx3xqnk9.js → chunk-gfc5fqvf.js} +14 -14
- package/dist/cli/{chunk-7ppf1rva.js → chunk-gxtg6ed2.js} +16 -16
- package/dist/cli/{chunk-8y2kmjv8.js → chunk-hvcxng5f.js} +6 -6
- package/dist/cli/{chunk-54ztw1nj.js → chunk-j1x92mx9.js} +1 -1
- package/dist/cli/{chunk-313vnn40.js → chunk-k3gr1nm9.js} +11 -9
- package/dist/cli/chunk-k3gr1nm9.js.map +10 -0
- package/dist/cli/{chunk-c887fj0s.js → chunk-kdpzw38a.js} +2 -2
- package/dist/cli/{chunk-t2m5j4pr.js → chunk-ms9z66nz.js} +2 -2
- package/dist/cli/{chunk-t2m5j4pr.js.map → chunk-ms9z66nz.js.map} +2 -2
- package/dist/cli/{chunk-ep6nd0rn.js → chunk-nbyvgnnm.js} +26 -14
- package/dist/cli/{chunk-ep6nd0rn.js.map → chunk-nbyvgnnm.js.map} +7 -7
- package/dist/cli/{chunk-w1r4vase.js → chunk-s0bdnc7v.js} +2 -2
- package/dist/cli/{chunk-fdrkhpjy.js → chunk-sc93apfs.js} +14 -14
- package/dist/cli/{chunk-dzwehpy7.js → chunk-tfycddbt.js} +2 -2
- package/dist/cli/{chunk-3x6x3pph.js → chunk-twy7psdz.js} +7 -7
- package/dist/cli/{chunk-e9q7a2ak.js → chunk-y4msrwm9.js} +15 -15
- package/dist/cli/{chunk-tdqhyxfz.js → chunk-ydp3065p.js} +5 -5
- package/dist/cli/index.js +18 -18
- package/dist/types/ai/ask.d.ts +24 -5
- package/dist/types/analytics/clics.d.ts +37 -0
- package/dist/types/analytics/index.d.ts +2 -0
- package/dist/types/analytics/schema.d.ts +18 -0
- package/dist/types/astro/integration.d.ts +6 -0
- package/dist/types/core/config-input.d.ts +7 -3
- package/dist/types/core/powered-by.d.ts +12 -0
- package/dist/types/core/schema.d.ts +109 -3
- package/dist/types/core/sources/lower.d.ts +10 -2
- package/dist/types/core/sources/notion.d.ts +8 -0
- package/dist/types/deploy/cloudflare-negotiation.d.ts +5 -2
- package/dist/types/deploy/node-headers.d.ts +12 -4
- package/dist/types/deploy/platforms/vercel.d.ts +2 -2
- package/dist/types/deploy/vercel-negotiation.d.ts +4 -2
- package/dist/types/narration/build.d.ts +34 -12
- package/dist/types/narration/provider.d.ts +96 -8
- package/dist/types/openapi/sentence.d.ts +11 -1
- package/dist/types/seo/jsonld.d.ts +11 -6
- package/dist/types/sources/notion.d.ts +8 -0
- package/dist/types/sources/registry.d.ts +9 -0
- package/docs/02-deployment.mdx +29 -1
- package/docs/03-upgrading.mdx +2 -1
- package/docs/04-migrating.mdx +9 -9
- package/docs/08-faq.mdx +12 -12
- package/docs/advanced/changelog.mdx +19 -2
- package/docs/advanced/custom-pages.mdx +1 -1
- package/docs/cli/doctor.mdx +1 -1
- package/docs/configuration/analytics.mdx +19 -1
- package/docs/configuration/index.mdx +1 -0
- package/docs/configuration/narration.mdx +43 -6
- package/docs/content/navigation.mdx +4 -2
- package/docs/content/sources/contentful.mdx +2 -0
- package/docs/content/sources/github-releases.mdx +2 -0
- package/docs/content/sources/notion.mdx +16 -1
- package/docs/content/sources/obsidian.mdx +2 -0
- package/docs/content/sources/payload.mdx +2 -0
- package/docs/content/sources/remote-mdx.mdx +2 -0
- package/docs/content/sources/sanity.mdx +2 -0
- package/docs/content/sources/strapi.mdx +2 -0
- package/docs/discoverability/mcp.mdx +2 -0
- package/docs/discoverability/structured-data.mdx +8 -6
- package/docs/references/asyncapi.mdx +2 -0
- package/docs/references/graphql.mdx +2 -0
- package/docs/references/openapi.mdx +14 -0
- package/package.json +1 -1
- package/skills/blume/SKILL.md +1 -1
- package/skills/blume-migrate/SKILL.md +1 -1
- package/skills/blume-migrate/assets/{oxfmt@0.67.0.patch → oxfmt@0.71.0.patch} +7 -7
- package/skills/blume-migrate/references/monorepo.md +5 -5
- package/src/ai/ask.ts +24 -6
- package/src/analytics/clics.ts +58 -0
- package/src/analytics/head.ts +5 -0
- package/src/analytics/index.ts +2 -0
- package/src/analytics/schema.ts +2 -0
- package/src/astro/generate.ts +13 -8
- package/src/astro/integration.ts +56 -9
- package/src/astro/render-deps.ts +10 -5
- package/src/astro/runtime-deps.ts +23 -2
- package/src/astro/templates.ts +4 -1
- package/src/cli/init/scaffold.ts +6 -3
- package/src/cli/required-secrets.ts +2 -1
- package/src/components/content/view-switcher.ts +7 -4
- package/src/components/layout/NavTree.astro +49 -5
- package/src/components/layout/NavTreeScript.astro +18 -0
- package/src/components/layout/PageLayout.astro +13 -3
- package/src/components/layout/RootLayout.astro +17 -12
- package/src/components/layout/analytics-client.ts +8 -8
- package/src/components/openapi/playground-client.ts +53 -53
- package/src/components/powered-by-middleware.ts +33 -0
- package/src/core/config-input.ts +11 -3
- package/src/core/powered-by.ts +12 -0
- package/src/core/schema.ts +5 -4
- package/src/core/sources/lower.ts +19 -3
- package/src/core/sources/notion.ts +27 -9
- package/src/deploy/cloudflare-negotiation.ts +36 -7
- package/src/deploy/headers.ts +15 -1
- package/src/deploy/node-headers.ts +32 -20
- package/src/deploy/platforms/cloudflare.ts +1 -0
- package/src/deploy/platforms/vercel.ts +4 -3
- package/src/deploy/vercel-negotiation.ts +38 -4
- package/src/markdown/base-links.ts +15 -14
- package/src/markdown/package-commands.ts +9 -8
- package/src/narration/build.ts +218 -40
- package/src/narration/provider.ts +89 -9
- package/src/openapi/render-mdx.ts +68 -8
- package/src/openapi/sentence.ts +29 -2
- package/src/registry/eject.ts +5 -1
- package/src/seo/jsonld.ts +38 -20
- package/src/sources/notion.ts +7 -0
- package/src/theme/color.ts +9 -10
- package/src/theme/entry.ts +14 -3
- package/dist/cli/chunk-313vnn40.js.map +0 -10
- package/dist/cli/chunk-gb420vvz.js.map +0 -17
- /package/dist/cli/{chunk-nq1d20w2.js.map → chunk-0w6hs479.js.map} +0 -0
- /package/dist/cli/{chunk-kma9f6sf.js.map → chunk-1f882900.js.map} +0 -0
- /package/dist/cli/{chunk-e8xw9qkh.js.map → chunk-297ajdcv.js.map} +0 -0
- /package/dist/cli/{chunk-e6atmn4h.js.map → chunk-32khwcb6.js.map} +0 -0
- /package/dist/cli/{chunk-073rdvgb.js.map → chunk-36bxs9c7.js.map} +0 -0
- /package/dist/cli/{chunk-mrcccgnh.js.map → chunk-53jqw8z0.js.map} +0 -0
- /package/dist/cli/{chunk-ft9vfys6.js.map → chunk-58bt3hag.js.map} +0 -0
- /package/dist/cli/{chunk-sds923fa.js.map → chunk-5cmts6dv.js.map} +0 -0
- /package/dist/cli/{chunk-cfyg47zg.js.map → chunk-60hz265r.js.map} +0 -0
- /package/dist/cli/{chunk-j134vrp2.js.map → chunk-74wfhv79.js.map} +0 -0
- /package/dist/cli/{chunk-nrns10mj.js.map → chunk-9qzgg6mj.js.map} +0 -0
- /package/dist/cli/{chunk-tphac74f.js.map → chunk-a57yh0h9.js.map} +0 -0
- /package/dist/cli/{chunk-hebn0tpx.js.map → chunk-ave1wcbr.js.map} +0 -0
- /package/dist/cli/{chunk-htrwgabr.js.map → chunk-bqxaqn5p.js.map} +0 -0
- /package/dist/cli/{chunk-5vjtgs6e.js.map → chunk-bxwhtvw8.js.map} +0 -0
- /package/dist/cli/{chunk-n0v7m8ry.js.map → chunk-desvm72z.js.map} +0 -0
- /package/dist/cli/{chunk-3v3ffbrj.js.map → chunk-dhwtqnnd.js.map} +0 -0
- /package/dist/cli/{chunk-c1y538kd.js.map → chunk-dqhg54mr.js.map} +0 -0
- /package/dist/cli/{chunk-42t4jxdx.js.map → chunk-dv2z0rzf.js.map} +0 -0
- /package/dist/cli/{chunk-hx3xqnk9.js.map → chunk-gfc5fqvf.js.map} +0 -0
- /package/dist/cli/{chunk-7ppf1rva.js.map → chunk-gxtg6ed2.js.map} +0 -0
- /package/dist/cli/{chunk-8y2kmjv8.js.map → chunk-hvcxng5f.js.map} +0 -0
- /package/dist/cli/{chunk-54ztw1nj.js.map → chunk-j1x92mx9.js.map} +0 -0
- /package/dist/cli/{chunk-c887fj0s.js.map → chunk-kdpzw38a.js.map} +0 -0
- /package/dist/cli/{chunk-w1r4vase.js.map → chunk-s0bdnc7v.js.map} +0 -0
- /package/dist/cli/{chunk-fdrkhpjy.js.map → chunk-sc93apfs.js.map} +0 -0
- /package/dist/cli/{chunk-dzwehpy7.js.map → chunk-tfycddbt.js.map} +0 -0
- /package/dist/cli/{chunk-3x6x3pph.js.map → chunk-twy7psdz.js.map} +0 -0
- /package/dist/cli/{chunk-e9q7a2ak.js.map → chunk-y4msrwm9.js.map} +0 -0
- /package/dist/cli/{chunk-tdqhyxfz.js.map → chunk-ydp3065p.js.map} +0 -0
|
@@ -81,6 +81,7 @@ declare const sourceAdapterUnion: z.ZodDiscriminatedUnion<readonly [z.ZodObject<
|
|
|
81
81
|
prefix: z.ZodOptional<z.ZodString>;
|
|
82
82
|
concurrency: z.ZodOptional<z.ZodNumber>;
|
|
83
83
|
database: z.ZodString;
|
|
84
|
+
mdx: z.ZodOptional<z.ZodBoolean>;
|
|
84
85
|
properties: z.ZodOptional<z.ZodObject<{
|
|
85
86
|
description: z.ZodOptional<z.ZodString>;
|
|
86
87
|
order: z.ZodOptional<z.ZodString>;
|
|
@@ -255,6 +256,7 @@ export declare const resolvedSourceAdapterSchema: z.ZodPipe<z.ZodDiscriminatedUn
|
|
|
255
256
|
prefix: z.ZodOptional<z.ZodString>;
|
|
256
257
|
concurrency: z.ZodOptional<z.ZodNumber>;
|
|
257
258
|
database: z.ZodString;
|
|
259
|
+
mdx: z.ZodOptional<z.ZodBoolean>;
|
|
258
260
|
properties: z.ZodOptional<z.ZodObject<{
|
|
259
261
|
description: z.ZodOptional<z.ZodString>;
|
|
260
262
|
order: z.ZodOptional<z.ZodString>;
|
|
@@ -414,6 +416,7 @@ export declare const resolvedSourceAdapterSchema: z.ZodPipe<z.ZodDiscriminatedUn
|
|
|
414
416
|
pollInterval?: number | undefined;
|
|
415
417
|
prefix?: string | undefined;
|
|
416
418
|
concurrency?: number | undefined;
|
|
419
|
+
mdx?: boolean | undefined;
|
|
417
420
|
properties?: {
|
|
418
421
|
description?: string | undefined;
|
|
419
422
|
order?: string | undefined;
|
|
@@ -573,6 +576,7 @@ export declare const resolvedSourceAdapterSchema: z.ZodPipe<z.ZodDiscriminatedUn
|
|
|
573
576
|
pollInterval?: number | undefined;
|
|
574
577
|
prefix?: string | undefined;
|
|
575
578
|
concurrency?: number | undefined;
|
|
579
|
+
mdx?: boolean | undefined;
|
|
576
580
|
properties?: {
|
|
577
581
|
description?: string | undefined;
|
|
578
582
|
order?: string | undefined;
|
|
@@ -737,6 +741,7 @@ export declare const contentSourcesSchema: z.ZodArray<z.ZodPipe<z.ZodCustom<{
|
|
|
737
741
|
pollInterval?: number | undefined;
|
|
738
742
|
prefix?: string | undefined;
|
|
739
743
|
concurrency?: number | undefined;
|
|
744
|
+
mdx?: boolean | undefined;
|
|
740
745
|
properties?: {
|
|
741
746
|
description?: string | undefined;
|
|
742
747
|
order?: string | undefined;
|
|
@@ -896,6 +901,7 @@ export declare const contentSourcesSchema: z.ZodArray<z.ZodPipe<z.ZodCustom<{
|
|
|
896
901
|
pollInterval?: number | undefined;
|
|
897
902
|
prefix?: string | undefined;
|
|
898
903
|
concurrency?: number | undefined;
|
|
904
|
+
mdx?: boolean | undefined;
|
|
899
905
|
properties?: {
|
|
900
906
|
description?: string | undefined;
|
|
901
907
|
order?: string | undefined;
|
|
@@ -1055,6 +1061,7 @@ export declare const contentSourcesSchema: z.ZodArray<z.ZodPipe<z.ZodCustom<{
|
|
|
1055
1061
|
prefix: z.ZodOptional<z.ZodString>;
|
|
1056
1062
|
concurrency: z.ZodOptional<z.ZodNumber>;
|
|
1057
1063
|
database: z.ZodString;
|
|
1064
|
+
mdx: z.ZodOptional<z.ZodBoolean>;
|
|
1058
1065
|
properties: z.ZodOptional<z.ZodObject<{
|
|
1059
1066
|
description: z.ZodOptional<z.ZodString>;
|
|
1060
1067
|
order: z.ZodOptional<z.ZodString>;
|
|
@@ -1214,6 +1221,7 @@ export declare const contentSourcesSchema: z.ZodArray<z.ZodPipe<z.ZodCustom<{
|
|
|
1214
1221
|
pollInterval?: number | undefined;
|
|
1215
1222
|
prefix?: string | undefined;
|
|
1216
1223
|
concurrency?: number | undefined;
|
|
1224
|
+
mdx?: boolean | undefined;
|
|
1217
1225
|
properties?: {
|
|
1218
1226
|
description?: string | undefined;
|
|
1219
1227
|
order?: string | undefined;
|
|
@@ -1373,6 +1381,7 @@ export declare const contentSourcesSchema: z.ZodArray<z.ZodPipe<z.ZodCustom<{
|
|
|
1373
1381
|
pollInterval?: number | undefined;
|
|
1374
1382
|
prefix?: string | undefined;
|
|
1375
1383
|
concurrency?: number | undefined;
|
|
1384
|
+
mdx?: boolean | undefined;
|
|
1376
1385
|
properties?: {
|
|
1377
1386
|
description?: string | undefined;
|
|
1378
1387
|
order?: string | undefined;
|
package/docs/02-deployment.mdx
CHANGED
|
@@ -82,6 +82,22 @@ deployment: {
|
|
|
82
82
|
|
|
83
83
|
Every page keeps its own path under the base, even in a content folder that shares the base's name: with `base: "/docs"`, `docs/setup.md` is served at `/docs/docs/setup`, and its canonical URL, sidebar link, and `llms.txt` entry point there. A root-relative link you write that already starts with the base is taken to include it and left as written, so link to that page with a relative link (`./docs/setup`) or its full path (`/docs/docs/setup`).
|
|
84
84
|
|
|
85
|
+
### A different base per deploy
|
|
86
|
+
|
|
87
|
+
`blume.config.ts` is a module, so `base` can come from an environment variable. That lets CI build each pull request's preview under its own path without editing the config:
|
|
88
|
+
|
|
89
|
+
```ts blume.config.ts lineNumbers
|
|
90
|
+
deployment: {
|
|
91
|
+
base: process.env.DOCS_BASE,
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
DOCS_BASE=/pr-1234 blume build
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
With `DOCS_BASE` unset, the site builds at the root, so production needs nothing extra.
|
|
100
|
+
|
|
85
101
|
## Mount the docs under a path
|
|
86
102
|
|
|
87
103
|
`basePath` mounts every generated route under a segment (`/docs/getting-started`) while leaving the sidebar untouched — the top level is your sections, not a wrapper group. Use it when the docs live at `/docs/*` but the site root stays yours (like Docusaurus `routeBasePath` or Fumadocs `baseUrl`).
|
|
@@ -214,6 +230,18 @@ A pattern can't also match a page: hosts disagree on which of the two answers, s
|
|
|
214
230
|
|
|
215
231
|
Where the host reads one, a build also emits a `_headers` file that pins `charset=utf-8` onto the raw AI-ready endpoints — `/<route>.md`, `/<route>.mdx`, and the `.txt` files (`llms.txt`, `llms-full.txt`). Those responses are valid UTF-8, but many static hosts serve them as `text/markdown` / `text/plain` with **no** charset, and browsers then fall back to Windows-1252 — so non-ASCII docs (Japanese, accented Latin, …) render as mojibake when the raw URL is opened directly. HTML pages are unaffected because they carry `<meta charset>`. Netlify reads `_headers` on a static deploy and Cloudflare (Pages, and Workers static assets) on both static and server builds, so those adapters and an unnamed static host get the file; Vercel (whose headers ride the routing config) and Node (whose server ignores the file) don't. A Netlify server build writes the same rules into the `headers` of its Frameworks API config (`.netlify/v1/config.json`) instead. A static Vercel build writes the same rules into `dist/vercel.json` instead, which a Git-connected project doesn't read (see [Redirects](#redirects)). A `_headers` you ship in `public/` is left untouched.
|
|
216
232
|
|
|
233
|
+
## Powered-by header
|
|
234
|
+
|
|
235
|
+
Blume names itself in an `X-Powered-By: Blume` response header, so HTTP inspectors and technology detectors can tell what built the site. Set `poweredBy` to `false` to leave it out:
|
|
236
|
+
|
|
237
|
+
```ts blume.config.ts
|
|
238
|
+
poweredBy: false,
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
`blume dev` sends it on every response. A server build sends it wherever the host takes its headers from: the `node()` server and the `cloudflare()` Worker set it themselves, a `vercel()` build carries it in its routing config, and a `netlify()` build in the header rules of its Frameworks API config, with Blume's middleware adding it to the pages rendered on demand. A static build lists it in `_headers` and `dist/vercel.json` beside the [content types](#content-types), so it reaches the hosts those files reach: not a Git-connected Vercel project, and not a host that sends no custom headers, such as GitHub Pages.
|
|
242
|
+
|
|
243
|
+
In an [ejected](/docs/configuration/customization#eject) app, the switch is the `poweredBy` option of `blumeIntegration()` in `astro.config.mjs`. Its build sends the header on server-rendered pages and from `_headers`; the server-entry wrapper and routing configs that carry it elsewhere are `blume build` steps.
|
|
244
|
+
|
|
217
245
|
## Environment variables
|
|
218
246
|
|
|
219
247
|
When a feature needs a secret, Blume warns at `blume dev`/`build` if it's missing — so the problem surfaces early instead of at the first request:
|
|
@@ -225,7 +253,7 @@ When a feature needs a secret, Blume warns at `blume dev`/`build` if it's missin
|
|
|
225
253
|
| [Assistant bot protection](/docs/configuration/assistant#bot-protection) | `TURNSTILE_SECRET_KEY` (`turnstile()`), `HCAPTCHA_SECRET_KEY` (`hcaptcha()`) |
|
|
226
254
|
| [Rate limiting](/docs/configuration/rate-limiting) (`upstash()`) | `UPSTASH_REDIS_REST_URL` and `UPSTASH_REDIS_REST_TOKEN` |
|
|
227
255
|
| Mixedbread search | `MIXEDBREAD_API_KEY` |
|
|
228
|
-
| [Narration](/docs/configuration/narration) (generated voices) | `AI_GATEWAY_API_KEY` (or Vercel OIDC), or the `apiKeyEnv` you passed
|
|
256
|
+
| [Narration](/docs/configuration/narration) (generated voices) | `AI_GATEWAY_API_KEY` (or Vercel OIDC) for `gateway()`, `OPENAI_API_KEY` for `openai()` (none with a `baseUrl`, unless you name one), or the `apiKeyEnv` you passed to either |
|
|
229
257
|
| [Content sources](/docs/content/sources) | `NOTION_TOKEN` (`notion()`), `SANITY_TOKEN` (`sanity()`), `CONTENTFUL_ACCESS_TOKEN` (`contentful()`, plus `CONTENTFUL_PREVIEW_TOKEN` for `--preview`, which fails without it), `PAYLOAD_API_KEY` (`payload()`), `STRAPI_API_TOKEN` (`strapi()`), `GITHUB_TOKEN` (`githubReleases()`, and `mdxRemote()` reading from GitHub) |
|
|
230
258
|
|
|
231
259
|
Set them in `.env.local` for local dev and in your host's environment for production. A content source reads its token when it fetches, during `blume dev` and `blume build`, and narration reads its key during `blume build`, so set those where your site builds. Build-time secrets for search-index sync (`ALGOLIA_ADMIN_API_KEY`, `ORAMA_PRIVATE_API_KEY`, `TYPESENSE_ADMIN_API_KEY`) are checked by the sync step itself: when one is unset, the build warns and skips the sync, and when it's set, a failed sync [fails the build](/docs/configuration/search#adapters). The [GitHub info](/docs/content/components#github-info) card also sends `GITHUB_TOKEN`, when it's set, to lift GitHub's API rate limit.
|
package/docs/03-upgrading.mdx
CHANGED
|
@@ -88,7 +88,7 @@ export default defineConfig({
|
|
|
88
88
|
- `netlify()`, `cloudflare()`, and `node()` work the same way. Naming a host adapter switches to server output; pass `output: "static"` to keep a static build with that host's platform files.
|
|
89
89
|
- A config that only sets `site` or `base` stays as it is: `deployment: { site, base }` is still the static form.
|
|
90
90
|
- `redirects` read `:name` segments and a trailing `*` as patterns, matched the same way on every host (see [Pattern redirects](/docs/deployment#pattern-redirects)). Blume 1 never supported patterns and handed them to each host as written, so check any `from` or `to` that holds one.
|
|
91
|
-
- The `--adapter`, `--output`, and `--base` flags on `blume build` are gone, and passing one stops the build with an error naming the `deployment` setting that replaces it. Set the adapter in `blume.config.ts`, and name it explicitly: server output is no longer inferred from the platform's environment.
|
|
91
|
+
- The `--adapter`, `--output`, and `--base` flags on `blume build` are gone, and passing one stops the build with an error naming the `deployment` setting that replaces it. Set the adapter in `blume.config.ts`, and name it explicitly: server output is no longer inferred from the platform's environment. A CI step that passed a different `--base` per deploy can [read it from an environment variable](/docs/deployment#a-different-base-per-deploy) in the config instead.
|
|
92
92
|
|
|
93
93
|
See [Deployment](/docs/deployment) for each adapter's options.
|
|
94
94
|
|
|
@@ -133,6 +133,7 @@ export default defineConfig({
|
|
|
133
133
|
- `mdx-remote`, `sanity`, `notion`, and `obsidian` become `mdxRemote()`, `sanity()`, `notion()`, and `obsidian()`, with every other field moving into the call unchanged. `{ type: "custom", source }` becomes `custom(source)`.
|
|
134
134
|
- `content.root`, `content.include`, and `content.exclude` are still the shorthand for a single folder, but they can't sit beside `sources` any more. Move them into the `filesystem()` entry.
|
|
135
135
|
- Release pages from `githubReleases()` publish in one language now, so a multi-locale site no longer copies them to every other locale's URL (`/de/changelog/…`). If other sites link to those copies, add [redirects](/docs/deployment#redirects) to the default-locale pages.
|
|
136
|
+
- Text typed in Notion renders as written now: a component, `{`, or Markdown character in a `notion()` page shows as typed instead of being read as MDX. If your Notion pages hold components or Markdown typed as text, set [`mdx: true`](/docs/content/sources/notion#writing-mdx-in-notion) on the source.
|
|
136
137
|
|
|
137
138
|
## API references
|
|
138
139
|
|
package/docs/04-migrating.mdx
CHANGED
|
@@ -22,15 +22,15 @@ Swap `fumadocs` for your framework, and `--codex` for `--claude` to use Claude C
|
|
|
22
22
|
|
|
23
23
|
Name the framework you're migrating from, or leave it out and Blume detects it from the project's own files. When you name one and the project looks like another, Blume warns and continues with the one you named:
|
|
24
24
|
|
|
25
|
-
| Source | Detected from |
|
|
26
|
-
| --- | --- |
|
|
27
|
-
| `mintlify` | `docs.json` or `mint.json` |
|
|
28
|
-
| `fumadocs` | `source.config.ts`, or `fumadocs-core`, `fumadocs-ui`, or `fumadocs-mdx` in `package.json` |
|
|
29
|
-
| `docusaurus` | `docusaurus.config.*` |
|
|
30
|
-
| `starlight` | `@astrojs/starlight` in `package.json` |
|
|
31
|
-
| `nextra` | `nextra` in `package.json` |
|
|
32
|
-
|
|
33
|
-
Each source has its own mapping reference in the playbook. A site built with anything else still migrates: run the command without a source, and the agent inventories the repo first, then works from the playbook's general rules.
|
|
25
|
+
| Source | Detected from | Guide |
|
|
26
|
+
| --- | --- | --- |
|
|
27
|
+
| `mintlify` | `docs.json` or `mint.json` | [Migrate from Mintlify](/guides/migrate-from-mintlify) |
|
|
28
|
+
| `fumadocs` | `source.config.ts`, or `fumadocs-core`, `fumadocs-ui`, or `fumadocs-mdx` in `package.json` | [Migrate from Fumadocs](/guides/migrate-from-fumadocs) |
|
|
29
|
+
| `docusaurus` | `docusaurus.config.*` | [Migrate from Docusaurus](/guides/migrate-from-docusaurus) |
|
|
30
|
+
| `starlight` | `@astrojs/starlight` in `package.json` | [Migrate from Starlight](/guides/migrate-from-starlight) |
|
|
31
|
+
| `nextra` | `nextra` in `package.json` | [Migrate from Nextra](/guides/migrate-from-nextra) |
|
|
32
|
+
|
|
33
|
+
Each source has its own mapping reference in the playbook. A site built with anything else still migrates: run the command without a source, and the agent inventories the repo first, then works from the playbook's general rules. The [MkDocs Material guide](/guides/migrate-from-mkdocs-material) walks through a migration that way.
|
|
34
34
|
|
|
35
35
|
## What the agent does
|
|
36
36
|
|
package/docs/08-faq.mdx
CHANGED
|
@@ -87,18 +87,18 @@ We reported it upstream in [oxc-project/oxc#24096](https://github.com/oxc-projec
|
|
|
87
87
|
|
|
88
88
|
Patch oxfmt so it preserves the line break that sits directly against a `:::` fence. Blume ships the same fix in its own repo, and you can apply it in any project.
|
|
89
89
|
|
|
90
|
-
1. Save the patch as `patches/oxfmt@0.
|
|
91
|
-
|
|
92
|
-
```diff patches/oxfmt@0.
|
|
93
|
-
diff --git a/dist/markdown-
|
|
94
|
-
index
|
|
95
|
-
--- a/dist/markdown-
|
|
96
|
-
+++ b/dist/markdown-
|
|
97
|
-
@@ -
|
|
90
|
+
1. Save the patch as `patches/oxfmt@0.71.0.patch`:
|
|
91
|
+
|
|
92
|
+
```diff patches/oxfmt@0.71.0.patch
|
|
93
|
+
diff --git a/dist/markdown-gizI1LFO.js b/dist/markdown-gizI1LFO.js
|
|
94
|
+
index 95a638402fa6f73b29ebb8d7a7ef229bee68a690..e2e2ea5b443fe10698c49c2ef3dc3d5909335915 100644
|
|
95
|
+
--- a/dist/markdown-gizI1LFO.js
|
|
96
|
+
+++ b/dist/markdown-gizI1LFO.js
|
|
97
|
+
@@ -4898,7 +4898,43 @@ function lu(e, t, r) {
|
|
98
98
|
case "sentence": return Oh(e, r);
|
|
99
99
|
case "word": return t.parser !== "mdx" ? zh(e, t) : Uh(e);
|
|
100
100
|
case "whitespace": {
|
|
101
|
-
- let { next: a } = e, u = a && /^>|^(?:[*+-]|#{1,6}|\d+[).])$/.test(a.value) && !
|
|
101
|
+
- let { next: a } = e, u = a && /^>|^(?:[*+-]|#{1,6}|\d+[).])$/.test(a.value) && !RE(e) && !(t.proseWrap === "preserve" && ME(e)) ? "never" : t.proseWrap;
|
|
102
102
|
+ let { next: a, previous: oxfmtFencePrev } = e;
|
|
103
103
|
+ // Preserve line breaks that sit directly against a `:::` container
|
|
104
104
|
+ // directive fence, so `proseWrap: "never"` keeps the opening/closing
|
|
@@ -135,7 +135,7 @@ Patch oxfmt so it preserves the line break that sits directly against a `:::` fe
|
|
|
135
135
|
+ }
|
|
136
136
|
+ oxfmtPrevIsTitledFence = oxfmtIsFence(oxfmtLineFirst);
|
|
137
137
|
+ }
|
|
138
|
-
+ let u = oxfmtIsFence(oxfmtFencePrev) || oxfmtPrevIsTitledFence || oxfmtIsFence(a) ? "preserve" : a && /^>|^(?:[*+-]|#{1,6}|\d+[).])$/.test(a.value) && !
|
|
138
|
+
+ let u = oxfmtIsFence(oxfmtFencePrev) || oxfmtPrevIsTitledFence || oxfmtIsFence(a) ? "preserve" : a && /^>|^(?:[*+-]|#{1,6}|\d+[).])$/.test(a.value) && !RE(e) && !(t.proseWrap === "preserve" && ME(e)) ? "never" : t.proseWrap;
|
|
139
139
|
return ou(e, n.value, u, !1, t);
|
|
140
140
|
}
|
|
141
141
|
case "emphasis": {
|
|
@@ -146,7 +146,7 @@ Patch oxfmt so it preserves the line break that sits directly against a `:::` fe
|
|
|
146
146
|
```json package.json
|
|
147
147
|
{
|
|
148
148
|
"patchedDependencies": {
|
|
149
|
-
"oxfmt@0.
|
|
149
|
+
"oxfmt@0.71.0": "patches/oxfmt@0.71.0.patch"
|
|
150
150
|
}
|
|
151
151
|
}
|
|
152
152
|
```
|
|
@@ -155,7 +155,7 @@ Patch oxfmt so it preserves the line break that sits directly against a `:::` fe
|
|
|
155
155
|
|
|
156
156
|
```yaml pnpm-workspace.yaml
|
|
157
157
|
patchedDependencies:
|
|
158
|
-
oxfmt@0.
|
|
158
|
+
oxfmt@0.71.0: patches/oxfmt@0.71.0.patch
|
|
159
159
|
```
|
|
160
160
|
|
|
161
161
|
3. Reinstall so the patch is applied:
|
|
@@ -75,8 +75,6 @@ Each takes a map of locale code to text too, like a [header tab label](/docs/con
|
|
|
75
75
|
|
|
76
76
|
The page appears only when nothing already occupies the `/changelog` route. To replace it with your own design, add a [custom page](/docs/advanced/custom-pages) at `pages/changelog.astro` — it takes over and Blume stops generating the default index. The `<Update>` component the previous timeline was built from still ships for a custom page that wants inline release notes. It isn't one of the MDX components, so import it in the `.astro` page itself: `import Update from "blume/components/content/Update.astro";`.
|
|
77
77
|
|
|
78
|
-
A header [tab](/docs/content/navigation#tabs) pointing at `/changelog` opens this index — no `href` needed.
|
|
79
|
-
|
|
80
78
|
## From GitHub Releases
|
|
81
79
|
|
|
82
80
|
Rather than authoring entries by hand, point the built-in [`githubReleases()` source](/docs/content/sources/github-releases) at a repo and every release becomes a `type: changelog` entry — the same timeline and feed, fed straight from the releases you already publish. Blume's own [changelog](/changelog) is built this way:
|
|
@@ -98,6 +96,25 @@ content: {
|
|
|
98
96
|
|
|
99
97
|
The release name becomes the title, its tag becomes `changelog.version`, and its published date sorts the timeline. Each release page also gets a unique meta description summarized from its notes — markdown stripped, section headings and changeset commit-hash prefixes dropped, trimmed to the search-snippet length [`blume audit`](/docs/cli/audit) checks for — instead of falling back to the site description. A private repo authenticates with the `GITHUB_TOKEN` environment variable. See [GitHub Releases](/docs/content/sources/github-releases) for every option.
|
|
100
98
|
|
|
99
|
+
## Keep entries out of the sidebar
|
|
100
|
+
|
|
101
|
+
Changelog entries are ordinary pages, so the generated sidebar lists them like any other: a `changelog/` folder, or a `githubReleases()` source with `prefix: "changelog"`, becomes a Changelog group, newest entry first, that grows with every release. Previous/next links run through it too, so the last docs page can lead into the newest release.
|
|
102
|
+
|
|
103
|
+
To keep the entries out of the docs sidebar, link the changelog with a header [tab](/docs/content/navigation#tabs) rather than a [header action](/docs/content/navigation#header-actions):
|
|
104
|
+
|
|
105
|
+
```ts blume.config.ts
|
|
106
|
+
navigation: {
|
|
107
|
+
tabs: [
|
|
108
|
+
{ label: "Docs", path: "/" },
|
|
109
|
+
{ label: "Changelog", path: "/changelog" },
|
|
110
|
+
],
|
|
111
|
+
}
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
The `/changelog` tab opens the index, with no `href` needed, and owns the `changelog/` section: pages outside it stop listing the releases, and their previous/next links stay among the docs. An entry's own page shows the release list as its sidebar. The index, the [RSS feed](#the-rss-feed), `llms.txt`, the sitemap, and search still list every entry. The tab's `path` has to match the folder or `prefix` the entries live under. The `Docs` tab at `/` is optional; it gives readers a tab to return to.
|
|
115
|
+
|
|
116
|
+
Setting `sidebar.hidden` on the entries doesn't do this: it unlists them everywhere, the index and the feed included.
|
|
117
|
+
|
|
101
118
|
## The RSS feed
|
|
102
119
|
|
|
103
120
|
Blume also builds a changelog feed at **`/changelog/rss.xml`**, sorted newest-first by `date`. Feeds need an absolute site URL, so set [`deployment.site`](/docs/deployment); Blume then injects a `<link rel="alternate">` tag on every page so readers discover it automatically.
|
|
@@ -255,7 +255,7 @@ Passing `siteUrl` (and `ogEnabled`) derives the page's `canonical` and a generat
|
|
|
255
255
|
|
|
256
256
|
Only this page changes — every other route keeps its generated card — so it's how you give the home page alone a bespoke share image. The generated card declares its size and alt text to crawlers on its own; for your own `ogImage`, pass `ogImageAlt` and `ogImageSize` alongside so the share card gets the same treatment.
|
|
257
257
|
|
|
258
|
-
The page also emits schema.org JSON-LD — the same
|
|
258
|
+
The page also emits schema.org JSON-LD — the same graph the docs pages carry, with a `WebPage` node on the home page — so the home page (usually a custom page) isn't the one URL without structured data. A custom page has no content file to date, so pass `lastModified` (a date string or `Date`) to emit its `dateModified`; without a machine-readable date, search engines can take any other date the page shows as the page's own. Pass `structuredDataEnabled={config.structuredData}` to keep it in sync with the [`structuredData`](/docs/discoverability/structured-data) config, or `structuredDataEnabled={false}` to turn it off for one page.
|
|
259
259
|
|
|
260
260
|
`page.title` is used verbatim as the document title (no `- siteTitle` suffix), since marketing pages usually set their own. To give a custom page the full docs chrome instead — sidebar, TOC, and all — wrap it in `RootLayout`, the layout the generated pages use. Pull the required props straight from `blume:data`:
|
|
261
261
|
|
package/docs/cli/doctor.mdx
CHANGED
|
@@ -19,7 +19,7 @@ blume doctor
|
|
|
19
19
|
- **Navigation that leads nowhere**: a tab, selector item, featured link, header action, or call to action whose path matches no page, custom `.astro` page, generated route, or reference: a warning (`BLUME_NAV_MISSING_PAGE`), the same one `blume dev` and `blume build` print.
|
|
20
20
|
- **[`<Component>`](/docs/content/components#component) examples that don't exist**: a `path` that names no file under `examples`, which the page would show as a "No example found" box: a warning (`BLUME_EXAMPLE_NOT_FOUND`) at its line, as `blume dev` and `blume build` print it. A `components.ts` override or island named `Component` replaces the built-in, and isn't checked.
|
|
21
21
|
- **Features that need a server** on a site configured for static output — the assistant, the MCP server, the Try it playground's built-in proxy, or server-mode search (Mixedbread): an error naming the feature, with the deployment adapter to switch to (or, when a host adapter is set to `output: "static"`, telling you to drop that option).
|
|
22
|
-
- **Packages your config needs** that aren't installed — the SDK a search, content source, or
|
|
22
|
+
- **Packages your config needs** that aren't installed — the SDK a search, content source, assistant, or narration adapter imports, a deployment adapter's `@astrojs/*` package, or the renderer for Vue or Svelte islands: an error naming each package, with the install command for your package manager. `blume build` stops on the same check.
|
|
23
23
|
- **`components.ts` overrides** Blume can't plan — an inline or computed entry, or an import whose file doesn't exist: an error for each, at its line.
|
|
24
24
|
- **A version-shaped folder** (`v1.0/`) on a site with no `versions` configured, which would otherwise build as ordinary content: a warning pointing at [`blume version`](/docs/cli/version).
|
|
25
25
|
- **Secrets an enabled feature reads** that aren't set, such as `MIXEDBREAD_API_KEY` or `OPENROUTER_API_KEY`: a warning naming the variable. Doctor loads `.env` and `.env.local` first, like `blume dev` and `blume build`, and checks before any remote content source fetches, so the warning still prints when a source then fails without its token.
|
|
@@ -39,6 +39,7 @@ Every identifier below — a project key, a measurement ID, a site token — is
|
|
|
39
39
|
| `amplitude()` | [Amplitude](#amplitude) | `key` |
|
|
40
40
|
| `clarity()` | [Microsoft Clarity](#microsoft-clarity) | `id` |
|
|
41
41
|
| `clearbit()` | [Clearbit](#clearbit) | `key` |
|
|
42
|
+
| `clics()` | [Clics](#clics) | `projectId` |
|
|
42
43
|
| `cloudflare()` | [Cloudflare Web Analytics](#cloudflare-web-analytics) | `token` |
|
|
43
44
|
| `databuddy()` | [Databuddy](#databuddy) | `clientId` |
|
|
44
45
|
| `fathom()` | [Fathom](#fathom) | `site` |
|
|
@@ -149,6 +150,20 @@ analytics: [
|
|
|
149
150
|
|
|
150
151
|
`domain` becomes `data-domain` and `host` picks where `/js/script.js` loads from. Any other option becomes its own `data-` attribute on the tag (`api: "/api/event"` → `data-api`). Script variants with extra behavior baked into the filename (`script.outbound-links.js`, …) are a different tag; load those with [`script()`](#custom-scripts).
|
|
151
152
|
|
|
153
|
+
## Clics
|
|
154
|
+
|
|
155
|
+
Pass the **project ID** from Configure → Tracking in the [Clics dashboard](https://platform.clics.dev) to `clics()`. Blume adds the [Clics browser tracker](https://docs.clics.dev/installation-guides) with the required `data-project-id` attribute.
|
|
156
|
+
|
|
157
|
+
```ts blume.config.ts lineNumbers
|
|
158
|
+
import { clics } from "blume/analytics";
|
|
159
|
+
|
|
160
|
+
analytics: [clics({ projectId: "YOUR_PROJECT_ID" })],
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
The tracker counts page loads and client-side navigations automatically, including Blume's client router, so don't add a second pageview hook. It also tracks outbound links by default. Set `disableOutboundLinks: true` to turn those events off. For a production build previewed on localhost, set `allowLocalhost: true`; ordinary `blume dev` does not load analytics.
|
|
164
|
+
|
|
165
|
+
Blume's [custom events](#custom-events) don't reach Clics: its browser tracker has no global event function to forward them to. To send your own events, call the [`track()` helper](https://docs.clics.dev/custom-events) from `@clicsdev/tracker` in your own client code.
|
|
166
|
+
|
|
152
167
|
## Fathom
|
|
153
168
|
|
|
154
169
|
Pass your **site ID** (from the site's script settings in [Fathom](https://usefathom.com)) to `fathom()`.
|
|
@@ -387,6 +402,9 @@ analytics: [
|
|
|
387
402
|
| `amplitude()` | anything else | `autocapture: true`, `fetchRemoteConfig: true` | Merged into the `amplitude.init` options verbatim. |
|
|
388
403
|
| `clarity()` | `id` | — | Clarity project ID. Required. |
|
|
389
404
|
| `clearbit()` | `key` | — | Clearbit publishable API key. Required. |
|
|
405
|
+
| `clics()` | `projectId` | — | Public Clics project ID. Required. |
|
|
406
|
+
| `clics()` | `allowLocalhost` | `false` | Emit `data-allow-localhost` for local previews of a production build. |
|
|
407
|
+
| `clics()` | `disableOutboundLinks` | `false` | Emit `data-disable-outbound-links` to stop outbound-link tracking. |
|
|
390
408
|
| `cloudflare()` | `token` | — | Cloudflare Web Analytics site token (manual setup). Required. |
|
|
391
409
|
| `cloudflare()` | anything else | — | Forwarded in the beacon's `data-cf-beacon` JSON verbatim. |
|
|
392
410
|
| `databuddy()` | `clientId` | — | Databuddy client ID. Required. |
|
|
@@ -431,4 +449,4 @@ analytics: [
|
|
|
431
449
|
|
|
432
450
|
## Custom events
|
|
433
451
|
|
|
434
|
-
Blume's page feedback (`feedback`, and `feedback_comment` when [written feedback](/docs/configuration#written-feedback) is on), [search](/docs/configuration/search#search-analytics) (`search`, `search_select`), [assistant](/docs/configuration/assistant#analytics) (`ask`, `ask_answer`, `ask_error`), and [narration](/docs/configuration/narration#analytics) player (`narration_play`, `narration_complete`) send custom events through every configured adapter that has a client API — PostHog, Mixpanel, Heap, Segment, Hightouch, Amplitude, LogRocket, Adobe, Google Analytics, Google Tag Manager (as a `{ event }` push on `window.dataLayer`), Plausible, Databuddy, Fathom, OneDollarStats (with each property value as a string), Pirsch, Clarity, Hotjar, and Vercel. Fathom, Clarity, and Hotjar take an event's name alone, so they count each event without its properties. Cloudflare and Clearbit have no event API. Every event also fires as a `blume:track` CustomEvent on `window` with `{ event, props }` in `detail`, so a `script()` adapter can forward it anywhere else. The assistant never sends a question's text to a provider, and search sends its queries as typed unless you set [`search.analytics.queries`](/docs/configuration/search#search-analytics) to `false`, which sends each query's length instead; either way the text still reaches the `blume:track` event, for a listener you write to forward. For deploying your built site, see [Deployment](/docs/deployment).
|
|
452
|
+
Blume's page feedback (`feedback`, and `feedback_comment` when [written feedback](/docs/configuration#written-feedback) is on), [search](/docs/configuration/search#search-analytics) (`search`, `search_select`), [assistant](/docs/configuration/assistant#analytics) (`ask`, `ask_answer`, `ask_error`), and [narration](/docs/configuration/narration#analytics) player (`narration_play`, `narration_complete`) send custom events through every configured adapter that has a client API — PostHog, Mixpanel, Heap, Segment, Hightouch, Amplitude, LogRocket, Adobe, Google Analytics, Google Tag Manager (as a `{ event }` push on `window.dataLayer`), Plausible, Databuddy, Fathom, OneDollarStats (with each property value as a string), Pirsch, Clarity, Hotjar, and Vercel. Fathom, Clarity, and Hotjar take an event's name alone, so they count each event without its properties. Cloudflare and Clearbit have no event API, and Clics has no global one for Blume to call. Every event also fires as a `blume:track` CustomEvent on `window` with `{ event, props }` in `detail`, so a `script()` adapter can forward it anywhere else. The assistant never sends a question's text to a provider, and search sends its queries as typed unless you set [`search.analytics.queries`](/docs/configuration/search#search-analytics) to `false`, which sends each query's length instead; either way the text still reaches the `blume:track` event, for a listener you write to forward. For deploying your built site, see [Deployment](/docs/deployment).
|
|
@@ -496,6 +496,7 @@ Each of these has its own guide. The config field is the entry point:
|
|
|
496
496
|
| `seo` | Metadata, OG images, feeds, structured data, sitemap, robots | [SEO and GEO](/docs/discoverability) |
|
|
497
497
|
| `deployment` | A host adapter from `blume/deploy`, or `{ site, base }` for a static build | [Deployment](/docs/deployment) |
|
|
498
498
|
| `redirects` | Permanent and temporary redirects | [Deployment](/docs/deployment#redirects) |
|
|
499
|
+
| `poweredBy` | The `X-Powered-By: Blume` response header (default `true`) | [Deployment](/docs/deployment#powered-by-header) |
|
|
499
500
|
| `integrations` | Astro integrations appended after Blume's built-ins | [Customization](/docs/configuration/customization#astro-integrations) |
|
|
500
501
|
| `basePath` | A path every generated route mounts under (e.g. `/docs`), invisible to the sidebar | [Deployment](/docs/deployment#mount-the-docs-under-a-path) |
|
|
501
502
|
| `i18n` | Locales, the default locale, and translated UI strings | [Internationalization](/docs/content/i18n) |
|
|
@@ -44,7 +44,7 @@ This needs nothing from you and costs nothing, but how it sounds depends on the
|
|
|
44
44
|
|
|
45
45
|
## Generated voices
|
|
46
46
|
|
|
47
|
-
Pass a `provider` to generate audio with a neural speech model at build time. The provider is the
|
|
47
|
+
Pass a `provider` to generate audio with a neural speech model at build time. The provider is one of the assistant's [adapters](/docs/configuration/assistant#adapters), pointed at a speech model: `gateway()` for the speech models on the Vercel AI Gateway, or [`openai()`](#openai-and-self-hosted-voices) for OpenAI's own speech models or a speech server you run yourself.
|
|
48
48
|
|
|
49
49
|
```ts blume.config.ts lineNumbers
|
|
50
50
|
import { defineConfig } from "blume";
|
|
@@ -60,7 +60,7 @@ export default defineConfig({
|
|
|
60
60
|
});
|
|
61
61
|
```
|
|
62
62
|
|
|
63
|
-
`blume build` reads each built page exactly as the player will, splits it into sentences, and generates one clip per sentence through the [Vercel AI Gateway](https://vercel.com/docs/ai-gateway). The clips and a small manifest per page are written into your build as static files under `/blume-narration/`, so replays cost nothing and nothing runs on a server.
|
|
63
|
+
`blume build` reads each built page exactly as the player will, splits it into sentences, and generates one clip per sentence through the provider, here the [Vercel AI Gateway](https://vercel.com/docs/ai-gateway). The clips and a small manifest per page are written into your build as static files under `/blume-narration/`, so replays cost nothing and nothing runs on a server.
|
|
64
64
|
|
|
65
65
|
Before it generates anything, the build logs how many new clips it needs and how many characters they cover, so the cost is visible up front:
|
|
66
66
|
|
|
@@ -68,6 +68,8 @@ Before it generates anything, the build logs how many new clips it needs and how
|
|
|
68
68
|
Generating narration: 214 new clip(s), 15,880 characters, with openai/tts-1-hd
|
|
69
69
|
```
|
|
70
70
|
|
|
71
|
+
`gateway()` takes:
|
|
72
|
+
|
|
71
73
|
| Option | Default | Description |
|
|
72
74
|
| --- | --- | --- |
|
|
73
75
|
| `model` | `openai/tts-1-hd` | A gateway speech model: `openai/tts-1`, `openai/tts-1-hd`, `fish-audio/s2.1-pro`, `spacexai/grok-tts`, and others the gateway lists. |
|
|
@@ -77,21 +79,56 @@ Generating narration: 214 new clip(s), 15,880 characters, with openai/tts-1-hd
|
|
|
77
79
|
| `headers` | | Static headers sent with every request. |
|
|
78
80
|
| `providerOptions` | | Passed to the AI SDK's `generateSpeech` as-is, for model settings Blume doesn't name. |
|
|
79
81
|
|
|
82
|
+
### OpenAI and self-hosted voices
|
|
83
|
+
|
|
84
|
+
`openai()` generates the clips with OpenAI's speech API instead: OpenAI's own models with your `OPENAI_API_KEY`, or, with a `baseUrl`, any server that serves the same `POST /audio/speech` endpoint, such as [Kokoro-FastAPI](https://github.com/remsky/Kokoro-FastAPI), [Speaches](https://speaches.ai), or a [LiteLLM](https://docs.litellm.ai) proxy with a speech model. A site whose build can't reach a hosted service gets generated voices this way:
|
|
85
|
+
|
|
86
|
+
```ts blume.config.ts lineNumbers
|
|
87
|
+
import { defineConfig } from "blume";
|
|
88
|
+
import { openai } from "blume/ai";
|
|
89
|
+
|
|
90
|
+
export default defineConfig({
|
|
91
|
+
narration: {
|
|
92
|
+
provider: openai({
|
|
93
|
+
baseUrl: "http://localhost:8880/v1",
|
|
94
|
+
model: "kokoro",
|
|
95
|
+
voice: "af_heart",
|
|
96
|
+
}),
|
|
97
|
+
},
|
|
98
|
+
});
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
A server that takes a key, like a LiteLLM proxy, gets the one you name in `apiKeyEnv` (`openai({ baseUrl, apiKeyEnv: "LITELLM_API_KEY", model })`). Without `apiKeyEnv`, a `baseUrl` server is called with no key, so your `OPENAI_API_KEY` never leaves for another server. A response that isn't MP3, like a sign-in page from a proxy in front of the server or WAV from a server that ignores the format it was asked for, counts as a failed clip rather than being cached.
|
|
102
|
+
|
|
103
|
+
It speaks through the AI SDK's OpenAI provider, with or without a `baseUrl`, so add `@ai-sdk/openai` to your project (`npm install @ai-sdk/openai`). If it's missing, `blume build` stops before Vite runs and names the package.
|
|
104
|
+
|
|
105
|
+
| Option | Default | Description |
|
|
106
|
+
| --- | --- | --- |
|
|
107
|
+
| `model` | | Required. A speech model: `gpt-4o-mini-tts`, `tts-1`, or `tts-1-hd` on OpenAI, or the id your server serves. |
|
|
108
|
+
| `baseUrl` | | The server's OpenAI-compatible base URL, including the `/v1`. Without it, the clips come from OpenAI, or from the server `OPENAI_BASE_URL` names when that's set, as with OpenAI's own SDKs. |
|
|
109
|
+
| `voice` | `alloy` | The model's voice. |
|
|
110
|
+
| `instructions` | | How the voice should sound, for models that take instructions (`gpt-4o-mini-tts`). |
|
|
111
|
+
| `apiKeyEnv` | `OPENAI_API_KEY` without a `baseUrl` | The env var holding the key, sent as a bearer token. With a `baseUrl`, only a key you name here is sent, so your `OPENAI_API_KEY` never reaches another server; name none and requests go without a bearer token (an `Authorization` you set in `headers` is still sent). |
|
|
112
|
+
| `headers` | | Static headers sent with every request. |
|
|
113
|
+
| `providerOptions` | | Passed to the AI SDK's `generateSpeech` as-is. OpenAI's speech API reads `speed` from it: `{ openai: { speed: 1.1 } }`. |
|
|
114
|
+
|
|
115
|
+
OpenAI's speech API takes no language, so `openai()` doesn't send the page's, and a sentence that appears in several languages is generated once. OpenAI's models read the language off the text, but many self-hosted servers go by the voice, so pick one that speaks your content's language.
|
|
116
|
+
|
|
80
117
|
### Caching
|
|
81
118
|
|
|
82
|
-
Clips are cached in `node_modules/.cache/blume/narration` by what decides their sound: the sentence, its language, the model, the voice, and the
|
|
119
|
+
Clips are cached in `node_modules/.cache/blume/narration` by what decides their sound: the sentence, its language (for `gateway()`, which sends it), the model, the voice, the instructions, the `providerOptions`, and, for `openai()`, the server it calls. Pointing `baseUrl` at another server regenerates every clip, even with the same model and voice. A rebuild only pays for sentences that changed, and a sentence shared by many pages is generated once. Vercel and Netlify restore `node_modules` from their build caches, so a deploy there regenerates only what changed. On other CI, cache that directory between runs.
|
|
83
120
|
|
|
84
121
|
### Fallbacks
|
|
85
122
|
|
|
86
123
|
Generated voices fall back to browser voices wherever no clips exist:
|
|
87
124
|
|
|
88
125
|
- In `blume dev`, which never generates audio. Run `blume build` and `blume preview` to hear the generated voice locally.
|
|
89
|
-
- When the key isn't set at build time. The build warns and
|
|
90
|
-
- When generation fails
|
|
126
|
+
- When the key isn't set at build time. The build warns and generates nothing new, but pages whose clips are all cached still get them. `openai()` with a `baseUrl` and no `apiKeyEnv` sends no key, so it goes ahead.
|
|
127
|
+
- When generation fails, a server the build can't reach included. Generation stops at the first failed clip, since the AI SDK has already retried it, and the build carries on with a warning, keeping every clip it has cached.
|
|
91
128
|
|
|
92
129
|
## Languages
|
|
93
130
|
|
|
94
|
-
Narration reads each page in its content language, so on an [internationalized](/docs/content/i18n) site every locale is read in its own voice. The spoken cues and the player's labels are translated in every built-in UI language. Override them per locale under `narration` in [`i18n.ui`](/docs/content/i18n#translated-ui).
|
|
131
|
+
Narration reads each page in its content language, so on an [internationalized](/docs/content/i18n) site every locale is read in its own voice. With `openai()`, that depends on the model and voice: see [OpenAI and self-hosted voices](#openai-and-self-hosted-voices). The spoken cues and the player's labels are translated in every built-in UI language. Override them per locale under `narration` in [`i18n.ui`](/docs/content/i18n#translated-ui).
|
|
95
132
|
|
|
96
133
|
## Turning it off for a page
|
|
97
134
|
|
|
@@ -64,7 +64,7 @@ navigation: {
|
|
|
64
64
|
|
|
65
65
|
- **`flat`** (default) — a non-collapsible header with its pages listed beneath. Pages that aren't in any group always list first, above the group sections, so they can't be mistaken for a group's children.
|
|
66
66
|
- **`group`** — a collapsible `<details>` disclosure per group. Groups start collapsed by default; a group containing the current page always starts open, so only the section you're in is expanded. Set `collapsed: false` in [folder meta](/docs/content/meta) to force a group open regardless.
|
|
67
|
-
- **`page`** — each group is a single row
|
|
67
|
+
- **`page`** — each group is a single row whose chevron slides the sidebar into a sub-panel showing only that group's items, with a back arrow at the top. A group with an index page links the row's name to that page, and a group without one slides the panel in from anywhere on the row. The panel is route-aware, so landing directly on a page inside the group opens straight to it. After the back arrow, the row of the section you're in stays highlighted.
|
|
68
68
|
|
|
69
69
|
:::tip
|
|
70
70
|
`page` mode keeps deep sections tidy — reach for it when groups have many children and you'd rather drill into them than scroll past them.
|
|
@@ -100,7 +100,7 @@ A generated group's effective mode resolves highest priority first:
|
|
|
100
100
|
3. The global `navigation.sidebar.display`
|
|
101
101
|
4. The Blume default (`flat`)
|
|
102
102
|
|
|
103
|
-
A group's `display` applies to that group only — nested subgroups resolve their own value through the same chain. A `page`-mode group with an index page
|
|
103
|
+
A group's `display` applies to that group only — nested subgroups resolve their own value through the same chain. A `page`-mode group with an index page links its row's name to that page, so clicking the name opens the landing page with its sub-panel already open, while the chevron beside it slides the panel in without leaving the current page. The panel's title links back to the landing page, and the index page lists as the panel's first item unless it's [hidden](#hidden-pages). A `page`-mode group without an index page has nothing to open, so its whole row slides the panel in.
|
|
104
104
|
|
|
105
105
|
`sidebar.display` means nothing anywhere else — on a non-index page, on the content root's own `index` page (the root isn't a group; use `navigation.sidebar.display`), or on any page when an [explicit sidebar](#explicit-sidebar) is configured (its items own each group's mode) — so Blume reports a `BLUME_SIDEBAR_DISPLAY_IGNORED` warning instead of silently dropping it. `collapsed` remains specific to `group` mode; it's inert when a group resolves to `flat` or `page`.
|
|
106
106
|
|
|
@@ -304,6 +304,8 @@ navigation: {
|
|
|
304
304
|
|
|
305
305
|
Like tab labels, a `label` here can be a map of locale code to text on a [multilingual](/docs/content/i18n#per-locale-navigation) site.
|
|
306
306
|
|
|
307
|
+
An action is only a link, so a changelog linked this way still lists its entries in the sidebar. Link it with a [tab](#tabs) to [keep them out](/docs/advanced/changelog#keep-entries-out-of-the-sidebar).
|
|
308
|
+
|
|
307
309
|
`cta` is singular on purpose — a docs header has room for exactly one thing a reader is being asked to do, and a row of buttons asks for nothing. Secondary links belong in `actions`, or in [`featured`](#featured-links) if they should sit with the sidebar instead.
|
|
308
310
|
|
|
309
311
|
An `http(s)` or protocol-relative href opens in a new tab; a route stays in the tab, and is validated against your pages at build time like a `featured` link — so a page served by another app on the same host (`/signup` on the product, say) should be written as an absolute URL. `actions` are hidden below the `sm` breakpoint, where the header has room for the logo and the navigation toggle and nothing else. `cta` hides there too, except on a page with no navigation toggle — a landing page on `PageLayout` with no tabs — where it stays, since nothing else can surface it on a phone.
|
|
@@ -33,3 +33,5 @@ export default defineConfig({
|
|
|
33
33
|
```
|
|
34
34
|
|
|
35
35
|
The Delivery API token comes from the `CONTENTFUL_ACCESS_TOKEN` environment variable, which the adapter declares. Without it, the source stops before its first request with a `BLUME_MISSING_SECRET` error rather than Contentful's `401`, unless it has a [cached snapshot](/docs/content/sources#caching-and-offline-builds) to serve instead. Under `--preview` the adapter reads drafts through the Preview API with `CONTENTFUL_PREVIEW_TOKEN`. The Preview API rejects delivery tokens, so `--preview` without a preview token fails with a clear error rather than falling back to `CONTENTFUL_ACCESS_TOKEN`. A space with EU data residency is served from other hosts: set `host` to `cdn.eu.contentful.com` and `previewHost` to `preview.eu.contentful.com`. Map `fields.order` to a number field to set each page's sidebar order; it has no default. Assets are referenced from Contentful's CDN rather than downloaded — their URLs are stable. A link to another entry of the source's `contentType` points at that entry's page, at the route the page is built at (its slug under the source's `prefix`). A link to an entry the source doesn't turn into a page, one of another content type or one your `params` filter out, keeps only its text. The Delivery API leaves an unpublished, archived, or deleted asset or entry out of its response, so a page that embeds or links one renders without it (a link keeps its text) and the source warns with `BLUME_SOURCE_UNRESOLVED_LINK`. An embedded entry maps to a Blume component through the engine's `serializers` option, keyed by content type id, when you construct `contentfulSource` directly and pass it to [`custom()`](/docs/content/sources/custom); setting `serializers` writes the source's pages as MDX so the returned components render, and an embedded entry without a serializer is noted in a comment.
|
|
36
|
+
|
|
37
|
+
[Publish developer documentation from Contentful](/guides/contentful-documentation-website) walks through it end to end: a content model for docs pages and code blocks, drafts reviewed with a preview token, and a static site that rebuilds whenever an editor publishes.
|
|
@@ -37,3 +37,5 @@ changelog: {
|
|
|
37
37
|
```
|
|
38
38
|
|
|
39
39
|
A private repo authenticates with the `GITHUB_TOKEN` environment variable — the same token the other GitHub features use, never inlined into your config; the adapter declares it, so a build without it warns. For a repository on GitHub Enterprise Server, set `baseUrl` to its REST API (`https://<host>/api/v3`); `GITHUB_TOKEN` is sent there, so it must be a token for that server. Like [every remote source](/docs/content/sources#caching-and-offline-builds) it's cached under `.blume/cache/<source>/` and served offline if the API is unreachable. Because a changelog is supplementary, a fetch failure with no cache (say a CI build without a token) degrades to an empty changelog with a warning rather than failing the build — set `GITHUB_TOKEN` in your CI and deploy environments to populate it.
|
|
40
|
+
|
|
41
|
+
[Turn GitHub releases into a product changelog](/guides/github-releases-changelog-website) walks through it end to end: a changelog timeline, a page per release, and an RSS feed, rebuilt every time you publish a release.
|
|
@@ -3,7 +3,7 @@ title: Notion
|
|
|
3
3
|
description: Import a Notion database with the notion() source — rows become pages, properties become frontmatter, and blocks become Blume components.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
The built-in `notion()` adapter turns a Notion database into a collection: each row becomes a page, its properties become frontmatter, and its block tree becomes MDX. Callouts, toggles, columns, and code blocks map to the matching Blume components, and the text you type in Notion renders as written: a `{`, `<`, or Markdown character in a page is escaped rather than read as MDX, JSX, or formatting. Video blocks become a `<YouTube>` embed when they hold a YouTube link and a `<video>` player otherwise, with the block's caption as a `<Frame>` caption either way. A link to a video page rather than a media file (a Vimeo or Loom URL, say) is reported as a build warning, and its `<video>` player keeps pointing at the page, which it can't play — link to that video from the text instead. The adapter declares `@notionhq/client` (v5 or later) as its runtime dependency — an optional peer; Blume reads the database through its first data source.
|
|
6
|
+
The built-in `notion()` adapter turns a Notion database into a collection: each row becomes a page, its properties become frontmatter, and its block tree becomes MDX. Callouts, toggles, columns, and code blocks map to the matching Blume components, and the text you type in Notion renders as written: a `{`, `<`, or Markdown character in a page is escaped rather than read as MDX, JSX, or formatting, unless you [opt in to MDX](#writing-mdx-in-notion). Video blocks become a `<YouTube>` embed when they hold a YouTube link and a `<video>` player otherwise, with the block's caption as a `<Frame>` caption either way. A link to a video page rather than a media file (a Vimeo or Loom URL, say) is reported as a build warning, and its `<video>` player keeps pointing at the page, which it can't play — link to that video from the text instead. The adapter declares `@notionhq/client` (v5 or later) as its runtime dependency — an optional peer; Blume reads the database through its first data source.
|
|
7
7
|
|
|
8
8
|
```ts blume.config.ts
|
|
9
9
|
import { defineConfig } from "blume";
|
|
@@ -30,3 +30,18 @@ The integration token comes from the `NOTION_TOKEN` environment variable (share
|
|
|
30
30
|
The `Status` property is a publish gate by default. A page whose Status (a status or select property) holds any value other than `publishedValue`, which defaults to `Published`, imports with `draft: true`, and production builds drop drafts. A page with no Status value is published, and a database without the property publishes every page. Notion's default status options are Not started, In progress, and Done, so a database that uses them has no `Published` value and publishes nothing until you set `publishedValue: "Done"` (or whichever option means published). `properties.status` names a differently named property; to import every page whatever its status, point it at a property the database doesn't have.
|
|
31
31
|
|
|
32
32
|
**Notion image and video URLs are signed and expire**, so the adapter downloads them at build time into the site's assets and rewrites the references — a CMS asset never rots a static build. Only a file the server reports as an image or video is saved (or, when the response doesn't say, one whose URL names an image or video extension); anything else keeps its original URL with a build warning, so nothing but media is ever served from your site's origin. API calls are paced through a small request pool (3 at a time, matching Notion's per-integration rate limit) so databases with hundreds of pages import without tripping `429` responses; set `concurrency` on the source to tune it.
|
|
33
|
+
|
|
34
|
+
[Turn a Notion database into a docs site](/guides/notion-documentation-website) walks through it end to end, with a Status property that decides what goes live. [Combine Markdown and Notion in one documentation site](/guides/markdown-notion-documentation) gives Notion editors a section of their own beside the Markdown in Git.
|
|
35
|
+
|
|
36
|
+
## Writing MDX in Notion
|
|
37
|
+
|
|
38
|
+
Notion has no block for most Blume components. To use one in a Notion page (a `<CardGroup>` of `<Card>`s, say), type its MDX as text and set `mdx: true`: the source then reads the text of paragraphs, headings, list items, quotes, and callouts as MDX, so components, `{expressions}`, and Markdown typed in Notion render alongside Notion's own bold, italic, links, and inline code. Code blocks, toggle titles, image and video captions, and properties stay plain text.
|
|
39
|
+
|
|
40
|
+
```ts blume.config.ts
|
|
41
|
+
notion({
|
|
42
|
+
database: "8f2c1e0a4b7d4f3c9e6a5d2b1c0f9e8d",
|
|
43
|
+
mdx: true,
|
|
44
|
+
});
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
The text is then held to MDX's rules, like an `.mdx` file: a stray `{` or `<` fails the page, and a paragraph starting with `import` reads as an import statement. **MDX runs code when the site builds**, so anyone who can edit the database can run code on your build machine and read its environment variables, `NOTION_TOKEN` included. Turn `mdx` on only for a database whose editors you'd trust with the site's code.
|
|
@@ -35,3 +35,5 @@ A link to an `index` note lands on its folder's route rather than a phantom `/in
|
|
|
35
35
|
Dot-folders are skipped, including Obsidian's own `.obsidian` config directory and `.trash` — which the dev watcher also ignores, so moving a pane in the app doesn't rebuild your site. Editing a note does, and so does deleting one to the trash, since the note leaves the vault. The directories no content scan reads (`node_modules`, `dist`, `.git`, …) are skipped too, so a vault rooted at the project itself doesn't publish dependency READMEs. A note with `#` or `?` in its path is left out with an error, the way a [content file](/docs/content#files-and-routes) is: Astro can't load its copy, so rename it. Symlinks inside the vault are followed, the way the filesystem source follows them, so a shared folder linked into the vault publishes with it. A vault that sits inside the filesystem source's root must be excluded from it (`filesystem({ root: "docs", exclude: ["**/_*", "**/.*", "vault/**"] })` — an `exclude` replaces the default `["**/_*", "**/.*"]` rather than adding to it, so keep those two to leave `_`-prefixed partials and dot-files unpublished); [`blume version <id>`](/docs/cli/version) then leaves it out of the snapshot, since the vault keeps publishing its own notes as current.
|
|
36
36
|
|
|
37
37
|
Not yet lowered: callouts (`> [!note]`) render as plain blockquotes, embeds (`![[image.png]]`) pass through untouched, multi-line `%%comments%%` are left in place, and there is no backlink graph.
|
|
38
|
+
|
|
39
|
+
[Publish Obsidian notes as a docs site](/guides/publish-obsidian-documentation) walks through it end to end, from a vault to a deployed site.
|
|
@@ -29,3 +29,5 @@ export default defineConfig({
|
|
|
29
29
|
```
|
|
30
30
|
|
|
31
31
|
The API key comes from the `PAYLOAD_API_KEY` environment variable and is sent as `users API-Key <key>`; set `authCollection` when the key belongs to another auth-enabled collection. Only published documents are imported — `--preview` requests drafts and stages them with `draft: true`. Documents are fetched with `depth: 1` so uploads carry their URLs, and a relative upload path (`/media/x.png`) resolves against `url`. Uploads are referenced at that URL rather than downloaded, since it doesn't expire the way Notion's do, so readers' browsers load them from your Payload server or its storage; to have Blume download and optimize them at build instead, authorize the host in [`image.domains`](/docs/configuration#images). A `block` or `inlineBlock` node maps to a Blume component through the engine's `serializers` option, keyed by `blockType`, when you construct `payloadSource` directly and pass it to [`custom()`](/docs/content/sources/custom). Setting `serializers` writes the source's pages as MDX so the returned components render; a body held in a Markdown text field stays Markdown. A node with no Markdown equivalent, or a block with no serializer, is left out as a comment, and the source warns with `BLUME_SOURCE_UNSUPPORTED_NODE` naming it and the page.
|
|
32
|
+
|
|
33
|
+
[Build a documentation website with Payload CMS](/guides/payload-cms-documentation-website) walks through it end to end: a collection published as a searchable docs site, with draft previews and a rebuild whenever an editor publishes.
|
|
@@ -42,3 +42,5 @@ A private repo's token is read from the `GITHUB_TOKEN` environment variable —
|
|
|
42
42
|
Remote pages are rendered with full MDX-plus-component fidelity: their bodies are materialized into a hidden staging directory and rendered through Astro alongside your local docs, so callouts, tabs, and every other Blume component keep working.
|
|
43
43
|
|
|
44
44
|
Like every remote source, fetched pages are cached for offline builds and refreshed with `blume sync` — see [Caching and offline builds](/docs/content/sources#caching-and-offline-builds).
|
|
45
|
+
|
|
46
|
+
[Build one docs portal from multiple GitHub repositories](/guides/documentation-multiple-repositories) uses it to pull each repository's docs folder into one site, pinned to a release that moves only when you change it.
|
|
@@ -27,3 +27,5 @@ export default defineConfig({
|
|
|
27
27
|
```
|
|
28
28
|
|
|
29
29
|
A read token for a private dataset comes from the `SANITY_TOKEN` environment variable. The adapter declares it, so `blume dev` and `blume build` warn when it's unset; a public dataset works without it. Sanity answers a private dataset's query without a token with no documents rather than an error, so when a query finds nothing and the token is unset, the source adds a `BLUME_MISSING_SECRET` warning saying so. Under `--preview` the adapter reads drafts with the `previewDrafts` perspective, which `@sanity/client` 8 reports as renamed to `drafts`; the old name still works, and `@sanity/client` 6 accepts only the old one. Custom Portable Text block types map to Blume components through the engine's `serializers` option, available when you construct `sanitySource` directly and pass it to [`custom()`](/docs/content/sources/custom). Setting `serializers` writes the source's pages as MDX, so a component or directive a serializer returns renders.
|
|
30
|
+
|
|
31
|
+
[Use Sanity as a CMS for developer documentation](/guides/sanity-documentation-website) walks through it end to end: editors writing in Sanity Studio while developers keep Markdown in Git, with drafts only in protected previews.
|
|
@@ -30,3 +30,5 @@ export default defineConfig({
|
|
|
30
30
|
```
|
|
31
31
|
|
|
32
32
|
The API token comes from the `STRAPI_API_TOKEN` environment variable, sent as a bearer token. Entries are fetched with `populate=*` so images carry their URLs (set `populate` to narrow it), and a relative upload path (`/uploads/x.png`) in a Blocks field resolves against `url`. A Markdown field passes through as written, so a relative path there stays relative to your docs site and breaks: use absolute image URLs in Markdown fields, as Strapi's Markdown editor inserts them. Only published entries are imported — `--preview` requests drafts (`status=draft` on Strapi 5, `publicationState=preview` on Strapi 4) and stages each document that has never been published with `draft: true`; a published document previews its latest draft.
|
|
33
|
+
|
|
34
|
+
[Use Strapi to manage a documentation website](/guides/strapi-documentation-website) walks through it end to end: editors publishing in Strapi, a static site rebuilt on every publish, and drafts kept off production.
|
|
@@ -23,6 +23,8 @@ agents: {
|
|
|
23
23
|
|
|
24
24
|
If a content or custom page already owns the route, the server isn't generated: the build warns, and `llms.txt` and the other [discovery documents](/docs/discoverability/agent-discovery) leave it out.
|
|
25
25
|
|
|
26
|
+
[Add an MCP server to your docs](/guides/mcp-server-for-documentation) walks through it from turning it on to deploying the server and connecting a client.
|
|
27
|
+
|
|
26
28
|
## Tools and resources
|
|
27
29
|
|
|
28
30
|
The server exposes read-only tools — `search_docs`, `get_page`, `list_pages`, and `get_navigation` — and every page as an MCP resource (`resources/list` enumerates the pages at their served URLs with a `text/markdown` type, leaving out i18n fallback copies of untranslated pages as `list_pages` does; `resources/read` returns the page's [agent Markdown](/docs/discoverability/markdown), the same output as `get_page`), so clients that attach context by URI can browse the docs without calling a tool. It publishes discovery documents at `/.well-known/mcp.json` and `/.well-known/mcp/server-card.json`. The server card follows the [SEP-2127](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2127) Server Card extension schema (reverse-DNS `name`, `remotes` transport endpoints), with initialize-shaped compat fields (`serverInfo`, `capabilities`, `transports`) for scanners built against the proposal's earlier revision. Each page's **Connect to MCP** menu offers copy-and-go install for Claude Code, Cursor, VS Code, and Codex (shown once [`deployment.site`](/docs/deployment) is set).
|