blume 2.1.1 → 2.1.3
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 +23 -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 +7 -6
- 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
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
title: Structured data
|
|
3
|
-
description: The schema.org JSON-LD every page carries — WebSite,
|
|
3
|
+
description: The schema.org JSON-LD every page carries — WebSite, page, and breadcrumb nodes — plus the optional Organization and SoftwareApplication blocks that tell search engines and agents who you are.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
Blume emits [schema.org](https://schema.org) JSON-LD in every page's `<head>` so search engines understand your content. On by default:
|
|
@@ -14,10 +14,12 @@ seo: {
|
|
|
14
14
|
Each page includes:
|
|
15
15
|
|
|
16
16
|
- a **WebSite** node for site identity,
|
|
17
|
-
- the page
|
|
18
|
-
- a **BreadcrumbList** built from the navigation trail.
|
|
17
|
+
- the page itself, with its description, language, and publish date: a **WebPage** on the homepage, and an **article** everywhere else — `BlogPosting` for blog posts, `TechArticle` for changelog and docs,
|
|
18
|
+
- on an article, a **BreadcrumbList** built from the navigation trail.
|
|
19
19
|
|
|
20
|
-
|
|
20
|
+
The homepage is the root of the navigation, so each locale's home (`/fr`) and the root of a [`basePath`](/docs/deployment#mount-the-docs-under-a-path) are WebPages too.
|
|
21
|
+
|
|
22
|
+
URLs are absolute when [`deployment.site`](/docs/deployment) is set. Without it they're route-relative, and the WebSite node, which needs an absolute identifier, is left out. Pages marked [`seo.noindex`](/docs/discoverability/metadata#per-page-overrides) are skipped. When [`lastModified`](/docs/configuration#last-modified) is on, each page's date is also emitted as `dateModified`, and the visible "Last updated" date is marked up as a `<time datetime>`, so search engines read that date rather than some other date on the page.
|
|
21
23
|
|
|
22
24
|
## Site identity
|
|
23
25
|
|
|
@@ -42,10 +44,10 @@ seo: {
|
|
|
42
44
|
}
|
|
43
45
|
```
|
|
44
46
|
|
|
45
|
-
`organization` adds an **Organization** node to every page — the WebSite and
|
|
47
|
+
`organization` adds an **Organization** node to every page — the WebSite and page nodes cite it as `publisher` — with the email and telephone as a `ContactPoint` (`contactType` defaults to `"customer support"`) and the address as a `PostalAddress`, the two fields business-verification checks look for. `name` and `url` default to the site's; a root-relative `logo` is absolutized like any page URL.
|
|
46
48
|
|
|
47
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`.
|
|
48
50
|
|
|
49
51
|
## Custom pages
|
|
50
52
|
|
|
51
|
-
A custom [`.astro` page](/docs/advanced/custom-pages) rendered through `PageLayout` emits the same
|
|
53
|
+
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.
|
|
@@ -22,6 +22,8 @@ navigation: {
|
|
|
22
22
|
}
|
|
23
23
|
```
|
|
24
24
|
|
|
25
|
+
[Document Kafka events with AsyncAPI](/guides/asyncapi-kafka-documentation) walks through it for Kafka topics, with producer and consumer code beside the reference.
|
|
26
|
+
|
|
25
27
|
## Spec versions
|
|
26
28
|
|
|
27
29
|
AsyncAPI **2.x specs are normalized to 3.x automatically** with the official AsyncAPI converter, so `publish`/`subscribe` channels map onto `send`/`receive` operation pages with stable URLs — later upgrading the spec file itself through the converter moves nothing. The converter is an optional peer dependency, so a site with a 1.x or 2.x spec installs it (`npm install @asyncapi/converter`); without it the build fails with that install command. A 3.x spec needs nothing extra. Operations group by tag; untagged operations group under their channel address.
|
|
@@ -38,6 +38,8 @@ navigation: {
|
|
|
38
38
|
}
|
|
39
39
|
```
|
|
40
40
|
|
|
41
|
+
[Generate GraphQL docs from your schema](/guides/graphql-documentation-website) walks through it from a schema to a deployed site, with a Try it panel that queries a sandbox.
|
|
42
|
+
|
|
41
43
|
## Generated examples
|
|
42
44
|
|
|
43
45
|
Every operation page carries a complete, valid example operation — one variable per argument, typed off the schema, with a bounded-depth selection set over the return type — plus matching example variables and an example response that mirrors the same selection. A variable whose argument declares a default starts at that default, and the selection leaves deprecated fields out. Code samples show the exact HTTP request (a JSON `POST` of `{ query, variables }`) in each configured language. The same [languages](/docs/references/openapi#code-samples-and-schemas) as `openapi()` are available, and `codeSamples: false` shows none:
|
|
@@ -20,6 +20,8 @@ export default defineConfig({
|
|
|
20
20
|
|
|
21
21
|
That mounts the reference at `/reference` (an overview page) with each operation at `/reference/<tag>/<operation>`. The `spec` is either an `http(s)` URL or a path to a local file in your project. Blume parses it with [Scalar's OpenAPI parser](https://github.com/scalar/scalar) — Swagger 2.0 and OpenAPI 3.0 specs are upgraded to 3.1 automatically, and 3.1 and [3.2](#openapi-32) specs are read as written. An adapter is a plain description of the reference — not a parsed spec — so Blume can validate it up front and inline it into the generated site; leaving `reference` out (or empty) renders no reference at all. Documenting an event-driven or GraphQL API instead? See [AsyncAPI](/docs/references/asyncapi) and [GraphQL](/docs/references/graphql).
|
|
22
22
|
|
|
23
|
+
To go from your framework's generated spec to a deployed reference, follow the guide for [FastAPI](/guides/fastapi-documentation-website), [Django REST Framework](/guides/django-rest-framework-api-documentation), [NestJS](/guides/nestjs-api-documentation-website), [Spring Boot](/guides/spring-boot-api-documentation), [Hono](/guides/hono-zod-api-documentation), [Fastify](/guides/fastify-api-documentation), [Elysia](/guides/elysia-api-documentation), or [oRPC](/guides/orpc-openapi-documentation). [Generate API docs from an OpenAPI spec](/guides/openapi-documentation-website) starts from any spec file.
|
|
24
|
+
|
|
23
25
|
The reference doesn't add a header tab on its own. To surface it, point a [navigation tab](/docs/content/navigation#tabs) at its route — this also scopes the operations sidebar for the native renderer:
|
|
24
26
|
|
|
25
27
|
```ts blume.config.ts
|
|
@@ -139,6 +141,8 @@ paths:
|
|
|
139
141
|
$ref: ./code_samples/go/list_plants.go
|
|
140
142
|
```
|
|
141
143
|
|
|
144
|
+
[Add SDK examples to generated API documentation](/guides/openapi-sdk-code-samples) walks through samples for a TypeScript and a Python SDK, with a check that catches samples an SDK release broke.
|
|
145
|
+
|
|
142
146
|
## Try it playground
|
|
143
147
|
|
|
144
148
|
Operation pages rendered natively ship an interactive **Try it** panel by default. Blume generates the form from the operation itself: an input per path, query, and header parameter, a body editor built from the request-body schema, everything prefilled from the spec's examples. Values go on the wire the way the spec describes them: array and object parameters follow their `style` and `explode` (`tags=dog&tags=cat` in a query, `1,2` in a path, `filter[color]=red` for `deepObject`), and an `application/x-www-form-urlencoded` or `multipart/form-data` body is sent as form fields rather than JSON. A server picker lists the operation's servers (its own `servers`, else its path's, else the spec's, with each `{variable}` at its `default`), with a free-text field for any other base URL, and auth inputs match the operation's [resolved security](#authorization) — bearer token, API key, and basic credentials, with OAuth2 as a token paste field (bring an access token; Blume doesn't run the flow).
|
|
@@ -174,6 +178,8 @@ reference: [
|
|
|
174
178
|
|
|
175
179
|
The built-in proxy only forwards requests to the origins your specs declare in `servers` (at the document, path, or operation level, with variables at their defaults) — including across redirects — so a public docs deployment can't be aimed at other hosts on its network. A **Custom base URL** typed into the panel isn't a documented server: with the proxy enabled, requests to it are refused with a 403. It forwards only the headers the panel sets itself — the credentials and header parameters filled in, and the body's `Content-Type` — so cookies, credentials the browser attaches for the docs site (HTTP Basic auth on a password-protected preview, say), and headers your host adds (`X-Forwarded-For`, `CF-*`, `X-Vercel-*`) never reach the API. It reads a request body only up to 4 MB (anything larger gets a `413`), and every response it relays carries `Content-Security-Policy: sandbox`, `X-Content-Type-Options: nosniff`, and `Cross-Origin-Resource-Policy: same-origin` — plus `Content-Disposition: attachment` for HTML or SVG — so an API error page that echoes its input can't run script on the docs origin. The proxy is also [rate limited](/docs/configuration/rate-limiting) per reader, like every server route a reader can call.
|
|
176
180
|
|
|
181
|
+
When a Try it request is blocked, [Fix CORS errors in an API documentation playground](/guides/api-documentation-cors-errors) reproduces it, reads the preflight the browser sends, and fixes it with a CORS policy on your API or the built-in proxy.
|
|
182
|
+
|
|
177
183
|
## Multiple specs
|
|
178
184
|
|
|
179
185
|
Use `sources` to publish more than one spec from one adapter. Each source gets its own overview route and operation pages, and shares the adapter's display options. Give each a `label`, which names its group in the sidebar as written and derives its route, or set an explicit `route`:
|
|
@@ -191,6 +197,8 @@ reference: [
|
|
|
191
197
|
|
|
192
198
|
`spec` is shorthand for a single-entry `sources`, so you only reach for `sources` when you have more than one. When two specs need different display options — a different code-sample set, say — list two `openapi()` adapters instead, each with its own `route`. An embedded [Scalar](/docs/references/scalar) reference beside native pages is its own `scalar()` adapter in the list. Sources are resolved in list order, and when two resolve to the same route the first one wins (the build warns about the one it dropped).
|
|
193
199
|
|
|
200
|
+
[Combine multiple OpenAPI specifications in one docs site](/guides/multiple-openapi-specs-documentation) builds an API portal this way, with each service's spec on its own route and tab.
|
|
201
|
+
|
|
194
202
|
### Per-source indexing
|
|
195
203
|
|
|
196
204
|
Generated pages participate in search, `llms.txt`, and crawler indexing by default. A secondary or overlapping spec can opt out of any surface without hiding its pages or removing it from navigation:
|
|
@@ -259,6 +267,8 @@ Each action's `target` is a [JSONPath](https://www.rfc-editor.org/rfc/rfc9535) e
|
|
|
259
267
|
|
|
260
268
|
Overlays apply to the spec as written, before Blume upgrades it to OpenAPI 3.1, so an overlay targets the version you wrote the spec in. Everything built from the spec sees the result: the operation pages, the sidebar, search, `llms.txt`, and the MCP server. With `sources`, give each source its own `overlays`. An overlay that doesn't apply fails the reference the way an unreadable spec does, so a broken overlay can't publish what it was meant to hide.
|
|
261
269
|
|
|
270
|
+
[Remove internal endpoints from public API docs](/guides/openapi-overlays-public-docs) walks through an overlay that strips internal operations from a generated spec on every build.
|
|
271
|
+
|
|
262
272
|
## Authorization
|
|
263
273
|
|
|
264
274
|
Operations that declare [security requirements](https://spec.openapis.org/oas/v3.1.0#security-requirement-object) render an **Authorization** section above their parameters, and the generated code samples send a placeholder credential (`Authorization: Bearer YOUR_TOKEN`, an API-key header, or a query key — whatever the scheme calls for). There's nothing to configure: Blume reads `security` from the spec, so the reference always matches what the API actually enforces.
|
|
@@ -295,6 +305,8 @@ An operation's [`callbacks`](https://spec.openapis.org/oas/v3.1.0#callback-objec
|
|
|
295
305
|
|
|
296
306
|
Webhook pages are real pages: they're in search, `llms.txt`, and the MCP server, and their Markdown tells agents it's a request the API sends, so an agent won't try to call it.
|
|
297
307
|
|
|
308
|
+
[Write webhook documentation with OpenAPI 3.1](/guides/openapi-webhook-documentation) adds signature headers and acknowledgments to the webhook pages, plus a receiving guide with a working Node.js receiver.
|
|
309
|
+
|
|
298
310
|
## Spec warnings
|
|
299
311
|
|
|
300
312
|
Blume doesn't validate a spec against the OpenAPI schema: run a linter such as [Spectral](https://github.com/stoplightio/spectral) or [oasdiff](https://github.com/oasdiff/oasdiff) in CI for that. It does warn, in `blume dev`, `blume build`, and `blume check`, about mistakes that would otherwise render wrong without any sign, and about parts of a spec it leaves out. Each warning names the spec, and the rest of the reference still builds:
|
|
@@ -309,6 +321,8 @@ Blume doesn't validate a spec against the OpenAPI schema: run a linter such as [
|
|
|
309
321
|
| `BLUME_OPENAPI_X_WEBHOOKS` | A 3.1 or later spec keeps its webhooks under `x-webhooks`, the extension 3.0 tools read, and has no `webhooks`, so none of them render. Rename the field to `webhooks`. |
|
|
310
322
|
| `BLUME_OPENAPI_REF_PATH_ITEM` | A path or webhook is a `$ref` to a shared path item, such as one under `components.pathItems`. Blume doesn't resolve these, so its operations are missing from the reference. Inline the path item under `paths` or `webhooks`; its schemas, parameters, and responses can stay `$ref`s. |
|
|
311
323
|
|
|
324
|
+
[Keep API documentation in sync with backend changes in CI](/guides/openapi-documentation-ci) runs those checks in GitHub Actions: it exports the spec from the backend, fails when the spec is stale, invalid, or breaking, and rebuilds the reference from it.
|
|
325
|
+
|
|
312
326
|
## Embedding Scalar instead
|
|
313
327
|
|
|
314
328
|
`openapi()` always renders Blume's own pages. To embed [Scalar](https://scalar.com)'s self-contained API reference UI on a single route instead — its own sidebar, search, theme, and request client — list a `scalar()` adapter from `blume/reference` in place of (or beside) this one. The [Scalar](/docs/references/scalar) page covers what the embed does and doesn't do, and how to pass Scalar's own options through.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "blume",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.3",
|
|
4
4
|
"description": "The open-source docs framework for humans and agents.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"agents",
|
|
@@ -113,19 +113,19 @@
|
|
|
113
113
|
"dependencies": {
|
|
114
114
|
"@astrojs/check": "^0.9.10",
|
|
115
115
|
"@astrojs/markdown-satteri": "^0.4.1",
|
|
116
|
-
"@astrojs/mdx": "^8.0.
|
|
117
|
-
"@astrojs/node": "^11.1.
|
|
116
|
+
"@astrojs/mdx": "^8.0.2",
|
|
117
|
+
"@astrojs/node": "^11.1.6",
|
|
118
118
|
"@astrojs/react": "^7.0.0",
|
|
119
119
|
"@astrojs/vercel": "^11.0.11",
|
|
120
120
|
"@clack/prompts": "^1.8.0",
|
|
121
|
-
"@iconify-json/lucide": "^1.2.
|
|
121
|
+
"@iconify-json/lucide": "^1.2.140",
|
|
122
122
|
"@iconify/types": "^2.0.0",
|
|
123
123
|
"@iconify/utils": "^3.1.7",
|
|
124
124
|
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
125
125
|
"@orama/orama": "^3.1.18",
|
|
126
126
|
"@pierre/diffs": "^1.5.1",
|
|
127
127
|
"@scalar/astro": "^0.4.26",
|
|
128
|
-
"@scalar/openapi-parser": "^0.29.
|
|
128
|
+
"@scalar/openapi-parser": "^0.29.11",
|
|
129
129
|
"@scalar/openapi-types": "^0.9.5",
|
|
130
130
|
"@shikijs/transformers": "^4.4.3",
|
|
131
131
|
"@shikijs/twoslash": "^4.4.3",
|
|
@@ -134,6 +134,7 @@
|
|
|
134
134
|
"@types/mdast": "^4.0.4",
|
|
135
135
|
"@types/node": "^22.20.2",
|
|
136
136
|
"@vercel/analytics": "^2.0.1",
|
|
137
|
+
"@vitejs/plugin-react": "6.1.1",
|
|
137
138
|
"ai": "^7.0.99",
|
|
138
139
|
"astro": "^7.3.2",
|
|
139
140
|
"chokidar": "^5.0.0",
|
|
@@ -141,7 +142,7 @@
|
|
|
141
142
|
"consola": "^3.4.2",
|
|
142
143
|
"cross-spawn": "^7.0.6",
|
|
143
144
|
"dompurify": "^3.4.16",
|
|
144
|
-
"dotenv": "^
|
|
145
|
+
"dotenv": "^18.0.5",
|
|
145
146
|
"epub-gen-memory": "^1.1.2",
|
|
146
147
|
"es-module-lexer": "^3.0.2",
|
|
147
148
|
"fast-xml-parser": "^5.11.1",
|
package/skills/blume/SKILL.md
CHANGED
|
@@ -68,7 +68,7 @@ Every build publishes a generated agent skill for the site at `/skill.md` (`agen
|
|
|
68
68
|
- **Components** — callouts, cards, steps, tabs, accordions, badges, file trees, and parameter tables, usable in MDX with no imports.
|
|
69
69
|
- **Local search** — Orama in dev and production, with no hosted index; Pagefind, Algolia, and other backends are one adapter away (`search: pagefind()` from `blume/search`).
|
|
70
70
|
- **AI** — `llms.txt`, raw Markdown URLs, a JSON docs API with an OpenAPI description, Copy as Markdown, Open in chat, an in-page assistant, and an MCP server endpoint served by the docs site itself.
|
|
71
|
-
- **Narration** — a "Listen to this page" player (`narration: true` for browser voices, or `narration: { provider: gateway({ … }) }` from `blume/ai` for voices generated at build).
|
|
71
|
+
- **Narration** — a "Listen to this page" player (`narration: true` for browser voices, or `narration: { provider: gateway({ … }) }` from `blume/ai` for voices generated at build; `openai({ model, voice })` uses OpenAI's speech models instead, and with a `baseUrl` a self-hosted OpenAI-compatible speech server, and needs `@ai-sdk/openai` installed).
|
|
72
72
|
- **Navigation** — inferred from files, refined with `meta.ts` or config.
|
|
73
73
|
- **SEO** — metadata, Open Graph images, RSS feeds, and JSON-LD.
|
|
74
74
|
- **Customization** — component overrides, React islands, custom pages, theme tokens, and a source-component registry via `blume add`.
|
|
@@ -32,7 +32,7 @@ Throughout this skill (including the `references/` files), **`<skill>` means the
|
|
|
32
32
|
3. **Write `blume.config.ts`** with `defineConfig` from `blume`. Map only declared fields (see the reference's mapping table); rely on defaults everywhere else. A minimal result is `defineConfig({ title: "…" })`.
|
|
33
33
|
4. **Restructure content.** Choose `content.root` (default `docs`) — **detect where `.md`/`.mdx` actually live, don't assume a `docs/` folder.** Many repos keep content directly under an app dir (`apps/docs/api/`, `.../getting-started/`) with no `docs/` subfolder; when so, set `content.root` to that dir and scope `content.include` to the real content folders rather than leaving a bare `content.root: "."` that scans everything (see `references/monorepo.md` §1). Order with numeric prefixes (`01-intro.mdx`), group without a URL segment via `(group)/` folders, and add a `meta.ts` (`defineMeta`) only where filesystem order isn't enough. **A source that already declares per-folder navigation in a sidecar file — Fumadocs `meta.json`, Nextra `_meta.*` — _is_ that case: convert each one to a `meta.ts`, carrying over its title/icon/order/collapse, rather than dropping it and hoping filenames reproduce the intent. Filesystem inference is the fallback for folders that declare no per-folder nav, never a reason to discard one that does.** Reach for an explicit `navigation.sidebar` only when the source nav genuinely can't be expressed by files. **Reshaping into folder-per-tab moves URLs** — track every old→new path as you go; you'll turn them into `redirects` in step 5.
|
|
34
34
|
5. **Rewrite pages.** Map frontmatter to Blume's strict schema; convert callout JSX to `:::` directives — **directives (and math/mermaid/package-install fences) are MDX-only, so rename any `.md` page that needs them to `.mdx`**; rename components; turn snippets/partials into `<include>` statements or inline them (Blume has no import-based includes: `import Snippet from "…"` plus `<Snippet />` has to become one or the other); fix asset paths; **rewrite internal links** to their new routes (including OpenAPI/GraphQL operation links — see the API-reference sections, their slugs differ from most sources); **add a `redirects` entry for every route you moved** in step 4; **convert every icon name to Lucide** (Blume is Lucide-only — no FontAwesome/Tabler). Remove any duplicated H1 in the body (`title` renders the H1; bodies start at `##`). **If the source has a hand-maintained changelog and the repo is open source on GitHub, offer to swap it for the `githubReleases()` source** (see "Changelogs" below) rather than porting the entries. For **Mintlify**, run the bundled codemod first — `node <skill>/scripts/mintlify-codemod.mjs --write <content-dir>` deterministically remaps icons and drops/renames unsupported frontmatter keys, and reports the rest (unknown icons, OpenAPI-stub flags) for you to finish by hand (see `references/mintlify.md`).
|
|
35
|
-
6. **Adopt `package.json`.** Repoint `dev`/`build`/`start` → `blume dev`/`blume build`/`blume preview`, remove the old framework's deps, add `blume`. A config-only source (e.g. a bare Mintlify `docs.json`) has no manifest — scaffold one. **In a pnpm workspace:** if `pnpm-workspace.yaml`/`.npmrc` sets `minimumReleaseAge`, add **only** `blume` to `minimumReleaseAgeExclude` (don't disable the guard) so the just-published version installs. **Always regenerate the lockfile in the same change:** after editing deps run a plain `pnpm install` (from the workspace root) and commit `pnpm-lock.yaml` alongside `package.json` — CI/Vercel use `--frozen-lockfile`, so a stale lockfile fails the build before it starts. **If the repo uses (or the user wants) [Ultracite](https://www.ultracite.ai) for formatting:** its oxfmt formatter mangles the `:::` directives you just wrote unless you ship the bundled `assets/oxfmt@0.
|
|
35
|
+
6. **Adopt `package.json`.** Repoint `dev`/`build`/`start` → `blume dev`/`blume build`/`blume preview`, remove the old framework's deps, add `blume`. A config-only source (e.g. a bare Mintlify `docs.json`) has no manifest — scaffold one. **In a pnpm workspace:** if `pnpm-workspace.yaml`/`.npmrc` sets `minimumReleaseAge`, add **only** `blume` to `minimumReleaseAgeExclude` (don't disable the guard) so the just-published version installs. **Always regenerate the lockfile in the same change:** after editing deps run a plain `pnpm install` (from the workspace root) and commit `pnpm-lock.yaml` alongside `package.json` — CI/Vercel use `--frozen-lockfile`, so a stale lockfile fails the build before it starts. **If the repo uses (or the user wants) [Ultracite](https://www.ultracite.ai) for formatting:** its oxfmt formatter mangles the `:::` directives you just wrote unless you ship the bundled `assets/oxfmt@0.71.0.patch` and register it under `patchedDependencies` — see `references/monorepo.md` §6. See `references/monorepo.md` §2–3.
|
|
36
36
|
7. **Wire up the host repo & deploy (non-trivial repos).** For a monorepo on Vercel, emit the root-aware install/build recipe and `apps/docs/vercel.json`, and tell the user the two settings you can't commit (Vercel Root Directory, Node 22). If the workspace pins Vite and `blume build` crashes inside Astro/Vite, apply the pnpm-patch workaround. All copy-pasteable in `references/monorepo.md` §4–5.
|
|
37
37
|
8. **Verify.** Run `blume build` (frontmatter schema, duplicate routes, config — it fails on any error diagnostic by default; **never pass `--no-strict`**, which builds anyway and silently drops invalid pages) and `blume validate --strict` (internal links, heading anchors, assets — the link checker lives in `validate`, not `build`), fix diagnostics, then `blume dev` for a visual pass. End with a written summary of what was migrated, dropped, and approximated — **and every repo-specific edit you made** (pnpm-workspace, vercel.json, config globs) with the reason, plus any manual step left to the user (the Astro patch, Vercel dashboard settings).
|
|
38
38
|
|
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
diff --git a/dist/markdown-
|
|
2
|
-
index
|
|
3
|
-
--- a/dist/markdown-
|
|
4
|
-
+++ b/dist/markdown-
|
|
5
|
-
@@ -
|
|
1
|
+
diff --git a/dist/markdown-gizI1LFO.js b/dist/markdown-gizI1LFO.js
|
|
2
|
+
index 95a638402fa6f73b29ebb8d7a7ef229bee68a690..e2e2ea5b443fe10698c49c2ef3dc3d5909335915 100644
|
|
3
|
+
--- a/dist/markdown-gizI1LFO.js
|
|
4
|
+
+++ b/dist/markdown-gizI1LFO.js
|
|
5
|
+
@@ -4898,7 +4898,43 @@ function lu(e, t, r) {
|
|
6
6
|
case "sentence": return Oh(e, r);
|
|
7
7
|
case "word": return t.parser !== "mdx" ? zh(e, t) : Uh(e);
|
|
8
8
|
case "whitespace": {
|
|
9
|
-
- let { next: a } = e, u = a && /^>|^(?:[*+-]|#{1,6}|\d+[).])$/.test(a.value) && !
|
|
9
|
+
- let { next: a } = e, u = a && /^>|^(?:[*+-]|#{1,6}|\d+[).])$/.test(a.value) && !RE(e) && !(t.proseWrap === "preserve" && ME(e)) ? "never" : t.proseWrap;
|
|
10
10
|
+ let { next: a, previous: oxfmtFencePrev } = e;
|
|
11
11
|
+ // Preserve line breaks that sit directly against a `:::` container
|
|
12
12
|
+ // directive fence, so `proseWrap: "never"` keeps the opening/closing
|
|
@@ -43,7 +43,7 @@ index bc9037f6c0de5516b139d8cdb195b1e25cd33bc0..a02c284e28bb535f9964a8a086ebb654
|
|
|
43
43
|
+ }
|
|
44
44
|
+ oxfmtPrevIsTitledFence = oxfmtIsFence(oxfmtLineFirst);
|
|
45
45
|
+ }
|
|
46
|
-
+ let u = oxfmtIsFence(oxfmtFencePrev) || oxfmtPrevIsTitledFence || oxfmtIsFence(a) ? "preserve" : a && /^>|^(?:[*+-]|#{1,6}|\d+[).])$/.test(a.value) && !
|
|
46
|
+
+ 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;
|
|
47
47
|
return ou(e, n.value, u, !1, t);
|
|
48
48
|
}
|
|
49
49
|
case "emphasis": {
|
|
@@ -190,13 +190,13 @@ This is a **manual step — do not fabricate the patch contents.** Leave the use
|
|
|
190
190
|
|
|
191
191
|
If the repo uses (or the user wants to adopt) **[Ultracite](https://www.ultracite.ai)** for formatting — an `ultracite check` / `ultracite fix` script, an `ultracite` dev dep, or oxlint/oxfmt in the toolchain — it will format the migrated `.md`/`.mdx` **and break Blume's `:::` directives.** Ultracite's formatter is **oxfmt**, and under `proseWrap` oxfmt joins the opening/closing `:::` fence line into the surrounding prose, which invalidates the directive (`:::note` … `:::` callouts, titled fences like `:::warning[Heads up]`, tabs, steps — the exact syntax you convert callouts _into_ in step 5). Blume's own repo hits this and pins a patched oxfmt; a migrated repo needs the same patch or every directive silently degrades to literal text on the next `ultracite fix`.
|
|
192
192
|
|
|
193
|
-
The fix is a committed **pnpm patch** (`patches/oxfmt@0.
|
|
193
|
+
The fix is a committed **pnpm patch** (`patches/oxfmt@0.71.0.patch`), shipped with this skill at `assets/oxfmt@0.71.0.patch` — the same diff Blume's own repo applies, covering titled fences as well as bare ones. Unlike the Astro/Vite patch, this one is a **known, deterministic diff** — copy it in, don't regenerate it:
|
|
194
194
|
|
|
195
195
|
```bash
|
|
196
196
|
# 1. Copy the shipped patch into the target repo's patches/ dir (keep the exact filename;
|
|
197
197
|
# <skill> = this skill's directory, the one containing SKILL.md):
|
|
198
198
|
mkdir -p patches
|
|
199
|
-
cp "<skill>/assets/oxfmt@0.
|
|
199
|
+
cp "<skill>/assets/oxfmt@0.71.0.patch" patches/oxfmt@0.71.0.patch
|
|
200
200
|
```
|
|
201
201
|
|
|
202
202
|
Then register it under `patchedDependencies` — `pnpm-workspace.yaml` on pnpm 10+, or root `package.json` (`pnpm.patchedDependencies`) on pnpm 9:
|
|
@@ -204,12 +204,12 @@ Then register it under `patchedDependencies` — `pnpm-workspace.yaml` on pnpm 1
|
|
|
204
204
|
```yaml
|
|
205
205
|
# pnpm-workspace.yaml
|
|
206
206
|
patchedDependencies:
|
|
207
|
-
oxfmt@0.
|
|
207
|
+
oxfmt@0.71.0: patches/oxfmt@0.71.0.patch
|
|
208
208
|
```
|
|
209
209
|
|
|
210
210
|
Commit **both** the patch file and the `patchedDependencies` entry, then re-run `pnpm install`.
|
|
211
211
|
|
|
212
|
-
- **The patch is pinned to `oxfmt@0.
|
|
212
|
+
- **The patch is pinned to `oxfmt@0.71.0`** (the version Blume's own repo pins). pnpm requires an exact version match, and the diff targets a file whose name is hashed per oxfmt release (`dist/markdown-*.js`), so it won't apply to any other version. If `pnpm why oxfmt` reports a different one, pin `oxfmt` to `0.71.0` as a direct dev dependency (Ultracite accepts any `oxfmt` ≥ 0.59 as a peer). If the repo can't take that pin, tell the user and fall back to keeping directive-heavy files out of the formatter's globs.
|
|
213
213
|
- **Not on pnpm?** The patch mechanism is pnpm-specific. For npm/yarn, either pin oxfmt and apply the diff with `patch-package`, or exclude `.md`/`.mdx` from Ultracite formatting so it never touches the directives — report whichever you chose.
|
|
214
214
|
|
|
215
215
|
---
|
|
@@ -221,5 +221,5 @@ Commit **both** the patch file and the `patchedDependencies` entry, then re-run
|
|
|
221
221
|
- [ ] Ran plain `pnpm install`; committed `pnpm-lock.yaml` with the `package.json` change; verified `pnpm install --frozen-lockfile` is clean.
|
|
222
222
|
- [ ] Wrote `apps/docs/vercel.json` + package scripts; told the user to set Root Directory + Node 22 in the Vercel project.
|
|
223
223
|
- [ ] Checked for a workspace Vite override; if the build crashes inside Astro/Vite, gave the pnpm-patch recipe.
|
|
224
|
-
- [ ] Uses Ultracite/oxfmt? Shipped `patches/oxfmt@0.
|
|
224
|
+
- [ ] Uses Ultracite/oxfmt? Shipped `patches/oxfmt@0.71.0.patch` + registered it under `patchedDependencies` so formatting doesn't mangle `:::` directives.
|
|
225
225
|
- [ ] Reported every repo-specific edit and every manual step left to the user.
|
package/src/ai/ask.ts
CHANGED
|
@@ -110,7 +110,8 @@ export interface AssistantGatewayOptions extends AssistantAdapterOptions {
|
|
|
110
110
|
* How much the model reasons before answering. Sent as the AI SDK's
|
|
111
111
|
* top-level `reasoning` option, which the gateway maps to the model's own
|
|
112
112
|
* control (OpenAI's `reasoning_effort`, for example); the model has to
|
|
113
|
-
* offer the level you pick. Omitted keeps the model's default.
|
|
113
|
+
* offer the level you pick. Omitted keeps the model's default. Assistant
|
|
114
|
+
* only.
|
|
114
115
|
*/
|
|
115
116
|
reasoning?: AssistantReasoning;
|
|
116
117
|
/** Narration only: the speech model's voice. Defaults to `alloy`. */
|
|
@@ -153,32 +154,47 @@ export const gateway = (
|
|
|
153
154
|
// openai()
|
|
154
155
|
// ---------------------------------------------------------------------------
|
|
155
156
|
|
|
156
|
-
const OPENAI_API_KEY_ENV = "OPENAI_API_KEY";
|
|
157
|
+
export const OPENAI_API_KEY_ENV = "OPENAI_API_KEY";
|
|
157
158
|
|
|
158
159
|
/** Options for {@link openai}. */
|
|
159
160
|
export interface AssistantOpenAIOptions extends AssistantAdapterOptions {
|
|
161
|
+
/**
|
|
162
|
+
* Name of the env var holding the key. Defaults to `OPENAI_API_KEY`, except
|
|
163
|
+
* for narration with a `baseUrl`, which sends only a key named here.
|
|
164
|
+
*/
|
|
165
|
+
apiKeyEnv?: string;
|
|
160
166
|
/**
|
|
161
167
|
* An OpenAI-compatible endpoint to call instead of OpenAI
|
|
162
168
|
* (`https://llm.internal.example.com/v1`): a self-hosted model or an
|
|
163
169
|
* internal gateway. The route then talks Chat Completions through
|
|
164
170
|
* `@ai-sdk/openai-compatible`, and the docs tools are off unless
|
|
165
171
|
* `ai.assistant.tools` turns them on, since the model may not call tools.
|
|
172
|
+
* For narration, a server with OpenAI's speech API (`/audio/speech`),
|
|
173
|
+
* called through `@ai-sdk/openai` and sent only the key `apiKeyEnv` names,
|
|
174
|
+
* if any.
|
|
166
175
|
*/
|
|
167
176
|
baseUrl?: string;
|
|
168
|
-
/**
|
|
177
|
+
/**
|
|
178
|
+
* Narration only: how the voice should sound, for speech models that take
|
|
179
|
+
* instructions ("Read calmly, like a teacher").
|
|
180
|
+
*/
|
|
181
|
+
instructions?: string;
|
|
182
|
+
/** The model id (`gpt-5.5`, or a speech model like `gpt-4o-mini-tts`). */
|
|
169
183
|
model: string;
|
|
170
184
|
/**
|
|
171
185
|
* The provider name the AI SDK reports, which a custom endpoint also reads
|
|
172
186
|
* its `providerOptions` under. Defaults to `openai`, or `openai-compatible`
|
|
173
|
-
* with a `baseUrl`.
|
|
187
|
+
* with a `baseUrl`. Assistant only.
|
|
174
188
|
*/
|
|
175
189
|
name?: string;
|
|
176
190
|
/**
|
|
177
191
|
* How much the model reasons before answering, sent as `reasoning_effort`;
|
|
178
192
|
* with a `baseUrl`, the endpoint has to accept that parameter. Omitted
|
|
179
|
-
* keeps the model's default.
|
|
193
|
+
* keeps the model's default. Assistant only.
|
|
180
194
|
*/
|
|
181
195
|
reasoning?: AssistantReasoning;
|
|
196
|
+
/** Narration only: the speech model's voice. Defaults to `alloy`. */
|
|
197
|
+
voice?: string;
|
|
182
198
|
}
|
|
183
199
|
|
|
184
200
|
const openaiOptionsSchema = z.strictObject({
|
|
@@ -203,7 +219,9 @@ export const openaiAdapterSchema = adapterDescriptorSchema(
|
|
|
203
219
|
* Route the assistant to OpenAI, or to any OpenAI-compatible endpoint with
|
|
204
220
|
* `baseUrl`. Reads `OPENAI_API_KEY` and needs `@ai-sdk/openai` installed; a
|
|
205
221
|
* custom endpoint needs `@ai-sdk/openai-compatible` instead, since most
|
|
206
|
-
* don't serve OpenAI's Responses API.
|
|
222
|
+
* don't serve OpenAI's Responses API. Narration generates audio with it
|
|
223
|
+
* through `@ai-sdk/openai` either way, and with a `baseUrl` sends only the key
|
|
224
|
+
* `apiKeyEnv` names.
|
|
207
225
|
*/
|
|
208
226
|
export const openai = (
|
|
209
227
|
options: AssistantOpenAIOptions
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
import type { AdapterDescriptor } from "../core/adapter.ts";
|
|
4
|
+
import { adapterDescriptorSchema } from "../core/adapter.ts";
|
|
5
|
+
import type { HeadScript } from "./head.ts";
|
|
6
|
+
|
|
7
|
+
/** The browser tracker in Clics' installation guide. */
|
|
8
|
+
export const CLICS_SCRIPT_SRC = "https://clics.dev/tracker.js";
|
|
9
|
+
|
|
10
|
+
/** Public project ID and optional tracker flags. */
|
|
11
|
+
export interface ClicsOptions {
|
|
12
|
+
/** Project ID from Configure → Tracking in the Clics dashboard. */
|
|
13
|
+
projectId: string;
|
|
14
|
+
/** Include localhost and 127.0.0.1 in a production build's reports. */
|
|
15
|
+
allowLocalhost?: boolean;
|
|
16
|
+
/** Turn off automatic outbound-link events. */
|
|
17
|
+
disableOutboundLinks?: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const clicsOptionsSchema = z.strictObject({
|
|
21
|
+
allowLocalhost: z.boolean().optional(),
|
|
22
|
+
disableOutboundLinks: z.boolean().optional(),
|
|
23
|
+
projectId: z.string().min(1),
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
export type ClicsAdapter = AdapterDescriptor<"clics", ClicsOptions>;
|
|
27
|
+
|
|
28
|
+
export const clicsAdapterSchema = adapterDescriptorSchema(
|
|
29
|
+
"clics",
|
|
30
|
+
clicsOptionsSchema
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
/** Clics website analytics. The project ID is public in the tracker tag. */
|
|
34
|
+
export const clics = (options: ClicsOptions): ClicsAdapter => ({
|
|
35
|
+
kind: "clics",
|
|
36
|
+
options,
|
|
37
|
+
requiredSecrets: [],
|
|
38
|
+
runtimeDeps: [],
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* The documented deferred tracker tag. Clics reads these attributes from
|
|
43
|
+
* `document.currentScript` and tracks history changes without a Blume hook.
|
|
44
|
+
*/
|
|
45
|
+
export const clicsHead = (options: ClicsOptions): HeadScript[] => {
|
|
46
|
+
const attributes: HeadScript["attributes"] = {
|
|
47
|
+
"data-project-id": options.projectId,
|
|
48
|
+
defer: true,
|
|
49
|
+
src: CLICS_SCRIPT_SRC,
|
|
50
|
+
};
|
|
51
|
+
if (options.allowLocalhost) {
|
|
52
|
+
attributes["data-allow-localhost"] = true;
|
|
53
|
+
}
|
|
54
|
+
if (options.disableOutboundLinks) {
|
|
55
|
+
attributes["data-disable-outbound-links"] = true;
|
|
56
|
+
}
|
|
57
|
+
return [{ attributes, content: null }];
|
|
58
|
+
};
|
package/src/analytics/head.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { adobeHead } from "./adobe.ts";
|
|
|
2
2
|
import { amplitudeHead } from "./amplitude.ts";
|
|
3
3
|
import { clarityHead } from "./clarity.ts";
|
|
4
4
|
import { clearbitHead } from "./clearbit.ts";
|
|
5
|
+
import { clicsHead } from "./clics.ts";
|
|
5
6
|
import { cloudflareHead } from "./cloudflare.ts";
|
|
6
7
|
import { databuddyHead } from "./databuddy.ts";
|
|
7
8
|
import { fathomHead } from "./fathom.ts";
|
|
@@ -47,6 +48,7 @@ export type HeadNode =
|
|
|
47
48
|
* the one place that branches on `kind`. A provider whose install snippet is
|
|
48
49
|
* two tags (an SDK plus its init call) emits two.
|
|
49
50
|
*/
|
|
51
|
+
// oxlint-disable-next-line complexity -- each adapter has one exhaustive, type-narrowed dispatch case
|
|
50
52
|
const adapterScripts = (
|
|
51
53
|
adapter: Exclude<AnalyticsAdapter, { kind: "vercel" }>
|
|
52
54
|
): HeadScript[] => {
|
|
@@ -63,6 +65,9 @@ const adapterScripts = (
|
|
|
63
65
|
case "clearbit": {
|
|
64
66
|
return clearbitHead(adapter.options);
|
|
65
67
|
}
|
|
68
|
+
case "clics": {
|
|
69
|
+
return clicsHead(adapter.options);
|
|
70
|
+
}
|
|
66
71
|
case "cloudflare": {
|
|
67
72
|
return cloudflareHead(adapter.options);
|
|
68
73
|
}
|
package/src/analytics/index.ts
CHANGED
|
@@ -28,6 +28,8 @@ export { clarity } from "./clarity.ts";
|
|
|
28
28
|
export type { ClarityAdapter, ClarityOptions } from "./clarity.ts";
|
|
29
29
|
export { clearbit } from "./clearbit.ts";
|
|
30
30
|
export type { ClearbitAdapter, ClearbitOptions } from "./clearbit.ts";
|
|
31
|
+
export { clics } from "./clics.ts";
|
|
32
|
+
export type { ClicsAdapter, ClicsOptions } from "./clics.ts";
|
|
31
33
|
export { cloudflare } from "./cloudflare.ts";
|
|
32
34
|
export type { CloudflareAdapter, CloudflareOptions } from "./cloudflare.ts";
|
|
33
35
|
export { databuddy } from "./databuddy.ts";
|
package/src/analytics/schema.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { adobeAdapterSchema } from "./adobe.ts";
|
|
|
4
4
|
import { amplitudeAdapterSchema } from "./amplitude.ts";
|
|
5
5
|
import { clarityAdapterSchema } from "./clarity.ts";
|
|
6
6
|
import { clearbitAdapterSchema } from "./clearbit.ts";
|
|
7
|
+
import { clicsAdapterSchema } from "./clics.ts";
|
|
7
8
|
import { cloudflareAdapterSchema } from "./cloudflare.ts";
|
|
8
9
|
import { databuddyAdapterSchema } from "./databuddy.ts";
|
|
9
10
|
import { fathomAdapterSchema } from "./fathom.ts";
|
|
@@ -28,6 +29,7 @@ export const analyticsAdapterSchema = z.discriminatedUnion("kind", [
|
|
|
28
29
|
amplitudeAdapterSchema,
|
|
29
30
|
clarityAdapterSchema,
|
|
30
31
|
clearbitAdapterSchema,
|
|
32
|
+
clicsAdapterSchema,
|
|
31
33
|
cloudflareAdapterSchema,
|
|
32
34
|
databuddyAdapterSchema,
|
|
33
35
|
fathomAdapterSchema,
|
package/src/astro/generate.ts
CHANGED
|
@@ -1818,21 +1818,26 @@ export const diagnosticWarning = (diagnostic: Diagnostic): string =>
|
|
|
1818
1818
|
|
|
1819
1819
|
/**
|
|
1820
1820
|
* Missing-dependency preflight: the search provider's SDK, content source
|
|
1821
|
-
* SDKs, the assistant backend's provider
|
|
1822
|
-
* and — since React ships with Blume while Vue/Svelte
|
|
1823
|
-
* framework's Astro integration. A build fails here,
|
|
1824
|
-
* written, with the install command for the project's
|
|
1825
|
-
* otherwise Vite dies later on an opaque unresolved import
|
|
1826
|
-
* runtime. Dev warns and keeps serving: only the pages and
|
|
1827
|
-
* the package break, and installing it is picked up on
|
|
1821
|
+
* SDKs, the assistant backend's and narration provider's SDKs, the deployment
|
|
1822
|
+
* adapter's package, and — since React ships with Blume while Vue/Svelte
|
|
1823
|
+
* don't — any island framework's Astro integration. A build fails here,
|
|
1824
|
+
* before anything is written, with the install command for the project's
|
|
1825
|
+
* package manager — otherwise Vite dies later on an opaque unresolved import
|
|
1826
|
+
* from the hidden runtime. Dev warns and keeps serving: only the pages and
|
|
1827
|
+
* routes that import the package break, and installing it is picked up on
|
|
1828
|
+
* the next restart. Dev leaves narration's SDK out, since only a build
|
|
1829
|
+
* generates the clips.
|
|
1828
1830
|
*/
|
|
1829
1831
|
const dependencyPreflight = async (
|
|
1830
1832
|
project: BlumeProject,
|
|
1831
1833
|
frameworks: Set<string>
|
|
1832
1834
|
): Promise<string[]> => {
|
|
1833
1835
|
const build = project.mode === "build";
|
|
1836
|
+
const { config } = project;
|
|
1834
1837
|
const diagnostic = await missingDependencyDiagnostic(
|
|
1835
|
-
|
|
1838
|
+
build
|
|
1839
|
+
? config
|
|
1840
|
+
: { ...config, narration: { ...config.narration, provider: null } },
|
|
1836
1841
|
project.context.root,
|
|
1837
1842
|
build ? "error" : "warning",
|
|
1838
1843
|
frameworks
|
package/src/astro/integration.ts
CHANGED
|
@@ -7,6 +7,7 @@ import { join, relative, resolve } from "pathe";
|
|
|
7
7
|
import { loadEnvFiles } from "../cli/env.ts";
|
|
8
8
|
import type { CustomPageRoute } from "../core/custom-pages.ts";
|
|
9
9
|
import { enrichDiagnostic } from "../core/diagnostics.ts";
|
|
10
|
+
import { POWERED_BY_HEADERS } from "../core/powered-by.ts";
|
|
10
11
|
import { scanProject } from "../core/project-graph.ts";
|
|
11
12
|
import type { BlumeProject } from "../core/project-graph.ts";
|
|
12
13
|
import {
|
|
@@ -146,23 +147,33 @@ export const publishBuildProject = (project: BlumeProject | null): void => {
|
|
|
146
147
|
* the Astro root recorded on `astro:config:done`. Scan diagnostics surface as
|
|
147
148
|
* warnings — there is no `--strict` to honor here, and the build itself has
|
|
148
149
|
* already succeeded. `null` when nothing asked for a scan.
|
|
150
|
+
*
|
|
151
|
+
* The app's runtime settings were baked in at eject, `poweredBy` among them
|
|
152
|
+
* (the integration option that registers the runtime middleware), so the
|
|
153
|
+
* artifacts follow that option rather than the config file: one switch puts
|
|
154
|
+
* `X-Powered-By` on the server-rendered responses and in `_headers` alike.
|
|
149
155
|
*/
|
|
150
156
|
const scanForArtifacts = async (
|
|
151
157
|
astroRoot: URL | null,
|
|
152
|
-
|
|
158
|
+
options: Pick<BlumeIntegrationOptions, "buildArtifactsRoot" | "poweredBy">,
|
|
153
159
|
logger: ArtifactLogger
|
|
154
160
|
): Promise<BlumeProject | null> => {
|
|
155
|
-
if (!(astroRoot &&
|
|
161
|
+
if (!(astroRoot && options.buildArtifactsRoot)) {
|
|
156
162
|
return null;
|
|
157
163
|
}
|
|
158
|
-
const root = resolve(fileURLToPath(astroRoot),
|
|
164
|
+
const root = resolve(fileURLToPath(astroRoot), options.buildArtifactsRoot);
|
|
159
165
|
// Remote sources read their tokens from the environment during the scan.
|
|
160
166
|
loadEnvFiles(root);
|
|
161
167
|
const project = await scanProject(root, { mode: "build" });
|
|
162
168
|
for (const diagnostic of project.diagnostics) {
|
|
163
169
|
logger.warn(`[${diagnostic.code}] ${diagnostic.message}`);
|
|
164
170
|
}
|
|
165
|
-
return
|
|
171
|
+
return options.poweredBy === undefined
|
|
172
|
+
? project
|
|
173
|
+
: {
|
|
174
|
+
...project,
|
|
175
|
+
config: { ...project.config, poweredBy: options.poweredBy },
|
|
176
|
+
};
|
|
166
177
|
};
|
|
167
178
|
|
|
168
179
|
/**
|
|
@@ -317,6 +328,12 @@ export type BlumePageRoute = CustomPageRoute;
|
|
|
317
328
|
|
|
318
329
|
export interface BlumeIntegrationOptions {
|
|
319
330
|
pages: BlumePageRoute[];
|
|
331
|
+
/**
|
|
332
|
+
* Whether responses name Blume in `X-Powered-By` (see
|
|
333
|
+
* `core/powered-by.ts`): the dev server's, and the server-rendered ones of
|
|
334
|
+
* a build, through the runtime middleware. Defaults to `true`.
|
|
335
|
+
*/
|
|
336
|
+
poweredBy?: boolean;
|
|
320
337
|
/**
|
|
321
338
|
* Page routes that have a raw-Markdown variant (the content manifest). The
|
|
322
339
|
* hidden runtime leaves this out: the CLI publishes the live set through
|
|
@@ -429,6 +446,24 @@ const answerRedirects =
|
|
|
429
446
|
res.end();
|
|
430
447
|
};
|
|
431
448
|
|
|
449
|
+
/**
|
|
450
|
+
* Name Blume in `X-Powered-By` on every dev response, as the deployed site
|
|
451
|
+
* does. It goes on the Node response before anything answers, so the
|
|
452
|
+
* redirects, the `public/` files Vite serves, and the rendered pages all
|
|
453
|
+
* carry it, and a page or project middleware that sets its own value
|
|
454
|
+
* replaces it.
|
|
455
|
+
*/
|
|
456
|
+
const identifyBlume = (
|
|
457
|
+
_req: IncomingMessage,
|
|
458
|
+
res: ServerResponse,
|
|
459
|
+
next: () => void
|
|
460
|
+
): void => {
|
|
461
|
+
for (const [name, value] of Object.entries(POWERED_BY_HEADERS)) {
|
|
462
|
+
res.setHeader(name, value);
|
|
463
|
+
}
|
|
464
|
+
next();
|
|
465
|
+
};
|
|
466
|
+
|
|
432
467
|
/** The `.d.ts` the integration injects for the `blume:*` virtual modules. */
|
|
433
468
|
const MODULE_TYPES_FILE = "modules.d.ts";
|
|
434
469
|
|
|
@@ -462,11 +497,7 @@ export const blumeIntegration = (
|
|
|
462
497
|
"astro:build:done": async ({ dir, logger }) => {
|
|
463
498
|
const project =
|
|
464
499
|
registry().buildProject ??
|
|
465
|
-
(await scanForArtifacts(
|
|
466
|
-
astroRoot,
|
|
467
|
-
options.buildArtifactsRoot,
|
|
468
|
-
logger
|
|
469
|
-
));
|
|
500
|
+
(await scanForArtifacts(astroRoot, options, logger));
|
|
470
501
|
if (!project) {
|
|
471
502
|
return;
|
|
472
503
|
}
|
|
@@ -505,6 +536,15 @@ export const blumeIntegration = (
|
|
|
505
536
|
entrypoint: "blume/components/icon-sprite-middleware.ts",
|
|
506
537
|
order: "post",
|
|
507
538
|
});
|
|
539
|
+
// Names Blume on the responses Astro renders (see
|
|
540
|
+
// components/powered-by-middleware.ts). Innermost too, so a header
|
|
541
|
+
// the project's own middleware sets wins.
|
|
542
|
+
if (options.poweredBy !== false) {
|
|
543
|
+
addMiddleware({
|
|
544
|
+
entrypoint: "blume/components/powered-by-middleware.ts",
|
|
545
|
+
order: "post",
|
|
546
|
+
});
|
|
547
|
+
}
|
|
508
548
|
for (const page of options.pages) {
|
|
509
549
|
injectRoute({
|
|
510
550
|
entrypoint: page.entrypoint,
|
|
@@ -540,6 +580,13 @@ export const blumeIntegration = (
|
|
|
540
580
|
route: "",
|
|
541
581
|
});
|
|
542
582
|
}
|
|
583
|
+
// Ahead of the redirects, so their answers carry it too.
|
|
584
|
+
if (options.poweredBy !== false) {
|
|
585
|
+
server.middlewares.stack.unshift({
|
|
586
|
+
handle: identifyBlume,
|
|
587
|
+
route: "",
|
|
588
|
+
});
|
|
589
|
+
}
|
|
543
590
|
},
|
|
544
591
|
},
|
|
545
592
|
name: "blume",
|