blume 0.5.4 → 0.6.1

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 (57) hide show
  1. package/dist/cli/index.js +759 -406
  2. package/dist/cli/index.js.map +27 -25
  3. package/dist/types/core/config-input.d.ts +759 -0
  4. package/dist/types/core/config.d.ts +126 -3
  5. package/dist/types/core/data.d.ts +4 -0
  6. package/dist/types/core/i18n-ui.d.ts +50 -0
  7. package/dist/types/core/schema.d.ts +334 -62
  8. package/dist/types/core/types.d.ts +8 -0
  9. package/dist/types/index.d.ts +2 -1
  10. package/docs/advanced/changelog.mdx +10 -2
  11. package/docs/configuration/ai.mdx +56 -0
  12. package/docs/configuration/index.mdx +0 -2
  13. package/docs/configuration/seo.mdx +59 -1
  14. package/docs/configuration/theming.mdx +14 -9
  15. package/docs/content/meta.mdx +3 -17
  16. package/docs/content/navigation.mdx +41 -4
  17. package/docs/content/syntax.mdx +4 -8
  18. package/package.json +3 -1
  19. package/src/ai/agent-readability.ts +97 -0
  20. package/src/ai/ask-context.ts +131 -8
  21. package/src/ai/ask-data.ts +4 -1
  22. package/src/astro/generate.ts +40 -11
  23. package/src/astro/templates.ts +90 -10
  24. package/src/cli/commands/build.ts +41 -1
  25. package/src/cli/commands/dev.ts +31 -14
  26. package/src/cli/dev-lock.ts +94 -21
  27. package/src/components/content/GithubInfo.astro +11 -10
  28. package/src/components/content/TypeTable.astro +8 -3
  29. package/src/components/content/Update.astro +12 -2
  30. package/src/components/content/changelog-element.ts +62 -0
  31. package/src/components/islands/AskAI.astro +66 -2
  32. package/src/components/islands/ask-ai.tsx +289 -53
  33. package/src/components/layout/Header.astro +1 -1
  34. package/src/components/layout/NavTree.astro +1 -1
  35. package/src/components/layout/PageActions.astro +73 -30
  36. package/src/components/layout/RootLayout.astro +79 -10
  37. package/src/core/config-input.ts +933 -0
  38. package/src/core/config.ts +126 -3
  39. package/src/core/data.ts +4 -0
  40. package/src/core/graph.ts +7 -2
  41. package/src/core/i18n-ui.ts +5 -0
  42. package/src/core/nav-diagnostics.ts +7 -0
  43. package/src/core/navigation.ts +38 -12
  44. package/src/core/schema.ts +130 -22
  45. package/src/core/sources/filesystem.ts +5 -1
  46. package/src/core/sources/watch.ts +43 -12
  47. package/src/core/types.ts +9 -0
  48. package/src/deploy/adapter-output.ts +82 -0
  49. package/src/deploy/robots.ts +37 -4
  50. package/src/index.ts +1 -1
  51. package/src/markdown/index.ts +28 -30
  52. package/src/markdown/math.ts +3 -2
  53. package/src/openapi/scalar.ts +1 -1
  54. package/src/registry/eject.ts +21 -14
  55. package/src/search/documents.ts +9 -2
  56. package/src/theme/entry.ts +7 -3
  57. package/src/theme/palette.ts +21 -14
@@ -168,11 +168,19 @@ export interface NavSelector {
168
168
  kind: "dropdown" | "language" | "product" | "version";
169
169
  items: NavSelectorItem[];
170
170
  }
171
+ /** A pinned link rendered above the sidebar sections (external or internal). */
172
+ export interface FeaturedLink {
173
+ label: string;
174
+ href: string;
175
+ icon?: string;
176
+ }
171
177
  /** The complete navigation model derived from the content graph. */
172
178
  export interface Navigation {
173
179
  tabs: NavTab[];
174
180
  selectors: NavSelector[];
175
181
  sidebar: NavNode[];
182
+ /** Pinned links shown above the sidebar sections, unscoped by tab. */
183
+ featured: FeaturedLink[];
176
184
  /** Repo URL for the header link, or null when hidden (`navigation.repo`). */
177
185
  repoUrl?: string | null;
178
186
  }
@@ -5,6 +5,7 @@ export type { ComponentOverride, ComponentOverrides, IslandDescriptor, } from ".
5
5
  export { defineMeta } from "./core/define-meta.ts";
6
6
  export type { FolderMetaDefinition, FolderMetaFactory, } from "./core/define-meta.ts";
7
7
  export type { UIStrings } from "./core/i18n-ui.ts";
8
- export type { BlumeConfig, FolderMeta, HydrationMode, ResolvedConfig, } from "./core/schema.ts";
8
+ export type { BlumeConfig } from "./core/config-input.ts";
9
+ export type { FolderMeta, HydrationMode, ResolvedConfig, } from "./core/schema.ts";
9
10
  export type { Diagnostic, Heading, NavNode, Navigation, NavTab, PageRecord, } from "./core/types.ts";
10
11
  export { getBlumeVersion } from "./core/version.ts";
@@ -53,14 +53,22 @@ The optional `changelog` object adds richer metadata for the timeline and feed:
53
53
 
54
54
  ## The timeline page
55
55
 
56
- Once you have at least one `type: changelog` entry, Blume generates a **`/changelog`** page automatically. Each entry renders newest-first with its date, label, and `category` tag in a left rail beside its content:
56
+ Once you have at least one `type: changelog` entry, Blume generates a **`/changelog`** page automatically. It renders as a focused, full-width timeline — no sidebar or table of contents — with each entry newest-first, showing its date, label, and `category` tag in a left rail beside its content:
57
57
 
58
- - The entry **title** becomes its label — or `v{version}` when there's no title.
58
+ - The entry **title** becomes its label — or `v{version}` when there's no title. It links to that entry's own page, so a release is both a line in the timeline and a shareable permalink.
59
59
  - The `category` renders as a tag next to the date.
60
60
  - Drafts and `sidebar.hidden` entries are skipped.
61
61
 
62
62
  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 timeline.
63
63
 
64
+ ### Grouped by major version
65
+
66
+ When your versions follow [semver](https://semver.org) and span more than one major, Blume paginates the timeline by major version. Only the newest major line is shown, with a **Show N.x releases** button at the bottom that reveals the next-oldest major one click at a time:
67
+
68
+ - Detection is automatic — no configuration. It kicks in only when every listed release parses as `major.minor.patch` and there is more than one major; otherwise the timeline stays flat.
69
+ - It tolerates the scoped tags monorepos publish, so `pkg@2.0.0` groups under `2.x` and `pkg@1.4.0` under `1.x`.
70
+ - It's progressive enhancement: every release is still in the page's HTML (and its RSS feed and search index), so readers without JavaScript — and crawlers — see the complete history. The button only collapses older majors once the page hydrates.
71
+
64
72
  ## From GitHub Releases
65
73
 
66
74
  Rather than authoring entries by hand, point the built-in [`github-releases` 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:
@@ -62,6 +62,25 @@ ai: {
62
62
  }
63
63
  ```
64
64
 
65
+ ### Suggested questions
66
+
67
+ Seed the empty state with a few starter prompts. Each renders as a clickable suggestion — click one to send it — with an optional [Lucide icon](/docs/content/components#icon) beside the label:
68
+
69
+ ```ts blume.config.ts lineNumbers
70
+ ai: {
71
+ ask: {
72
+ enabled: true,
73
+ suggestions: [
74
+ { label: "What is Blume?", icon: "rocket" },
75
+ { label: "How do I write a docs page?", icon: "file-text" },
76
+ { label: "How do I configure the theme?", icon: "settings" },
77
+ ],
78
+ },
79
+ }
80
+ ```
81
+
82
+ `label` is the question that gets asked; `icon` is optional. Leave `suggestions` unset (or empty) and the panel opens to a plain input.
83
+
65
84
  ### Grounding
66
85
 
67
86
  Ask AI is **grounded in your docs**. For each question it retrieves the most relevant pages — using the same lexical [Orama](/docs/configuration/search) index that powers on-page search — and injects them into the model's system prompt, so answers come from your content instead of the model's own knowledge. The assistant is told to answer only from the retrieved pages, to say when something isn't covered, and to cite the pages it drew from.
@@ -175,6 +194,43 @@ A static build with `mcp.enabled` fails fast with a message telling you to set `
175
194
  claude mcp add --transport http my-docs https://docs.example.com/mcp
176
195
  ```
177
196
 
197
+ ## Agent readability
198
+
199
+ Blume writes an **`/agent-readability.json`** manifest at your site root that indexes the agent-facing surface described on this page — so an agent can discover it in a single fetch instead of guessing at conventions or scraping HTML. Unlike the features above, it's on by default:
200
+
201
+ ```ts blume.config.ts lineNumbers
202
+ seo: {
203
+ agentReadability: true,
204
+ }
205
+ ```
206
+
207
+ The manifest lists only what you've enabled — the [raw Markdown](#raw-markdown) mirror pattern, [`llms.txt`](#llmstxt) and `llms-full.txt`, the [MCP server](#mcp-server) and its discovery document, the [Ask AI](#ask-ai) endpoint, the [sitemap](/docs/configuration/seo#sitemap), and [RSS feeds](/docs/configuration/seo#rss-feeds) — alongside your site name, description, source repository, and the [content-signal](/docs/configuration/seo#content-signals) usage policy. URLs are absolute when [`deployment.site`](/docs/deployment) is set and root-relative otherwise:
208
+
209
+ ```json agent-readability.json
210
+ {
211
+ "artifacts": {
212
+ "markdown": {
213
+ "contentNegotiation": "text/markdown",
214
+ "pattern": "https://docs.example.com/{route}.md"
215
+ },
216
+ "llmsFullTxt": "https://docs.example.com/llms-full.txt",
217
+ "llmsTxt": "https://docs.example.com/llms.txt",
218
+ "mcp": {
219
+ "discovery": "https://docs.example.com/.well-known/mcp.json",
220
+ "url": "https://docs.example.com/mcp"
221
+ }
222
+ },
223
+ "description": "Docs for the Acme API.",
224
+ "generator": "blume@1.0.0",
225
+ "name": "Acme Docs",
226
+ "site": "https://docs.example.com",
227
+ "contentUsage": { "search": true, "ai-input": true, "ai-train": true },
228
+ "repository": "https://github.com/acme/docs"
229
+ }
230
+ ```
231
+
232
+ Set `seo.agentReadability` to `false` to skip it, or ship your own `public/agent-readability.json` to take over — Blume never overwrites a file you place in `public/`.
233
+
178
234
  ## Agent skill
179
235
 
180
236
  Building a Blume site with the help of a coding agent? Install the Blume [agent skill](https://docs.claude.com/en/docs/claude-code/skills) so it knows how Blume works without you explaining it:
@@ -49,11 +49,9 @@ export default defineConfig({
49
49
  // Markdown features
50
50
  markdown: {
51
51
  imageZoom: true,
52
- math: false,
53
52
  code: {
54
53
  icons: true, // language icon in the code-block header
55
54
  wrap: false, // wrap long lines instead of scrolling
56
- inline: false, // highlight inline `code{:lang}` snippets
57
55
  },
58
56
  },
59
57
 
@@ -154,7 +154,7 @@ Ship your own `public/sitemap.xml` to take over — Blume never overwrites a fil
154
154
 
155
155
  ## Robots
156
156
 
157
- Blume writes a `robots.txt` that allows all crawlers and adds a `Sitemap:` line pointing to the sitemap when one is available. On by default:
157
+ Blume writes a `robots.txt` that allows all crawlers, declares your [content signals](#content-signals), and adds a `Sitemap:` line pointing to the sitemap when one is available. On by default:
158
158
 
159
159
  ```ts blume.config.ts lineNumbers
160
160
  seo: {
@@ -164,9 +164,67 @@ seo: {
164
164
 
165
165
  ```txt robots.txt
166
166
  User-agent: *
167
+ Content-Signal: search=yes, ai-input=yes, ai-train=yes
167
168
  Allow: /
168
169
 
169
170
  Sitemap: https://docs.example.com/sitemap.xml
170
171
  ```
171
172
 
173
+ ### Content signals
174
+
175
+ The `Content-Signal` line — the emerging content-usage convention — declares how AI crawlers may reuse your docs. Blume emits it **on by default with every signal set to `yes`**, matching its stance that docs are open to humans and agents alike:
176
+
177
+ - `search` — traditional and AI search indexing
178
+ - `aiInput` — grounding / RAG at answer time
179
+ - `aiTrain` — model training
180
+
181
+ Restrict any signal by setting it to `false`; the ones you leave out stay `yes`:
182
+
183
+ ```ts blume.config.ts lineNumbers
184
+ seo: {
185
+ contentSignals: {
186
+ aiTrain: false, // opt out of training, keep search + grounding
187
+ },
188
+ }
189
+ ```
190
+
191
+ ```txt robots.txt
192
+ User-agent: *
193
+ Content-Signal: search=yes, ai-input=yes, ai-train=no
194
+ Allow: /
195
+ ```
196
+
197
+ Set `contentSignals: false` to drop the declaration entirely:
198
+
199
+ ```ts blume.config.ts lineNumbers
200
+ seo: {
201
+ contentSignals: false,
202
+ }
203
+ ```
204
+
205
+ <TypeTable
206
+ type={{
207
+ "seo.contentSignals": {
208
+ type: "boolean | object",
209
+ description:
210
+ "Content-Signal declaration. true or omitted emits all signals as yes; false drops the line; an object sets signals individually.",
211
+ },
212
+ "contentSignals.search": {
213
+ type: "boolean",
214
+ description: "Allow use for search indexing (search). Default true.",
215
+ },
216
+ "contentSignals.aiInput": {
217
+ type: "boolean",
218
+ description:
219
+ "Allow use for AI grounding / RAG at answer time (ai-input). Default true.",
220
+ },
221
+ "contentSignals.aiTrain": {
222
+ type: "boolean",
223
+ description: "Allow use for AI model training (ai-train). Default true.",
224
+ },
225
+ }}
226
+ />
227
+
228
+ Content signals express a preference, not access control: they tell well-behaved crawlers how you'd like your content used, and it's on the crawler to honour them.
229
+
172
230
  Ship your own `public/robots.txt` to take over.
@@ -34,6 +34,8 @@ theme: {
34
34
 
35
35
  Named presets: `blue` (default), `green`, `orange`, `pink`, `purple`, `red`, and `teal`.
36
36
 
37
+ A string applies to both color modes; pass an object for [a different accent per mode](#dark-mode-colors).
38
+
37
39
  ### Radius
38
40
 
39
41
  `radius` sets the corner rounding shared by cards, code blocks, callouts, and inputs — `none`, `sm`, `md` (default), or `lg`.
@@ -89,18 +91,19 @@ Need a font that isn't listed, or want to drop back to the system stack? Overrid
89
91
 
90
92
  ### Dark-mode colors
91
93
 
92
- By default light and dark share one `accent`. Set `accentDark` for a different accent in dark mode, and `background` / `backgroundDark` to override the page background per mode:
94
+ `accent` and `background` follow one rule: a string applies to both color modes, and a `{ light, dark }` object sets each mode individually:
93
95
 
94
96
  ```ts blume.config.ts lineNumbers
95
97
  theme: {
96
- accent: "blue",
97
- accentDark: "teal", // a different accent in dark mode
98
- background: "#ffffff", // light-mode page background
99
- backgroundDark: "#0a0a0a", // dark-mode page background
98
+ accent: { light: "blue", dark: "teal" },
99
+ background: {
100
+ light: "#ffffff",
101
+ dark: "#0a0a0a",
102
+ },
100
103
  }
101
104
  ```
102
105
 
103
- Each takes a named preset or any CSS color, exactly like `accent`.
106
+ Each color takes a named preset or any CSS color. For `background` (and `backgroundImage`) either key can be omitted to override a single mode — `background: { dark: "#0a0a0a" }` keeps the default light background.
104
107
 
105
108
  ### Action color
106
109
 
@@ -114,12 +117,14 @@ theme: {
114
117
 
115
118
  ### Background image
116
119
 
117
- Set a background image behind your content with `backgroundImage` (and `backgroundImageDark` for a dark variant) a URL or a path under `public/`:
120
+ Set a background image behind your content with `backgroundImage` a URL or a path under `public/`. Like the colors, a string applies to both modes and a `{ light, dark }` object sets each mode's image:
118
121
 
119
122
  ```ts blume.config.ts lineNumbers
120
123
  theme: {
121
- backgroundImage: "/bg-light.svg",
122
- backgroundImageDark: "/bg-dark.svg",
124
+ backgroundImage: {
125
+ light: "/bg-light.svg",
126
+ dark: "/bg-dark.svg",
127
+ },
123
128
  }
124
129
  ```
125
130
 
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  title: Folder meta
3
- description: Configure a sidebar group — its title, icon, order, display mode, and page order — with a meta.ts file.
3
+ description: Configure a sidebar group — its title, icon, order, and page order — with a meta.ts file.
4
4
  ---
5
5
 
6
6
  Every folder in your content tree becomes a sidebar group. Drop a `meta.ts` beside its pages to control how that group looks and how its children are ordered. It's entirely optional: without one, the group's label is the humanized folder name and its pages sort by [index, numeric prefix, then alphabetically](/docs/content/navigation#ordering).
@@ -16,7 +16,6 @@ export default defineMeta({
16
16
  title: "Guides",
17
17
  icon: "book-open",
18
18
  order: 2,
19
- display: "flat",
20
19
  collapsed: false,
21
20
  pages: ["configuration", "theming", "deployment"],
22
21
  });
@@ -31,25 +30,12 @@ Every field is optional — set only what you want to override.
31
30
  | `title` | `string` | The group's label. Defaults to the humanized folder name. |
32
31
  | `icon` | `string` | Icon shown next to the label. |
33
32
  | `order` | `number` | Position among sibling groups and pages. Lower numbers sort first. |
34
- | `display` | `"flat" \| "group" \| "page"` | How the group renders in the sidebar. Defaults to `flat`. See [below](#display-modes). |
35
- | `collapsed` | `boolean` | For `display: "group"`, whether the group starts collapsed. |
33
+ | `collapsed` | `boolean` | Under the [`group` display mode](/docs/content/navigation#display-modes), whether the group starts collapsed. |
36
34
  | `pages` | `string[]` | Explicit order for the group's children, by slug. |
37
35
 
38
36
  The `pages` array lists children by slug — the folder or file name with its numeric prefix and any parentheses stripped (so `01-quickstart.mdx` is `"quickstart"`). Children you leave out still appear, after the listed ones.
39
37
 
40
- ## Display modes
41
-
42
- `display` controls how a group and its items appear in the sidebar:
43
-
44
- - **`flat`** (default) — a non-collapsible header with its pages listed beneath.
45
- - **`group`** — a collapsible `<details>` disclosure. Pair it with `collapsed` to set the initial state; a group containing the current page always starts open.
46
- - **`page`** — a single row that, when clicked, slides the sidebar into a sub-panel showing only that group's items, with a back arrow at the top. The panel is route-aware, so landing directly on a page inside the group opens straight to it.
47
-
48
- :::tip
49
- `page` mode keeps deep sections tidy — reach for it when a group has many children and you'd rather drill into it than scroll past it.
50
- :::
51
-
52
- `display` works the same on a group in an [explicit sidebar](/docs/content/navigation#explicit-sidebar) config.
38
+ How groups render — flat headers, collapsible disclosures, or drill-in panels — is a sidebar-wide setting, not per folder: see [display modes](/docs/content/navigation#display-modes).
53
39
 
54
40
  ## Computed meta
55
41
 
@@ -31,7 +31,7 @@ See [Frontmatter](/docs/reference/frontmatter) for the full page schema.
31
31
 
32
32
  ## Folder groups
33
33
 
34
- Each folder becomes a sidebar group. Drop a [`meta.ts`](/docs/content/meta) beside its pages to set the group's title, icon, order, [display mode](/docs/content/meta#display-modes), and the order of its children:
34
+ Each folder becomes a sidebar group. Drop a [`meta.ts`](/docs/content/meta) beside its pages to set the group's title, icon, order, and the order of its children:
35
35
 
36
36
  ```ts meta.ts
37
37
  import { defineMeta } from "blume";
@@ -43,10 +43,32 @@ export default defineMeta({
43
43
  });
44
44
  ```
45
45
 
46
- See [Folder meta](/docs/content/meta) for every field, the three display modes, and computing meta at scan time.
46
+ See [Folder meta](/docs/content/meta) for every field and computing meta at scan time.
47
47
 
48
48
  To group pages _without_ adding a URL segment, use a parenthesized folder name — see [Pages](/docs/content#group-folders).
49
49
 
50
+ ## Display modes
51
+
52
+ `navigation.sidebar.display` sets how every sidebar group renders:
53
+
54
+ ```ts blume.config.ts lineNumbers
55
+ navigation: {
56
+ sidebar: {
57
+ display: "flat", // "flat" | "group" | "page"
58
+ },
59
+ }
60
+ ```
61
+
62
+ - **`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.
63
+ - **`group`** — a collapsible `<details>` disclosure per group. Pair it with `collapsed` in [folder meta](/docs/content/meta) to set the initial state; a group containing the current page always starts open.
64
+ - **`page`** — each group is a single row that, when clicked, slides the sidebar into a sub-panel showing only that group's items, with a back arrow at the top. The panel is route-aware, so landing directly on a page inside the group opens straight to it.
65
+
66
+ :::tip
67
+ `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.
68
+ :::
69
+
70
+ A group in an [explicit sidebar](#explicit-sidebar) can override the global mode with its own `display`.
71
+
50
72
  ## Ordering
51
73
 
52
74
  When the sidebar is generated, order is resolved highest priority first:
@@ -112,9 +134,24 @@ navigation: {
112
134
 
113
135
  Each item takes a `label`, a `path`, and optional `icon`, `description`, and `tag`. `kind` (`dropdown`, `product`, `version`, or `language`) is a hint for how the selector is used; all render the same dropdown.
114
136
 
137
+ ## Featured links
138
+
139
+ Pin links to the top of the sidebar, above every section — a blog, a changelog, a contact or support page that should always be one click away. Unlike the generated tree, featured links are **not scoped by tab**: they show on every route, on every breakpoint.
140
+
141
+ ```ts blume.config.ts lineNumbers
142
+ navigation: {
143
+ featured: [
144
+ { label: "Blog", href: "https://example.com/blog", icon: "newspaper" },
145
+ { label: "Contact", href: "/contact", icon: "headphones" },
146
+ ],
147
+ }
148
+ ```
149
+
150
+ Each link takes a `label`, an `href`, and an optional `icon` (a [built-in icon](/docs/configuration/customization) name, image path/URL, or inline SVG — the same as anywhere else). An `href` may point anywhere: an external URL opens in a new tab, while an internal route (`/contact`) is validated against your pages at build time, warning you if nothing matches.
151
+
115
152
  ## Explicit sidebar
116
153
 
117
- For full control, define `navigation.sidebar` in config. When it's set, Blume uses it verbatim and skips file-system generation:
154
+ For full control, list explicit items in `navigation.sidebar` a bare array is shorthand for `sidebar.items`, and the object form combines them with a global [`display`](#display-modes). When items are set, Blume uses them verbatim and skips file-system generation:
118
155
 
119
156
  ```ts blume.config.ts lineNumbers
120
157
  navigation: {
@@ -130,7 +167,7 @@ navigation: {
130
167
  }
131
168
  ```
132
169
 
133
- Each item is a page route (a string), a group (`label` + `items`), or a link (`label` + `href`). Groups can nest, take a [`display` mode](/docs/content/meta#display-modes), and start `collapsed`.
170
+ Each item is a page route (a string), a group (`label` + `items`), or a link (`label` + `href`). Groups can nest, override the global [`display` mode](#display-modes), and start `collapsed`.
134
171
 
135
172
  ## Repository link
136
173
 
@@ -139,7 +139,7 @@ export default defineConfig({
139
139
  ```
140
140
  ````
141
141
 
142
- Inline code can be highlighted too: add a `{:lang}` marker inside a backtick span and it's colored like a tiny code block — `useState(){:js}` or `T extends object{:ts}`. Turn it on with `markdown: { code: { inline: true } }`.
142
+ Inline code can be highlighted too: add a `{:lang}` marker inside a backtick span and it's colored like a tiny code block — `useState(){:js}` or `T extends object{:ts}`. It only kicks in when you add the marker, so plain inline code stays untouched — nothing to switch on.
143
143
 
144
144
  ### Line numbers
145
145
 
@@ -372,24 +372,20 @@ The names `caution`, `error`, `important`, and `warn` are accepted as aliases fo
372
372
 
373
373
  ## Math
374
374
 
375
- Render LaTeX with KaTeX for formulas in prose or as centered blocks — useful for math-heavy or scientific docs. Inline math goes in `$…$`; block math in `$$…$$`.
376
-
377
- The Pythagorean theorem is $a^2 + b^2 = c^2$.
375
+ Render LaTeX with KaTeX as centered blocks — useful for math-heavy or scientific docs. Wrap a formula in `$$…$$`:
378
376
 
379
377
  $$
380
378
  \int_0^\infty e^{-x^2}\,dx = \frac{\sqrt{\pi}}{2}
381
379
  $$
382
380
 
383
381
  ```md
384
- The Pythagorean theorem is $a^2 + b^2 = c^2$.
385
-
386
382
  $$
387
- \int_0^\infty e^{-x^2}\,dx = \frac{\sqrt{\pi}}{2}
383
+ a^2 + b^2 = c^2
388
384
  $$
389
385
  ```
390
386
 
391
387
  :::note
392
- Math is opt-in because `$` is common in prose and code. Enable it with `markdown: { math: true }` in `blume.config.ts`.
388
+ Math is block-only and on automatically write `$$…$$` and it renders; write none and KaTeX's stylesheet never ships. There's no inline `$…$` math: a lone `$` (currency, shell variables, code) is always left as literal text, so there's no delimiter to escape and no setting to toggle. Math is an MDX-only feature.
393
389
  :::
394
390
 
395
391
  ## Smart punctuation
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "blume",
3
- "version": "0.5.4",
3
+ "version": "0.6.1",
4
4
  "description": "Documentation that's fast, AI-ready, and zero-config.",
5
5
  "keywords": [
6
6
  "astro",
@@ -92,12 +92,14 @@
92
92
  "citty": "^0.1.6",
93
93
  "consola": "^3.4.0",
94
94
  "deepmerge": "^4.3.1",
95
+ "dompurify": "^3.4.11",
95
96
  "epub-gen-memory": "^1.1.2",
96
97
  "github-slugger": "^2.0.0",
97
98
  "gray-matter": "^4.0.3",
98
99
  "jiti": "^2.4.0",
99
100
  "js-yaml": "^4.1.0",
100
101
  "katex": "^0.17.0",
102
+ "marked": "^18.0.5",
101
103
  "mermaid": "^11.15.0",
102
104
  "pagefind": "^1.3.0",
103
105
  "pathe": "^2.0.0",
@@ -0,0 +1,97 @@
1
+ import type { BlumeProject } from "../core/project-graph.ts";
2
+ import type { ContentSignalPolicy, ContentSignals } from "../core/schema.ts";
3
+ import { buildRssFeeds } from "../deploy/rss.ts";
4
+
5
+ /** Token map for the machine-readable content-usage echo. */
6
+ const USAGE_TOKENS: [keyof ContentSignalPolicy, string][] = [
7
+ ["search", "search"],
8
+ ["aiInput", "ai-input"],
9
+ ["aiTrain", "ai-train"],
10
+ ];
11
+
12
+ /**
13
+ * The configured usage preferences as a `{ token: allowed }` object, or null
14
+ * when the declaration is disabled (`contentSignals: false`). Mirrors the
15
+ * robots.txt `Content-Signal` line so an agent that reads the manifest instead
16
+ * of robots.txt sees the same policy.
17
+ */
18
+ const usagePolicy = (
19
+ signals: ContentSignals
20
+ ): Record<string, boolean> | null => {
21
+ if (!signals) {
22
+ return null;
23
+ }
24
+ return Object.fromEntries(
25
+ USAGE_TOKENS.map(([key, token]) => [token, signals[key]] as const)
26
+ );
27
+ };
28
+
29
+ /**
30
+ * Build `agent-readability.json`: a root manifest that indexes the project's
31
+ * agent-facing surface — llms.txt, the raw-Markdown mirrors, the MCP server,
32
+ * Ask AI, sitemap, and feeds — so agents can discover and cite the docs without
33
+ * scraping HTML. URLs are absolute when a `site` is configured and root-relative
34
+ * otherwise. Returns null when the manifest is disabled.
35
+ */
36
+ export const buildAgentReadability = (
37
+ project: BlumeProject
38
+ ): Record<string, unknown> | null => {
39
+ const { config } = project;
40
+ if (!config.seo.agentReadability) {
41
+ return null;
42
+ }
43
+
44
+ const site = config.deployment.site ?? null;
45
+ // Concatenate rather than `new URL()` so a subpath deployment's base is kept.
46
+ const abs = (path: string): string =>
47
+ site ? `${site.replace(/\/+$/u, "")}${path}` : path;
48
+
49
+ const artifacts: Record<string, unknown> = {
50
+ markdown: {
51
+ contentNegotiation: "text/markdown",
52
+ pattern: abs("/{route}.md"),
53
+ },
54
+ };
55
+ if (config.ai.llmsTxt) {
56
+ artifacts.llmsFullTxt = abs("/llms-full.txt");
57
+ artifacts.llmsTxt = abs("/llms.txt");
58
+ }
59
+ if (config.mcp.enabled) {
60
+ artifacts.mcp = {
61
+ discovery: abs("/.well-known/mcp.json"),
62
+ url: abs(config.mcp.route),
63
+ };
64
+ }
65
+ if (config.ai.ask?.enabled) {
66
+ artifacts.askApi = abs("/api/ask");
67
+ }
68
+ if (site && config.seo.sitemap) {
69
+ artifacts.sitemap = abs("/sitemap.xml");
70
+ }
71
+ const feeds =
72
+ site && config.seo.rss.enabled
73
+ ? buildRssFeeds(project).map((feed) => abs(feed.path))
74
+ : [];
75
+ if (feeds.length > 0) {
76
+ artifacts.feeds = feeds;
77
+ }
78
+
79
+ const version = project.manifest?.blumeVersion;
80
+ const manifest: Record<string, unknown> = {
81
+ artifacts,
82
+ description: config.description,
83
+ generator: version ? `blume@${version}` : undefined,
84
+ name: config.mcp.name ?? config.title,
85
+ site,
86
+ };
87
+
88
+ const usage = usagePolicy(config.seo.contentSignals);
89
+ if (usage) {
90
+ manifest.contentUsage = usage;
91
+ }
92
+ if (config.github) {
93
+ manifest.repository = `https://github.com/${config.github.owner}/${config.github.repo}`;
94
+ }
95
+
96
+ return manifest;
97
+ };