blume 0.1.5 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (85) hide show
  1. package/dist/cli/index.js +2123 -555
  2. package/dist/cli/index.js.map +39 -25
  3. package/dist/types/core/data.d.ts +16 -0
  4. package/dist/types/core/define-components.d.ts +9 -2
  5. package/dist/types/core/diagnostics.d.ts +5 -0
  6. package/dist/types/core/schema.d.ts +136 -508
  7. package/dist/types/core/types.d.ts +2 -2
  8. package/docs/02-deployment.mdx +21 -2
  9. package/docs/advanced/changelog.mdx +28 -1
  10. package/docs/advanced/custom-pages.mdx +63 -2
  11. package/docs/configuration/ai.mdx +20 -3
  12. package/docs/configuration/customization.mdx +103 -5
  13. package/docs/configuration/index.mdx +25 -11
  14. package/docs/configuration/search.mdx +13 -1
  15. package/docs/configuration/seo.mdx +5 -0
  16. package/docs/configuration/theming.mdx +51 -0
  17. package/docs/content/components.mdx +18 -0
  18. package/docs/content/islands.mdx +73 -0
  19. package/docs/content/navigation.mdx +25 -0
  20. package/docs/content/sources.mdx +43 -0
  21. package/docs/content/syntax.mdx +1 -1
  22. package/docs/index.mdx +3 -12
  23. package/docs/reference/cli.mdx +49 -1
  24. package/docs/reference/frontmatter.mdx +9 -1
  25. package/package.json +3 -1
  26. package/src/ai/ask-context.ts +131 -0
  27. package/src/ai/ask-data.ts +25 -0
  28. package/src/astro/component-slots.ts +165 -0
  29. package/src/astro/generate.ts +162 -26
  30. package/src/astro/integration.ts +59 -0
  31. package/src/astro/pages.ts +5 -12
  32. package/src/astro/templates.ts +102 -45
  33. package/src/blume-modules.d.ts +25 -0
  34. package/src/cli/commands/build.ts +186 -1
  35. package/src/cli/commands/check.ts +62 -0
  36. package/src/cli/commands/dev.ts +21 -1
  37. package/src/cli/commands/doctor.ts +23 -6
  38. package/src/cli/commands/init.ts +163 -15
  39. package/src/cli/commands/validate.ts +16 -2
  40. package/src/cli/env.ts +84 -0
  41. package/src/cli/index.ts +20 -0
  42. package/src/cli/internal-error.ts +63 -0
  43. package/src/cli/log.ts +30 -1
  44. package/src/cli/prepare.ts +22 -3
  45. package/src/cli/required-secrets.ts +44 -0
  46. package/src/components/BlumePage.astro +107 -0
  47. package/src/components/content/CodeBlock.astro +7 -2
  48. package/src/components/index.ts +3 -3
  49. package/src/components/islands/ask-ai.tsx +15 -1
  50. package/src/components/islands/hooks.ts +188 -0
  51. package/src/components/layout/Empty.astro +6 -0
  52. package/src/components/layout/Header.astro +24 -39
  53. package/src/components/layout/Logo.astro +50 -0
  54. package/src/components/layout/NavSelector.astro +75 -0
  55. package/src/components/layout/PageLayout.astro +38 -2
  56. package/src/components/layout/RootLayout.astro +70 -4
  57. package/src/components/layout/hydration-hint.ts +30 -0
  58. package/src/components/layout/overrides.ts +6 -4
  59. package/src/components/props.ts +68 -0
  60. package/src/core/builtin-tags.ts +39 -0
  61. package/src/core/component-diagnostics.ts +44 -0
  62. package/src/core/component-overrides.ts +478 -0
  63. package/src/core/config.ts +8 -0
  64. package/src/core/data.ts +14 -0
  65. package/src/core/define-components.ts +9 -2
  66. package/src/core/diagnostics.ts +90 -1
  67. package/src/core/graph.ts +7 -0
  68. package/src/core/nav-diagnostics.ts +205 -0
  69. package/src/core/project-graph.ts +40 -1
  70. package/src/core/schema.ts +54 -96
  71. package/src/core/sources/github-releases.ts +200 -0
  72. package/src/core/sources/normalize.ts +51 -0
  73. package/src/core/sources/resolve.ts +16 -0
  74. package/src/core/types.ts +2 -2
  75. package/src/deploy/redirects.ts +43 -0
  76. package/src/markdown/index.ts +24 -0
  77. package/src/migrate/mintlify/config.ts +1 -176
  78. package/src/migrate/starlight/config.ts +0 -4
  79. package/src/og/card.ts +163 -38
  80. package/src/registry/eject.ts +39 -9
  81. package/src/registry/registry.ts +166 -0
  82. package/src/runtime/index.ts +61 -0
  83. package/src/vite-env.d.ts +14 -0
  84. package/docs/changelog/v0-1-0.mdx +0 -12
  85. package/docs/changelog/v0-2-0.mdx +0 -16
@@ -108,6 +108,8 @@ export interface PageRecord {
108
108
  format: "md" | "mdx";
109
109
  /** Internal/asset links discovered in the page (for validation). */
110
110
  links: PageLink[];
111
+ /** Capitalized JSX component tags used in the body (`.mdx` only). */
112
+ componentsUsed?: string[];
111
113
  /** Resolved "last updated" ISO date, when the feature is enabled. */
112
114
  lastModified?: string;
113
115
  }
@@ -168,8 +170,6 @@ export interface NavSidebarVariant {
168
170
  export interface NavChromeVariant {
169
171
  path: string;
170
172
  banner?: ResolvedConfig["banner"];
171
- footer?: ResolvedConfig["footer"];
172
- navbar?: ResolvedConfig["navbar"];
173
173
  }
174
174
  /** The complete navigation model derived from the content graph. */
175
175
  export interface Navigation {
@@ -118,8 +118,27 @@ Map old URLs to new ones in `blume.config.ts`:
118
118
  redirects: [{ from: "/old", to: "/new", status: 301 }];
119
119
  ```
120
120
 
121
- `status` accepts `301`, `302`, `307`, or `308` (default `301`). Static builds
122
- emit redirect pages; server builds handle redirects at request time.
121
+ `status` accepts `301`, `302`, `307`, or `308` (default `301`). Server builds
122
+ handle redirects at request time. Static builds emit redirect pages **and**
123
+ platform files so your host issues a real HTTP redirect: `_redirects` (Netlify,
124
+ Cloudflare Pages), `vercel.json` (Vercel), and `blume-redirects.json` — a
125
+ structured manifest for anything else (nginx/Apache rules, an edge worker). A
126
+ `_redirects` or `vercel.json` you ship in `public/` is left untouched.
127
+
128
+ ## Environment variables
129
+
130
+ When a feature needs a runtime secret, Blume warns at `blume dev`/`build` if it's
131
+ missing — so the problem surfaces early instead of at the first request:
132
+
133
+ | Feature | Variable |
134
+ | --------------------------------------- | ------------------------------------- |
135
+ | Ask AI (AI Gateway) | `AI_GATEWAY_API_KEY` (or Vercel OIDC) |
136
+ | Ask AI (OpenRouter / OpenAI-compatible) | the provider's `apiKeyEnv` |
137
+ | Mixedbread search | `MIXEDBREAD_API_KEY` |
138
+
139
+ Set them in `.env.local` for local dev and in your host's environment for
140
+ production. Build-time secrets for search-index sync (Algolia, Orama Cloud,
141
+ Typesense) are warned about separately during the sync step.
123
142
 
124
143
  ## Build summary
125
144
 
@@ -5,7 +5,8 @@ description: Author release notes as content, and Blume builds a timeline page a
5
5
 
6
6
  Blume ships a changelog out of the box. Write each release as a normal content
7
7
  file, mark it `type: changelog`, and Blume collects every entry into a generated
8
- timeline page and an RSS feed — no layout to build, no list to maintain.
8
+ timeline page and an RSS feed — no layout to build, no list to maintain. Or skip
9
+ the files entirely and [source your changelog from GitHub Releases](#from-github-releases).
9
10
 
10
11
  ## Write an entry
11
12
 
@@ -72,6 +73,32 @@ replace it with your own design, add a [custom page](/docs/advanced/custom-pages
72
73
  `pages/changelog.astro` — it takes over and Blume stops generating the default
73
74
  timeline.
74
75
 
76
+ ## From GitHub Releases
77
+
78
+ Rather than authoring entries by hand, point the built-in
79
+ [`github-releases` source](/docs/content/sources#github-releases) at a repo and
80
+ every release becomes a `type: changelog` entry — the same timeline and feed,
81
+ fed straight from the releases you already publish:
82
+
83
+ ```ts blume.config.ts
84
+ content: {
85
+ sources: [
86
+ { type: "filesystem", root: "content" },
87
+ {
88
+ type: "github-releases",
89
+ prefix: "changelog",
90
+ owner: "acme",
91
+ repo: "sdk",
92
+ },
93
+ ],
94
+ }
95
+ ```
96
+
97
+ The release name becomes the title, its tag becomes `changelog.version`, and its
98
+ published date sorts the timeline. A private repo authenticates with the
99
+ `GITHUB_TOKEN` environment variable. See
100
+ [Content sources](/docs/content/sources#github-releases) for every option.
101
+
75
102
  ## The RSS feed
76
103
 
77
104
  Blume also builds a changelog feed at **`/changelog/rss.xml`**, sorted
@@ -167,6 +167,51 @@ const posts = (await getCollection("docs"))
167
167
  </ul>
168
168
  ```
169
169
 
170
+ ## Runtime helpers
171
+
172
+ `blume/runtime` bundles the common data patterns so you don't reach into
173
+ `blume:data` internals.
174
+
175
+ **`getBlumeCollection(data, query?)`** selects content routes — filtered by
176
+ collection, locale, or path prefix, with drafts and hidden pages excluded and the
177
+ result sorted by path — which is exactly what a custom index needs:
178
+
179
+ ```astro pages/blog/index.astro lineNumbers
180
+ ---
181
+ import data from "blume:data";
182
+ import { getBlumeCollection } from "blume/runtime";
183
+
184
+ const posts = getBlumeCollection(data, { prefix: "/blog" });
185
+ ---
186
+
187
+ <ul>
188
+ {posts.map((post) => (
189
+ <li><a href={post.path}>{post.title}</a></li>
190
+ ))}
191
+ </ul>
192
+ ```
193
+
194
+ **`<BlumePage>`** renders a content entry's body inside a custom page, with
195
+ Blume's built-in MDX components (callouts, cards, steps…) already wired in — for
196
+ featuring a doc on a landing page or building a bespoke index that shows real
197
+ content:
198
+
199
+ ```astro pages/index.astro lineNumbers
200
+ ---
201
+ import BlumePage from "blume/components/BlumePage.astro";
202
+ import data from "blume:data";
203
+ import { getBlumeCollection } from "blume/runtime";
204
+
205
+ const [intro] = getBlumeCollection(data, { prefix: "/docs" });
206
+ ---
207
+
208
+ {intro && <BlumePage id={intro.entryId} />}
209
+ ```
210
+
211
+ Pass `components` to add your own overrides or islands (which live in the
212
+ generated runtime and aren't imported by default), and `collection` to read from
213
+ a collection other than `"docs"`.
214
+
170
215
  ## Using the site layout
171
216
 
172
217
  `RootLayout` gives a custom page the full docs chrome — header, sidebar, search,
@@ -211,7 +256,24 @@ generated `og:image` automatically: Blume renders an Open Graph card for every
211
256
  static custom page — the home included, the most-shared URL — served at
212
257
  `/og/<route>.png` (`/og/index.png` for `/`). The home card uses the site title
213
258
  with the description as its eyebrow; a deeper page is titled from its last path
214
- segment. Set `ogImage` or `canonical` explicitly to override.
259
+ segment. Set `ogImage` or `canonical` explicitly to override either. `ogImage`
260
+ takes a root-relative path — a file in `public/`, resolved against
261
+ [`deployment.site`](/docs/deployment) to the absolute URL crawlers need — or an
262
+ external URL, which passes through untouched:
263
+
264
+ ```astro pages/index.astro lineNumbers
265
+ <PageLayout
266
+ siteUrl={config.site}
267
+ ogEnabled={config.og.enabled}
268
+ ogImage="/opengraph-image.png"
269
+ page={{ title: config.title }}
270
+ >
271
+ <!-- page content -->
272
+ </PageLayout>
273
+ ```
274
+
275
+ Only this page changes — every other route keeps its generated card — so it's how
276
+ you give the home page alone a bespoke share image.
215
277
 
216
278
  `page.title` is used verbatim as the document title (no `- siteTitle` suffix),
217
279
  since marketing pages usually set their own. To give a custom page the full docs
@@ -233,7 +295,6 @@ import data from "blume:data";
233
295
  headings={[]}
234
296
  themeMode={data.config.theme.mode}
235
297
  searchEnabled={data.config.search.enabled}
236
- searchProvider={data.config.search.provider}
237
298
  indexable={true}
238
299
  >
239
300
  <h1>Pricing</h1>
@@ -84,6 +84,24 @@ ai: {
84
84
  }
85
85
  ```
86
86
 
87
+ ### Grounding
88
+
89
+ Ask AI is **grounded in your docs**. For each question it retrieves the most
90
+ relevant pages — using the same lexical [Orama](/docs/configuration/search) index
91
+ that powers on-page search — and injects them into the model's system prompt, so
92
+ answers come from your content instead of the model's own knowledge. The
93
+ assistant is told to answer only from the retrieved pages, to say when something
94
+ isn't covered, and to cite the pages it drew from.
95
+
96
+ The page the reader is currently on is added to the context first and used to
97
+ scope retrieval to that page's language, so answers stay relevant to where they
98
+ are in the docs. Retrieval runs at request time from a snapshot baked into the
99
+ build, so it works regardless of your [search](/docs/configuration/search)
100
+ provider — even when search is set to `none` — and needs no configuration.
101
+
102
+ Grounding is on for every backend except **[Inkeep](#backends)**, which runs its
103
+ own retrieval over the content you've indexed in its dashboard.
104
+
87
105
  ### Server output required
88
106
 
89
107
  Ask AI is a server route (`POST /api/ask`), so it can't run on a static build.
@@ -151,9 +169,8 @@ at a different env var or proxy.
151
169
 
152
170
  :::note
153
171
  **Inkeep** answers from the content you've indexed in the Inkeep dashboard — it
154
- runs its own retrieval — rather than from this site's pages. The other backends
155
- are plain model passthroughs: the endpoint streams the reader's messages straight
156
- to the model.
172
+ runs its own retrieval — so Blume leaves it ungrounded. Every other backend is
173
+ [grounded](#grounding) in this site's pages.
157
174
  :::
158
175
 
159
176
  Keys are read with `process.env`, which covers the Node, Vercel, and Netlify
@@ -25,6 +25,101 @@ export default defineComponents({
25
25
  Keys are the names you write in MDX (`<Callout>`, `<Pricing>`). Use the `.tsx`
26
26
  filename when you import React components.
27
27
 
28
+ ### Reference form
29
+
30
+ Every override — in `mdx`, `layout`, or `islands` — accepts three forms:
31
+
32
+ ```ts components.ts
33
+ import { defineComponents } from "blume";
34
+ import Callout from "./components/Callout.astro";
35
+
36
+ export default defineComponents({
37
+ mdx: {
38
+ Callout, // 1. an imported component
39
+ Note: "./components/Note.astro", // 2. a path string (resolved from the project root)
40
+ Chart: { component: "./components/Chart.tsx", client: "load" }, // 3. a descriptor
41
+ },
42
+ });
43
+ ```
44
+
45
+ The **descriptor** form adds a hydration mode so an interactive
46
+ React/Vue/Svelte component ships its JavaScript and comes alive on the client.
47
+ Without a `client` mode a framework component renders as static HTML — Blume
48
+ prints a build warning when it spots one, since that's usually a mistake.
49
+
50
+ | `client` | Hydrates |
51
+ | ----------- | ---------------------------------------------------------------- |
52
+ | `"load"` | Immediately on page load |
53
+ | `"idle"` | When the main thread is idle |
54
+ | `"visible"` | When scrolled into view |
55
+ | `"media"` | When a `media` query matches (add `media: "(min-width: 40rem)"`) |
56
+ | `"only"` | Client only, never server-rendered |
57
+
58
+ For interactive components you use across many pages, the [`islands`
59
+ group](/docs/content/islands#registering-islands-in-componentsts) is a shorthand
60
+ for the descriptor form with `client: "visible"`.
61
+
62
+ ### Typing an override
63
+
64
+ When you replace a built-in, import its prop type from `blume/components` so your
65
+ component matches the contract — the types are derived from the components
66
+ themselves, so they never drift:
67
+
68
+ ```tsx components/Callout.tsx
69
+ import type { CalloutProps } from "blume/components";
70
+
71
+ export default function Callout(props: CalloutProps) {
72
+ // …your own callout, same props as the built-in
73
+ }
74
+ ```
75
+
76
+ Prop types are exported for the content components (`CalloutProps`, `CardProps`,
77
+ `TabsProps`, `StepsProps`, `BadgeProps`, and more).
78
+
79
+ ## Layout slots
80
+
81
+ The `layout` map replaces a piece of Blume's chrome with your own component. Each
82
+ override receives the same props as the built-in it replaces, so you can wrap the
83
+ default or start from scratch.
84
+
85
+ ```ts components.ts
86
+ import { defineComponents } from "blume";
87
+ import Footer from "./components/Footer.astro";
88
+ import Logo from "./components/Logo.astro";
89
+
90
+ export default defineComponents({
91
+ layout: {
92
+ Logo, // brand mark + title in the header
93
+ Footer, // site-wide footer (no built-in — renders only when set)
94
+ },
95
+ });
96
+ ```
97
+
98
+ Wired slots:
99
+
100
+ | Slot | Replaces | Props |
101
+ | ----------------- | -------------------------------------------------------- | -------------------------------------------------------------- |
102
+ | `Layout` | The entire page shell (`RootLayout`) | Everything the built-in layout receives, plus the `layout` map |
103
+ | `Header` | The top navigation bar | `site`, `logo`, `navigation`, `route`, `searchEnabled`, … |
104
+ | `Logo` | The brand link (mark + title) in the header | `site`, `logo` |
105
+ | `Search` | The header search trigger + modal | `navigation`, `strings`, `locale`, `askEnabled` |
106
+ | `Sidebar` | The primary navigation tree | `items`, `currentRoute` |
107
+ | `MobileNav` | The nav inside the mobile drawer (defaults to `Sidebar`) | `items`, `currentRoute` |
108
+ | `Breadcrumbs` | The breadcrumb trail | `crumbs` |
109
+ | `TableOfContents` | The on-this-page outline | `headings`, `title`, `variant` |
110
+ | `Pagination` | The prev/next footer links | `prev`, `next`, `strings` |
111
+ | `PageHeader` | An injection point above the article (no built-in) | `page`, `headings`, `route` |
112
+ | `PageFooter` | An injection point below the article (no built-in) | `page`, `headings`, `route` |
113
+ | `Footer` | A site-wide footer after the content grid (no built-in) | `site`, `navigation`, `ui` |
114
+
115
+ `PageHeader`, `PageFooter`, and `Footer` have no built-in component — they render
116
+ nothing until you set them, which makes them handy injection points for a
117
+ promo banner, a "last updated" note, or a marketing footer.
118
+
119
+ Layout slots accept the same [three reference forms](#reference-form) as MDX
120
+ overrides, so a slot can be a path string or a hydrated descriptor
121
+ (`{ component, client }`) when you want an interactive header or footer.
122
+
28
123
  ## Interactive islands
29
124
 
30
125
  For interactive UI (React, Vue, or Svelte), drop a component into an `islands/`
@@ -66,16 +161,19 @@ available:
66
161
  blume add
67
162
  ```
68
163
 
69
- Install a widget, or any built-in layout slot you want to customize the
70
- header, sidebar, breadcrumbs, table of contents, or pagination:
164
+ Install a layout slot (header, sidebar, breadcrumbs, table of contents,
165
+ pagination, or feedback) or any content component (callout, card, tabs, steps,
166
+ accordion, and more):
71
167
 
72
168
  ```bash
169
+ blume add callout
73
170
  blume add pagination
74
171
  ```
75
172
 
76
- The copy imports the rest of the framework from `blume/*`, so it renders
77
- exactly like the built-in until you change it. `blume add` prints the
78
- `defineComponents` snippet to register it under the matching layout slot.
173
+ The copy imports the rest of the framework from `blume/*`, so it renders exactly
174
+ like the built-in until you change it. `blume add` prints the `defineComponents`
175
+ snippet to register it — content components under `mdx`, layout pieces under
176
+ `layout`.
79
177
 
80
178
  ## Eject
81
179
 
@@ -20,7 +20,8 @@ export default defineConfig({
20
20
 
21
21
  ## A complete example
22
22
 
23
- Every option Blume reads, with its default:
23
+ A broader example touching the most common options (see each feature's guide for
24
+ the rest):
24
25
 
25
26
  ```ts blume.config.ts lineNumbers
26
27
  import { defineConfig } from "blume";
@@ -238,7 +239,7 @@ the full reference.
238
239
 
239
240
  ```ts blume.config.ts lineNumbers
240
241
  seo: {
241
- og: { enabled: false },
242
+ og: { enabled: true },
242
243
  rss: { enabled: true, types: ["blog", "changelog"] },
243
244
  sitemap: true,
244
245
  robots: true,
@@ -246,18 +247,31 @@ seo: {
246
247
  }
247
248
  ```
248
249
 
249
- | Option | Default | Description |
250
- | ---------------- | ----------------------- | --------------------------------------------- |
251
- | `og.enabled` | `false` | Generate per-page Open Graph images. |
252
- | `rss.enabled` | `true` | Build feeds for blog and changelog content. |
253
- | `rss.types` | `["blog", "changelog"]` | Content types that each get a feed. |
254
- | `rss.limit` | `50` | Maximum items per feed. |
255
- | `sitemap` | `true` | Generate sitemap.xml (needs deployment.site). |
256
- | `robots` | `true` | Generate robots.txt with a Sitemap link. |
257
- | `structuredData` | `true` | Emit schema.org JSON-LD in each page's head. |
250
+ | Option | Default | Description |
251
+ | ---------------- | ----------------------- | ------------------------------------------------------- |
252
+ | `og.enabled` | auto | Per-page Open Graph images — on when a site URL is set. |
253
+ | `rss.enabled` | `true` | Build feeds for blog and changelog content. |
254
+ | `rss.types` | `["blog", "changelog"]` | Content types that each get a feed. |
255
+ | `rss.limit` | `50` | Maximum items per feed. |
256
+ | `sitemap` | `true` | Generate sitemap.xml (needs deployment.site). |
257
+ | `robots` | `true` | Generate robots.txt with a Sitemap link. |
258
+ | `structuredData` | `true` | Emit schema.org JSON-LD in each page's head. |
258
259
 
259
260
  These work best with an absolute [`deployment.site`](/docs/deployment) for full URLs.
260
261
 
262
+ ## Table of contents
263
+
264
+ The on-this-page outline is on by default and lists `H2`–`H3` headings. Turn it
265
+ off, or change the heading range, with `toc`:
266
+
267
+ ```ts blume.config.ts
268
+ export default defineConfig({
269
+ toc: false, // hide it everywhere
270
+ // …or narrow the range:
271
+ toc: { minHeadingLevel: 2, maxHeadingLevel: 4 },
272
+ });
273
+ ```
274
+
261
275
  ## Feature options
262
276
 
263
277
  Each of these has its own guide. The config field is the entry point:
@@ -17,7 +17,8 @@ so picking one backend never pulls in the others.
17
17
  ## Using search
18
18
 
19
19
  Open search with <Badge variant="accent">⌘K</Badge> (or `Ctrl K`), or press `/`
20
- when you're not typing in a field. `Esc` closes it.
20
+ when you're not typing in a field. `Esc` closes it, and `⌘J` (or `Ctrl J`)
21
+ toggles the result preview pane.
21
22
 
22
23
  Queries match page **titles**, **descriptions**, and **body text**, with title
23
24
  matches ranked highest and descriptions above body.
@@ -29,6 +30,17 @@ to plain text — code blocks, images, and markup are stripped, so results stay
29
30
  relevant. The index is built from your source files, so it's identical in dev and
30
31
  production.
31
32
 
33
+ ## Tags
34
+
35
+ Add `search.tags` to a page's frontmatter to group it under a filter in the
36
+ search dialog — readers can narrow results to a tag with a click. Tags also
37
+ become a facet on the hosted providers.
38
+
39
+ ```yaml
40
+ search:
41
+ tags: [api, reference]
42
+ ```
43
+
32
44
  ## Providers
33
45
 
34
46
  The client-side providers are keyless and need no extra config. The hosted ones
@@ -114,6 +114,11 @@ OG rendering uses hex internally, so an `oklch` custom accent falls back to the
114
114
  default. Use a named accent (`blue`, `teal`, …) or a hex value for custom cards.
115
115
  :::
116
116
 
117
+ `seo.image` is frontmatter, so it only covers Markdown and MDX content. To give a
118
+ custom [`.astro` page](/docs/advanced/custom-pages) its own social image — a
119
+ marketing home or landing page, and the way to give the home page alone a bespoke
120
+ share image — pass the `ogImage` prop to `PageLayout`.
121
+
117
122
  ## RSS feeds
118
123
 
119
124
  Blume builds an RSS feed for each content type in `rss.types` — `blog` and
@@ -98,6 +98,55 @@ Each value is a Google Fonts slug from the curated set below:
98
98
  Need a font that isn't listed, or want to drop back to the system stack? Override
99
99
  the `--blume-font-*` tokens directly in [`theme.css`](#theme-css).
100
100
 
101
+ ### Dark-mode colors
102
+
103
+ By default light and dark share one `accent`. Set `accentDark` for a different
104
+ accent in dark mode, and `background` / `backgroundDark` to override the page
105
+ background per mode:
106
+
107
+ ```ts blume.config.ts lineNumbers
108
+ theme: {
109
+ accent: "blue",
110
+ accentDark: "teal", // a different accent in dark mode
111
+ background: "#ffffff", // light-mode page background
112
+ backgroundDark: "#0a0a0a", // dark-mode page background
113
+ }
114
+ ```
115
+
116
+ Each takes a named preset or any CSS color, exactly like `accent`.
117
+
118
+ ### Action color
119
+
120
+ `action` is a secondary accent for primary calls to action and the `action`
121
+ Tailwind utilities (`bg-action`, `text-action`). It defaults to your `accent`:
122
+
123
+ ```ts blume.config.ts
124
+ theme: {
125
+ action: "#ff0066",
126
+ }
127
+ ```
128
+
129
+ ### Background decoration
130
+
131
+ Add a subtle, theme-aware pattern behind your content with `backgroundDecoration`
132
+ — `gradient`, `grid`, or `windows`:
133
+
134
+ ```ts blume.config.ts
135
+ theme: {
136
+ backgroundDecoration: "grid",
137
+ }
138
+ ```
139
+
140
+ For a full background image, set `backgroundImage` (and `backgroundImageDark` for
141
+ a dark variant) to a URL or a path under `public/`:
142
+
143
+ ```ts blume.config.ts lineNumbers
144
+ theme: {
145
+ backgroundImage: "/bg-light.svg",
146
+ backgroundImageDark: "/bg-dark.svg",
147
+ }
148
+ ```
149
+
101
150
  ## theme.css
102
151
 
103
152
  Drop a `theme.css` in your project root to override any design token. It's the
@@ -128,6 +177,7 @@ dark mode.
128
177
  | `--blume-border` | Borders and dividers |
129
178
  | `--blume-accent` | Accent color |
130
179
  | `--blume-accent-foreground` | Text and icons on an accent background |
180
+ | `--blume-action` | Secondary accent (defaults to accent) |
131
181
  | `--blume-code-background` | Code block surface |
132
182
  | `--blume-radius` | Corner radius |
133
183
  | `--blume-font-display` | Heading font |
@@ -159,6 +209,7 @@ a utility, so your components track the theme automatically:
159
209
  | `--blume-border` | `border-border` |
160
210
  | `--blume-accent` | `bg-accent`, `text-accent` |
161
211
  | `--blume-accent-foreground` | `text-accent-foreground` |
212
+ | `--blume-action` | `bg-action`, `text-action` |
162
213
  | `--blume-radius` | `rounded-blume` |
163
214
  | `--blume-font-display` | `font-display` |
164
215
  | `--blume-font-body` | `font-sans` |
@@ -133,6 +133,24 @@ A negative or breaking state, such as a deprecation.
133
133
  <Badge variant="danger">Deprecated</Badge>
134
134
  ```
135
135
 
136
+ ## Icon
137
+
138
+ Render an icon from Blume's built-in set by name — the same set the `icon` props
139
+ on cards, tiles, tabs, and sidebar entries draw from. Names are lowercase and
140
+ kebab-cased (`rocket`, `book-open`, `chevron-right`).
141
+
142
+ <Icon icon="rocket" size={20} />
143
+
144
+ ```astro
145
+ <Icon icon="rocket" size={20} />
146
+ ```
147
+
148
+ `icon` is the icon name; `size` sets the pixel size (default `16`) and `color`
149
+ tints it (any CSS color; defaults to `currentColor`). Pass a raw `<svg>` string,
150
+ an image URL, or a local image path in place of a name to render your own art,
151
+ and add a `label` to expose it to assistive tech — without one, the icon is
152
+ decorative.
153
+
136
154
  ## File tree
137
155
 
138
156
  Illustrate a project or folder layout. Wrap a normal Markdown list and Blume
@@ -36,6 +36,35 @@ pages (a styled callout, a pricing table), use an [MDX
36
36
  override](/docs/configuration/customization) instead — it ships no JavaScript.
37
37
  :::
38
38
 
39
+ ## Registering islands in `components.ts`
40
+
41
+ If you'd rather keep islands next to the rest of your components — or give them a
42
+ different name than the file — register them with `defineComponents`. The
43
+ `islands` group is exactly like the `islands/` folder: each entry is available in
44
+ every MDX page and hydrates (defaulting to `client: "visible"`).
45
+
46
+ ```ts components.ts
47
+ import { defineComponents } from "blume";
48
+ import Counter from "./widgets/Counter.tsx";
49
+
50
+ export default defineComponents({
51
+ islands: {
52
+ Counter, // <Counter /> in any MDX page, hydrated
53
+ },
54
+ });
55
+ ```
56
+
57
+ Reference the component by import or by a path string, and set a hydration mode
58
+ per island with the descriptor form:
59
+
60
+ ```ts components.ts
61
+ export default defineComponents({
62
+ islands: {
63
+ Chart: { component: "./widgets/Chart.tsx", client: "only" },
64
+ },
65
+ });
66
+ ```
67
+
39
68
  ## Hydration
40
69
 
41
70
  By default an island uses `client:visible`: it hydrates when the reader scrolls
@@ -92,3 +121,47 @@ and children (`<Counter>label</Counter>`) arrive as the default slot.
92
121
  Islands hydrate on the client, so anything you pass as a prop must be
93
122
  serializable — strings, numbers, plain objects, not functions.
94
123
  :::
124
+
125
+ ## Hooks
126
+
127
+ Islands hydrate on their own, so there's no React context to thread project data
128
+ through. Instead, `blume/hooks` reads a small snapshot the layout serializes into
129
+ the page — no props to drill:
130
+
131
+ ```tsx islands/PageInfo.tsx lineNumbers
132
+ import { useBlume, usePage } from "blume/hooks";
133
+
134
+ export default function PageInfo() {
135
+ const blume = useBlume();
136
+ const page = usePage();
137
+ if (!(blume && page)) {
138
+ return null;
139
+ }
140
+ return (
141
+ <p>
142
+ You're reading <strong>{page.title}</strong> on {blume.config.title}.
143
+ </p>
144
+ );
145
+ }
146
+ ```
147
+
148
+ | Hook | Returns |
149
+ | ------------- | --------------------------------------------------------------------- |
150
+ | `useBlume()` | `{ config, navigation }` for the site, or `null` before mount |
151
+ | `usePage()` | `{ route, title }` for the current page, or `null` before mount |
152
+ | `useSearch()` | `{ search, results, loading }` — query the configured search provider |
153
+ | `useAskAI()` | `{ ask, messages, loading, reset }` — stream from the Ask AI endpoint |
154
+
155
+ `useBlume()` and `usePage()` return `null` until the island mounts (so server and
156
+ client render the same first frame) — guard for it. The snapshot is emitted only
157
+ on pages that ship React, so a fully static site pays nothing.
158
+
159
+ On [custom pages](/docs/advanced/custom-pages) built with `PageLayout`, pass
160
+ `clientData` so islands there can read it:
161
+
162
+ ```astro
163
+ <PageLayout
164
+ clientData={{ config: data.config, navigation: data.navigation, page: { route: "/", title: "Home" } }}
165
+ {/* …other props… */}
166
+ />
167
+ ```
@@ -113,6 +113,31 @@ your loose top-level pages while the sectioned content stays behind its tab,
113
113
  mirroring Fumadocs' root folders. If a route has no pages of its own to show
114
114
  this way, the full tree is shown instead, so the sidebar is never left blank.
115
115
 
116
+ ## Selectors
117
+
118
+ For switching between whole partitions of a site — a product, a version, or any
119
+ grouped set of destinations — add a `selector`. Each renders as a dropdown in the
120
+ header, showing the option whose `path` matches the current route:
121
+
122
+ ```ts blume.config.ts lineNumbers
123
+ navigation: {
124
+ selectors: [
125
+ {
126
+ kind: "version",
127
+ label: "Version",
128
+ items: [
129
+ { label: "v2 (latest)", path: "/v2", icon: "rocket" },
130
+ { label: "v1", path: "/v1" },
131
+ ],
132
+ },
133
+ ],
134
+ }
135
+ ```
136
+
137
+ Each item takes a `label`, a `path`, and optional `icon`, `description`, and
138
+ `tag`. `kind` (`dropdown`, `product`, `version`, or `language`) is a hint for how
139
+ the selector is used; all render the same dropdown.
140
+
116
141
  ## Explicit sidebar
117
142
 
118
143
  For full control, define `navigation.sidebar` in config. When it's set, Blume