blume 1.0.4 → 1.1.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 (117) hide show
  1. package/CHANGELOG.md +80 -0
  2. package/dist/cli/index.js +13404 -10228
  3. package/dist/cli/index.js.map +94 -63
  4. package/dist/types/ai/component-markdown.d.ts +12 -1
  5. package/dist/types/core/config-input.d.ts +73 -4
  6. package/dist/types/core/data.d.ts +9 -0
  7. package/dist/types/core/deployment-env.d.ts +6 -0
  8. package/dist/types/core/diagnostics.d.ts +23 -0
  9. package/dist/types/core/i18n-ui.d.ts +8 -8
  10. package/dist/types/core/schema.d.ts +144 -22
  11. package/dist/types/core/sources/types.d.ts +3 -1
  12. package/dist/types/core/standard-schema.d.ts +41 -0
  13. package/dist/types/core/types.d.ts +20 -0
  14. package/dist/types/og/card.d.ts +63 -0
  15. package/dist/types/og/dimensions.d.ts +12 -0
  16. package/docs/01-quickstart.mdx +1 -1
  17. package/docs/02-deployment.mdx +9 -1
  18. package/docs/advanced/api-reference.mdx +11 -0
  19. package/docs/advanced/changelog.mdx +2 -2
  20. package/docs/advanced/skills.mdx +1 -1
  21. package/docs/configuration/ai.mdx +3 -3
  22. package/docs/configuration/customization.mdx +1 -1
  23. package/docs/configuration/export.mdx +1 -1
  24. package/docs/configuration/index.mdx +21 -1
  25. package/docs/configuration/search.mdx +28 -1
  26. package/docs/configuration/seo.mdx +37 -2
  27. package/docs/configuration/theming.mdx +1 -1
  28. package/docs/content/components.mdx +14 -0
  29. package/docs/content/index.mdx +1 -1
  30. package/docs/content/meta.mdx +1 -1
  31. package/docs/content/navigation.mdx +5 -1
  32. package/docs/content/sources.mdx +1 -1
  33. package/docs/reference/cli.mdx +80 -2
  34. package/docs/reference/frontmatter.mdx +31 -1
  35. package/package.json +4 -3
  36. package/skills/blume-migrate/SKILL.md +1 -1
  37. package/skills/blume-migrate/references/mintlify.md +3 -2
  38. package/skills/blume-migrate/scripts/mintlify-codemod.mjs +16 -4
  39. package/src/ai/component-markdown.ts +39 -11
  40. package/src/ai/llms.ts +19 -2
  41. package/src/ai/markdown.ts +5 -1
  42. package/src/astro/adapter-root.ts +70 -0
  43. package/src/astro/generate.ts +124 -50
  44. package/src/astro/index.ts +1 -0
  45. package/src/astro/pages.ts +39 -8
  46. package/src/astro/templates.ts +93 -28
  47. package/src/audit/agent.ts +114 -0
  48. package/src/audit/catalog.ts +826 -0
  49. package/src/audit/checks/assets.ts +177 -0
  50. package/src/audit/checks/content.ts +231 -0
  51. package/src/audit/checks/duplicates.ts +131 -0
  52. package/src/audit/checks/i18n.ts +246 -0
  53. package/src/audit/checks/indexability.ts +213 -0
  54. package/src/audit/checks/links.ts +223 -0
  55. package/src/audit/checks/llms.ts +138 -0
  56. package/src/audit/checks/network.ts +272 -0
  57. package/src/audit/checks/og-image.ts +113 -0
  58. package/src/audit/checks/redirects.ts +87 -0
  59. package/src/audit/checks/robots.ts +114 -0
  60. package/src/audit/checks/sitemap.ts +229 -0
  61. package/src/audit/checks/social.ts +238 -0
  62. package/src/audit/crawl.ts +259 -0
  63. package/src/audit/graph.ts +74 -0
  64. package/src/audit/html.ts +54 -0
  65. package/src/audit/image-size.ts +63 -0
  66. package/src/audit/locate.ts +33 -0
  67. package/src/audit/redirects.ts +74 -0
  68. package/src/audit/report.ts +278 -0
  69. package/src/audit/run.ts +198 -0
  70. package/src/audit/snapshot.ts +189 -0
  71. package/src/audit/types.ts +214 -0
  72. package/src/audit/url.ts +103 -0
  73. package/src/cli/commands/audit.ts +205 -0
  74. package/src/cli/commands/build.ts +64 -13
  75. package/src/cli/index.ts +2 -0
  76. package/src/cli/prepare.ts +10 -2
  77. package/src/components/content/Tabs.astro +98 -15
  78. package/src/components/layout/Breadcrumbs.astro +1 -1
  79. package/src/components/layout/Header.astro +1 -0
  80. package/src/components/layout/PageFeedback.astro +1 -1
  81. package/src/components/layout/PageLayout.astro +5 -1
  82. package/src/components/layout/Pagination.astro +1 -1
  83. package/src/components/layout/RootLayout.astro +5 -3
  84. package/src/components/layout/Search.astro +35 -6
  85. package/src/components/layout/TableOfContents.astro +1 -1
  86. package/src/components/openapi/Authorization.astro +80 -0
  87. package/src/components/openapi/Operation.astro +19 -1
  88. package/src/components/openapi/ParametersTable.astro +1 -1
  89. package/src/components/openapi/security.ts +201 -0
  90. package/src/components/openapi/snippets.ts +42 -13
  91. package/src/core/config-input.ts +78 -4
  92. package/src/core/data.ts +9 -1
  93. package/src/core/deployment-env.ts +9 -0
  94. package/src/core/diagnostics.ts +61 -12
  95. package/src/core/graph.ts +23 -4
  96. package/src/core/links.ts +2 -91
  97. package/src/core/nav-diagnostics.ts +48 -4
  98. package/src/core/navigation.ts +169 -14
  99. package/src/core/probe.ts +136 -0
  100. package/src/core/project-graph.ts +54 -20
  101. package/src/core/schema.ts +93 -3
  102. package/src/core/sources/github-releases.ts +65 -2
  103. package/src/core/sources/normalize.ts +198 -25
  104. package/src/core/sources/types.ts +3 -1
  105. package/src/core/standard-schema.ts +54 -0
  106. package/src/core/types.ts +20 -0
  107. package/src/deploy/adapter-output.ts +27 -15
  108. package/src/deploy/headers.ts +66 -0
  109. package/src/deploy/redirects.ts +49 -9
  110. package/src/markdown/index.ts +1 -0
  111. package/src/markdown/twoslash.ts +60 -0
  112. package/src/og/card.ts +98 -33
  113. package/src/og/index.ts +1 -1
  114. package/src/registry/eject.ts +3 -1
  115. package/src/search/popular.ts +33 -0
  116. package/src/theme/entry.ts +6 -1
  117. /package/docs/{03-faq.mdx → 07-faq.mdx} +0 -0
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  title: AI
3
- description: Machine-readable docs with llms.txt and an optional in-page Ask AI assistant.
3
+ description: Make your docs machine-readable with llms.txt, add an optional in-page Ask AI assistant, and expose a hosted MCP server for coding agents.
4
4
  ---
5
5
 
6
6
  Blume has a few AI features: machine-readable docs for external tools (`llms.txt`, on by default), an in-page **Ask AI** assistant, and a hosted **MCP server** for coding agents. Ask AI and MCP are opt-in, and static docs stay fully static until you turn a feature on.
@@ -47,11 +47,11 @@ Append `.md` or `.mdx` to any page's URL to fetch its raw Markdown source — pe
47
47
 
48
48
  Nested routes work the same way (`/content/syntax.md`), and the home page is served at `/index.md`.
49
49
 
50
- The `.md` variant _downlevels_ components to plain Markdown for consumers that can't interpret JSX: `<TypeTable>` becomes a Markdown table, `<Callout>` a labeled blockquote, `<Steps>` an ordered list, `<Tabs>` bold-labeled sections, and `<YouTube>` a link. Anything that can't be converted faithfully — a custom component, or a prop computed from an import — is left as-is, and component markup inside fenced code blocks is never touched. The same conversion applies to `llms-full.txt` and the MCP server's `get_page` tool, so every agent-facing surface reads clean Markdown. When you want the untransformed source, use the `.mdx` variant.
50
+ The `.md` variant _downlevels_ components to plain Markdown for consumers that can't interpret JSX: `<TypeTable>` becomes a Markdown table, `<Callout>` a labeled blockquote, `<Steps>` an ordered list, `<Tabs>` bold-labeled sections, and `<YouTube>` a link. Props are evaluated with the page's `frontmatter` in scope, so a prop like `title={frontmatter.status}` resolves to the same value the rendered page shows. Anything that can't be converted faithfully — a custom component, or a prop computed from an import — is left as-is, and component markup inside fenced code blocks is never touched. The same conversion applies to `llms-full.txt` and the MCP server's `get_page` tool, so every agent-facing surface reads clean Markdown. When you want the untransformed source, use the `.mdx` variant.
51
51
 
52
52
  ### Custom component serializers
53
53
 
54
- Give your own components a Markdown form with `ai.markdownComponents` — a map of JSX name to serializer. Each serializer receives the component's `props` (statically evaluated from the MDX attributes) and its `children` (already downleveled to Markdown), and returns the replacement — or `null` to leave the JSX as-is:
54
+ Give your own components a Markdown form with `ai.markdownComponents` — a map of JSX name to serializer. Each serializer receives the component's `props` (statically evaluated from the MDX attributes, with the page's `frontmatter` in scope), its `children` (already downleveled to Markdown), and the page's `frontmatter` data, and returns the replacement — or `null` to leave the JSX as-is:
55
55
 
56
56
  ```ts blume.config.ts lineNumbers
57
57
  import { defineConfig } from "blume";
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  title: Customization
3
- description: Override components, add interactive islands, mount custom pages, install registry components, or eject.
3
+ description: Override components, add interactive islands, mount custom pages, install registry components, or eject entirely when you need full control.
4
4
  ---
5
5
 
6
6
  ## Component overrides
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  title: Export
3
- description: Let readers download any page as a PDF or EPUB — client-side, so static builds stay static.
3
+ description: Let readers download any page as a PDF or EPUB — rendered client-side, so your static builds stay static and need no server infrastructure.
4
4
  ---
5
5
 
6
6
  Blume can add an **Export** action to the [page actions](/docs/content/navigation#page-actions) beneath the table of contents, letting readers save the page they're on as a **PDF** or an **EPUB**. It's off by default and entirely client-side — no server, and [static](/docs/deployment) builds stay static.
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  title: Configuration file
3
- description: Every option in blume.config.ts — site metadata, content, and links to each feature guide.
3
+ description: Every option in blume.config.ts, from site metadata and content sources to the links that lead into each individual feature configuration guide.
4
4
  sidebar:
5
5
  label: blume.config.ts
6
6
  ---
@@ -188,6 +188,26 @@ content: {
188
188
 
189
189
  Static assets live in `public/` — a file at `public/logo.png` is served at `/logo.png`, so a reference like `![](/images/create.png)` resolves against `public/images/create.png`.
190
190
 
191
+ ## Frontmatter
192
+
193
+ Page frontmatter is strictly validated — an unknown key fails the build, so typos are caught early. To carry project-specific metadata (an owner, a review date), declare the extra keys under `frontmatter.extend`, each mapped to a schema you supply:
194
+
195
+ ```ts blume.config.ts lineNumbers
196
+ import { defineConfig } from "blume";
197
+ import { z } from "zod";
198
+
199
+ export default defineConfig({
200
+ frontmatter: {
201
+ extend: {
202
+ owner: z.string(),
203
+ reviewedAt: z.coerce.date().optional(),
204
+ },
205
+ },
206
+ });
207
+ ```
208
+
209
+ Any [Standard Schema](https://standardschema.dev) library works — Zod (whichever version your project installs), Valibot, ArkType. Keys outside the extension stay strictly validated, so typo-catching is unchanged. See [Custom keys](/docs/reference/frontmatter#custom-keys) for the validation semantics.
210
+
191
211
  ## GitHub
192
212
 
193
213
  Point Blume at your repository with `github`. It powers the header [repository link](/docs/content/navigation#repository-link) and the **Edit on GitHub** and **Give feedback** [page actions](/docs/content/navigation#page-actions):
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  title: Search
3
- description: Client-side search out of the box, with optional hosted and semantic backends.
3
+ description: Client-side search that works out of the box with no API keys, plus optional hosted and semantic backends you can switch to as your docs grow.
4
4
  ---
5
5
 
6
6
  Blume ships local search with no hosted infrastructure and no API keys. It runs in the browser, works in both `blume dev` and `blume build`, and indexes only your real content — navigation chrome and excluded pages are skipped. When you outgrow it, you can switch to a hosted or semantic backend without changing how search looks or behaves — only the `search.provider` you configure changes.
@@ -13,6 +13,33 @@ Open search with <Badge variant="accent">⌘K</Badge> (or `Ctrl K`), or press `/
13
13
 
14
14
  Queries match page **titles**, **descriptions**, and **body text**, with title matches ranked highest and descriptions above body.
15
15
 
16
+ ## Popular pages
17
+
18
+ Before a reader types a query, the search dialog shows a **Popular** list. By default it is the first six sidebar pages — which on multi-tab sites often surfaces the wrong section. Pin the links you want instead:
19
+
20
+ ```ts blume.config.ts lineNumbers
21
+ search: {
22
+ popular: [
23
+ { href: "/guides/getting-started", icon: "rocket", label: "Getting started" },
24
+ { href: "/guides/install", icon: "download", label: "Install" },
25
+ { href: "/concepts/overview", label: "Overview" },
26
+ ],
27
+ },
28
+ ```
29
+
30
+ Each entry takes an `href` (internal route or external URL) and a `label`, plus an optional `icon` — a built-in icon name shown beside the label, defaulting to a file glyph. Omit `popular` or leave it empty to keep the sidebar fallback.
31
+
32
+ Unlike icons elsewhere in Blume, `icon` here must be a built-in name: these rows render in a client island, so an image path or inline SVG isn't supported and falls back to the file glyph.
33
+
34
+ Write `href` as if the site were mounted at the root — a `basePath` is applied for you, the same as `navigation.featured`. External URLs pass through untouched.
35
+
36
+ <Callout type="warning">
37
+ A curated list is a single set of links shared by every language. On a site
38
+ with `i18n` configured, the sidebar fallback follows the reader's locale, but
39
+ `popular` entries point wherever their `href` says — so pin locale-prefixed
40
+ routes only if you want every reader sent to that one language.
41
+ </Callout>
42
+
16
43
  ## What's indexed
17
44
 
18
45
  For every indexable page, Blume indexes its title, description, and body reduced to plain text — code blocks, images, and markup are stripped, so results stay relevant. The index is built from your source files, so it's identical in dev and production.
@@ -108,7 +108,7 @@ seo: {
108
108
 
109
109
  ### Brand the generated card
110
110
 
111
- Set a local SVG and hex palette to match the generated card to your brand. The logo can live in `public/` or at the project root. Omit any palette value to keep its default.
111
+ Set a local SVG and color palette to match the generated card to your brand. The logo can live in `public/` or at the project root. Omit any palette value to keep its default.
112
112
 
113
113
  ```ts blume.config.ts lineNumbers
114
114
  seo: {
@@ -144,9 +144,44 @@ seo:
144
144
  ```
145
145
 
146
146
  :::note
147
- OG rendering uses hex internally, so an `oklch` custom accent falls back to the default. Use a named accent (`blue`, `teal`, …) or a hex value for custom cards.
147
+ Every palette color accepts any CSS color — hex, `oklch(…)`, `rgb(…)`, and so on. The accent also accepts a named preset (`blue`, `teal`, …), matching [`theme.accent`](/docs/configuration/theming#accent). A color the renderer can't parse fails the build rather than silently shipping a default-colored card.
148
148
  :::
149
149
 
150
+ Emoji in a page title or site title render as [Twemoji](https://github.com/jdecked/twemoji) glyphs, fetched from a CDN while the card renders — so a build whose titles contain emoji needs network access. Each glyph is fetched once per build, however many pages use it.
151
+
152
+ ### Non-Latin titles
153
+
154
+ The card's built-in font covers only Latin glyphs, so a title in another script (Japanese, Chinese, Korean, Arabic, …) renders as tofu — empty boxes. List one or more [Google Fonts](https://fonts.google.com) families in `og.fonts` that cover your script, by name:
155
+
156
+ ```ts blume.config.ts lineNumbers
157
+ seo: {
158
+ og: {
159
+ fonts: [
160
+ "Noto Sans JP",
161
+ { name: "Inter", weight: [400, 700] },
162
+ ],
163
+ },
164
+ }
165
+ ```
166
+
167
+ Each entry is a family name, or an object pinning its `weight` (a number, a list, or a variable range like `"100..900"`) and `style` (`"normal"`, `"italic"`, or both). The families are fetched from Google Fonts at build — so a build that uses them needs network access — and the renderer only pulls the glyph subsets each title actually uses. Fallback is per-glyph, so adding a family only affects glyphs the built-in font can't draw; Latin text renders exactly as before.
168
+
169
+ ### Custom page titles
170
+
171
+ A custom [`.astro` page](/docs/advanced/custom-pages) has no frontmatter to read, so its generated card is titled by humanizing the last URL segment of its route — `/getting-started` becomes "Getting Started", but `/cli` becomes "Cli". Name those cards explicitly with `og.titles`, keyed by route (`"/"` addresses the home, whose card otherwise carries the site title):
172
+
173
+ ```ts blume.config.ts lineNumbers
174
+ seo: {
175
+ og: {
176
+ titles: {
177
+ "/cli": "CLI",
178
+ },
179
+ },
180
+ }
181
+ ```
182
+
183
+ Entries only apply to custom pages — a content page's card always takes its headline from the page title, so retitle those in frontmatter instead.
184
+
150
185
  `seo.image` is frontmatter, so it only covers Markdown and MDX content. To give a custom [`.astro` page](/docs/advanced/custom-pages) its own social image — a marketing home or landing page, and the way to give the home page alone a bespoke share image — pass the `ogImage` prop to `PageLayout`.
151
186
 
152
187
  ## RSS feeds
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  title: Theming
3
- description: Tune the look with config tokens, override any CSS variable in theme.css, or use Tailwind utilities.
3
+ description: Tune the look with a handful of config tokens, override any CSS variable in theme.css, or drop down to Tailwind utilities for custom components.
4
4
  ---
5
5
 
6
6
  Blume's theme is token-driven and works in light and dark mode out of the box. Reach for as little or as much as you need: a few config tokens for the common cases, a `theme.css` to override any design token, or Tailwind utilities for custom components.
@@ -69,6 +69,20 @@ Switch between equivalent content in place — language variants, OS-specific co
69
69
  </Tabs>
70
70
  ```
71
71
 
72
+ Add `inline` to render borderless — a tab strip on a full-width rule with the content flowing beneath as prose — instead of the bordered box. Add `param` to sync the active tab to a URL query param instead of the hash, which makes the selection shareable: a link ending in `?install=windows` opens on the Windows tab. Each group syncs to its own `param`, so you can use several independent, deep-linkable groups on one page.
73
+
74
+ <Tabs inline param="install">
75
+ <Tab title="macOS">Use Homebrew to install the toolchain.</Tab>
76
+ <Tab title="Windows">Use winget to install the toolchain.</Tab>
77
+ </Tabs>
78
+
79
+ ```astro lineNumbers
80
+ <Tabs inline param="install">
81
+ <Tab title="macOS">Use Homebrew to install the toolchain.</Tab>
82
+ <Tab title="Windows">Use winget to install the toolchain.</Tab>
83
+ </Tabs>
84
+ ```
85
+
72
86
  ## Badge
73
87
 
74
88
  A small inline label for status or metadata — version tags, “new” or “beta” markers, stability levels. The `variant` tunes the color to the meaning.
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  title: Pages
3
- description: How files in your content folder become pages, and how to organize them.
3
+ description: How the files in your content folder become pages, and how to organize and name them so routing and navigation are inferred automatically.
4
4
  ---
5
5
 
6
6
  Your docs are just a folder of Markdown and MDX files. Blume turns each file into a page — routing, navigation, and metadata are inferred from the file system, so there's no manifest to keep in sync.
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  title: Folder meta
3
- description: Configure a sidebar group — its title, icon, order, and page order — with a meta.ts file.
3
+ description: Configure a sidebar group with a meta.ts file beside its pages — set the group's title, icon, order, and the order of the pages nested inside.
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).
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  title: Navigation
3
- description: Build the sidebar from your files, then refine it with frontmatter, folder meta, or config.
3
+ description: Blume builds the sidebar from your files, then lets you refine it with frontmatter, folder meta, or config — breadcrumbs and outlines follow along.
4
4
  ---
5
5
 
6
6
  Blume builds your sidebar from the file system, then lets you refine it as much — or as little — as you want: page by page, folder by folder, or with one explicit config. Breadcrumbs, previous/next links, and the on-page outline all follow from the same model, with nothing to wire up.
@@ -45,6 +45,8 @@ export default defineMeta({
45
45
 
46
46
  See [Folder meta](/docs/content/meta) for every field and computing meta at scan time.
47
47
 
48
+ A folder's `meta.title` and its own `index` page's frontmatter `title` are resolved independently — translating one under i18n and forgetting the other renders a correct sidebar with a stale `<title>`/heading on the landing page itself. Blume reports a `BLUME_NAV_INDEX_TITLE_MISMATCH` warning when they diverge. Untranslated pages filled in from the fallback locale are exempt — their title belongs to the fallback locale, and the fix is translating the page, not editing its frontmatter.
49
+
48
50
  To group pages _without_ adding a URL segment, use a parenthesized folder name — see [Pages](/docs/content#group-folders).
49
51
 
50
52
  ## Display modes
@@ -86,6 +88,8 @@ When the sidebar is generated, order is resolved highest priority first:
86
88
  </Step>
87
89
  </Steps>
88
90
 
91
+ Two siblings that land on the same explicit or numeric order fall back to alphabetical order between themselves — Blume reports a `BLUME_DUPLICATE_SIDEBAR_ORDER` warning so the tie doesn't go unnoticed.
92
+
89
93
  ## Hidden pages
90
94
 
91
95
  Hide a page from the sidebar — and from previous/next pagination — while keeping it built and reachable by its URL:
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  title: Content sources
3
- description: Pull docs from local files, a remote repo, or any custom backend — and mix several sources into one site.
3
+ description: Pull docs from local files, a remote repository, or any custom backend — and mix several sources into one static-first site read at build time.
4
4
  ---
5
5
 
6
6
  By default Blume reads a folder of `.md`/`.mdx` files. **Content sources** let you pull pages from somewhere else — a remote repository, a CMS, or any custom backend — and mix several sources into a single site. Sources are read at build time; Blume stays static-first.
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  title: CLI
3
- description: Every Blume command and flag — init, dev, build, preview, add, sync, and eject.
3
+ description: Every Blume command and flag explained in one place — init, dev, build, preview, add, sync, and eject — along with the options each one accepts.
4
4
  ---
5
5
 
6
6
  ```bash
@@ -21,6 +21,7 @@ blume <command> [options]
21
21
  | `blume check` | Type-check the site with `astro check`. |
22
22
  | `blume doctor` | Diagnose config and content problems. |
23
23
  | `blume validate` | Validate links across your content. |
24
+ | `blume audit` | Audit the built site for SEO and health issues. |
24
25
 
25
26
  ## Common flags
26
27
 
@@ -34,7 +35,7 @@ blume <command> [options]
34
35
  - `blume dev --content-dir <dir>` — scan a different content folder without editing `blume.config.ts`.
35
36
  - `blume dev --debug` — verbose Astro/Vite logging for troubleshooting.
36
37
  - `blume dev --preview` / `blume build --preview` — include drafts and unpublished CMS content.
37
- - `blume build --strict` — fail the build on diagnostic errors (also works on `blume dev`).
38
+ - `blume build --no-strict` — build despite diagnostic errors. By default `blume build` fails (exit 1) on any error diagnostic, because pages that fail frontmatter validation are dropped from the output; with `--no-strict` the build succeeds and reports how many pages are missing. `blume dev --strict` opts dev into the same fail-fast behavior.
38
39
  - `blume build --output static|server --adapter vercel|node|netlify|cloudflare --base /docs` — override the deployment output, adapter, and base path from `blume.config.ts`.
39
40
  - `blume build --analyze` — print the client JavaScript bundle sizes (largest first) after the build.
40
41
  - `blume build --budget-js <kb> --budget-css <kb>` — fail the build when total client JavaScript/CSS exceeds the budget, turning a performance target into a CI gate.
@@ -49,6 +50,14 @@ blume <command> [options]
49
50
  - `blume validate --external` — also check external links over the network.
50
51
  - `blume validate --strict` — exit non-zero on warnings too.
51
52
  - `blume validate --json` / `blume doctor --json` — emit diagnostics as JSON on stdout (with `code`, `severity`, `file`, `line`/`column`, and `docsUrl`) for CI and editor integrations.
53
+ - `blume audit --fail-on error|warning|info` — the CI gate; defaults to `error`. `--strict` is an alias for `--fail-on warning`.
54
+ - `blume audit --url <origin>` — also probe a live deployment for status codes, response headers, and redirect chains.
55
+ - `blume audit --external` — probe outbound links over the network.
56
+ - `blume audit --only <check|category>` / `--skip <check|category>` — narrow the report while you work through it (comma-separated).
57
+ - `blume audit --list-checks` — print every check the audit can report.
58
+ - `blume audit --verbose` — list every affected page instead of the first few.
59
+ - `blume audit --json` — emit the report as JSON on stdout.
60
+ - `blume audit --claude` / `--codex` — hand the findings to Claude Code or Codex to fix interactively.
52
61
 
53
62
  ## Verifying while the dev server runs
54
63
 
@@ -107,3 +116,72 @@ Without a project `tsconfig.json`, only the generated runtime is checked.
107
116
  - **Anchor links** (`#section`, `/guides/intro#setup`) must match a heading on the target page — misses are warnings.
108
117
  - **Asset links** (`/logo.png`) are checked against the `public/` directory.
109
118
  - **External links** are only checked with `--external` (off by default since it requires the network); dead links (404/410/unreachable) are errors, while rate-limited or transient responses (403/429/5xx/timeout) are warnings.
119
+
120
+ ## Auditing the built site
121
+
122
+ `blume validate` reads your _content_; `blume audit` reads the _built site_. It crawls the HTML in `dist/` after a build and reports SEO and site-health issues — titles, meta descriptions, canonicals, Open Graph and X cards, headings, hreflang, images, the sitemap, `robots.txt`, and structured data.
123
+
124
+ Because Blume built the site, every finding names the source file **and the front matter line** that fixes it, not just the URL a crawler would see:
125
+
126
+ ```
127
+ ⚠ Meta description too long or too short 5 pages
128
+ /docs/configuration/export content/docs/configuration/export.mdx:3
129
+ fix: Rewrite `description` in the frontmatter to fit the length range.
130
+ ```
131
+
132
+ Run it after a build:
133
+
134
+ ```bash
135
+ blume build
136
+ blume audit
137
+ ```
138
+
139
+ Findings are grouped by check rather than listed per page, so the report reads as a to-do list. Use `--verbose` to expand every affected page, and `--only`/`--skip` to work through one category at a time. `blume audit --list-checks` prints the full catalog.
140
+
141
+ ### Failing CI
142
+
143
+ The exit code is the contract. By default `blume audit` fails only on errors — things that are definitely broken, like a link to a page that was never built, a redirect loop, or an invalid sitemap. Advisory findings (a short description, a duplicate title) are warnings and do not fail the build:
144
+
145
+ ```bash
146
+ blume audit # fails on errors
147
+ blume audit --fail-on warning # also fails on warnings
148
+ ```
149
+
150
+ ### Checking a live deployment
151
+
152
+ Some things only the real server can tell you: whether a page that exists in `dist/` actually 404s behind a bad rewrite, whether responses are compressed, and whether an `X-Robots-Tag` header is quietly deindexing a page whose HTML looks perfectly fine. Point the audit at a deployment to add those checks:
153
+
154
+ ```bash
155
+ blume audit --url https://docs.example.com
156
+ blume audit --url https://docs.example.com --external # also probe outbound links
157
+ ```
158
+
159
+ Outbound links are graded rather than flatly failed: a 404 is a broken link you can fix, while a 403 or 5xx is usually rate limiting or someone else's outage and is reported as a warning.
160
+
161
+ ### Fixing the findings with an agent
162
+
163
+ If you use [Claude Code](https://claude.com/claude-code) or [Codex](https://developers.openai.com/codex/cli), the audit can hand its findings straight to it:
164
+
165
+ ```bash
166
+ blume audit --claude # or --codex
167
+ ```
168
+
169
+ This writes the complete JSON report — every affected page, not the terminal's three-page preview — to a file and opens the agent interactively with a prompt that walks it through the findings: edit the source file each finding names, apply its suggested fix, then run `blume build` and `blume audit` again until the report is clean. The session is interactive by design: you review the edits through the agent's own permission flow, and the agent is told never to fix a finding by deleting content.
170
+
171
+ `--only` and `--skip` narrow the handoff the same way they narrow the report, so you can send one category at a time.
172
+
173
+ ### What it does and doesn't check
174
+
175
+ The check set is deliberately narrower than a general-purpose SEO crawler's. Much of what such a crawler reports cannot happen to a Blume site — it never emits `rel=nofollow`, and Vite's content-hashed bundles are never missing or redirecting — and reporting those as permanent zeroes would just teach you to ignore the report.
176
+
177
+ Two limits worth stating plainly:
178
+
179
+ - **Structured data** is validated for well-formedness (valid JSON, a `@context`, a `@type` on every node). Blume does not validate against the full schema.org vocabulary or Google's rich-results rules.
180
+ - **Core Web Vitals** are not checked. They need a real browser, and a flag that quietly measured nothing would be worse than not having one — so `blume audit` reports the layout-shift causes it _can_ see offline (images with no `width`/`height`, oversized assets) and leaves the rest alone for now.
181
+
182
+ Anything the audit did not run is reported as skipped rather than silently passing:
183
+
184
+ ```
185
+ ⊘ network skipped — pass --url <origin> (9 checks)
186
+ ⊘ external skipped — pass --external (2 checks)
187
+ ```
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  title: Frontmatter
3
- description: Every frontmatter field a page accepts — title, description, sidebar, SEO, search, and more.
3
+ description: Every frontmatter field a page accepts, all optional — title, description, sidebar, SEO, search, and the rest, with what each one controls.
4
4
  ---
5
5
 
6
6
  Every page accepts the following frontmatter. All fields are optional.
@@ -81,4 +81,34 @@ changelog:
81
81
 
82
82
  `date` may live here or at the top level — both feed the [changelog RSS feed](/docs/content#feeds). See [Changelog](/docs/advanced/changelog) for the generated timeline page and feed.
83
83
 
84
+ ## Custom keys
85
+
86
+ Any key outside this reference fails the build, so typos are caught early. Projects that carry their own metadata can opt extra keys in via [`frontmatter.extend`](/docs/configuration#frontmatter) in `blume.config.ts`, each validated by a schema the project supplies:
87
+
88
+ ```ts blume.config.ts lineNumbers
89
+ import { defineConfig } from "blume";
90
+ import { z } from "zod";
91
+
92
+ export default defineConfig({
93
+ frontmatter: {
94
+ extend: {
95
+ owner: z.string(),
96
+ reviewedAt: z.coerce.date().optional(),
97
+ },
98
+ },
99
+ });
100
+ ```
101
+
102
+ ```yaml page.mdx
103
+ ---
104
+ title: Install
105
+ owner: "@sam"
106
+ reviewedAt: 2026-06-20
107
+ ---
108
+ ```
109
+
110
+ Schemas are accepted through the [Standard Schema](https://standardschema.dev) interface, so Zod (whichever version your project installs), Valibot, and ArkType all work. Every declared key is validated on every page — absent ones included — so a required schema enforces the key site-wide; mark it `.optional()` to validate only where present. All other keys stay strictly validated, and built-in fields can't be redeclared.
111
+
112
+ A page that fails validation fails `blume build` with a diagnostic naming the file and key. With [`--no-strict`](/docs/reference/cli#common-flags), the build succeeds anyway and the failing pages are dropped from the output — the build summary reports how many.
113
+
84
114
  Schemas are exported from `blume/schema` for editor and migration tooling.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "blume",
3
- "version": "1.0.4",
3
+ "version": "1.1.1",
4
4
  "description": "Documentation that's fast, AI-ready, and zero-config.",
5
5
  "keywords": [
6
6
  "astro",
@@ -86,8 +86,6 @@
86
86
  "@shikijs/twoslash": "^4.2.0",
87
87
  "@tailwindcss/typography": "^0.5.20",
88
88
  "@tailwindcss/vite": "^4",
89
- "@takumi-rs/core": "^1.8.7",
90
- "@takumi-rs/helpers": "^1.8.7",
91
89
  "@vercel/analytics": "^2.0.1",
92
90
  "ai": "^5.0.0",
93
91
  "astro": "^7.0.2",
@@ -103,6 +101,7 @@
103
101
  "katex": "^0.17.0",
104
102
  "marked": "^18.0.5",
105
103
  "mermaid": "^11.15.0",
104
+ "node-html-parser": "^9.0.0",
106
105
  "pagefind": "^1.3.0",
107
106
  "pathe": "^2.0.0",
108
107
  "react": "^19.0.0",
@@ -111,7 +110,9 @@
111
110
  "shiki": "^4.2.0",
112
111
  "simple-icons": "^13.0.0",
113
112
  "tailwindcss": "^4",
113
+ "takumi-js": "^2.2.1",
114
114
  "tinyglobby": "^0.2.10",
115
+ "twoslash": "^0.3.9",
115
116
  "typescript": "^6.0.3",
116
117
  "undici": "^8.6.0",
117
118
  "zod": "^3.24.0"
@@ -66,7 +66,7 @@ The single biggest shift for most sources — especially Mintlify — is that **
66
66
  - **`content`:** `root` (default `"docs"`, relative to the project dir where `blume` runs), `include`/`exclude` (arrays of globs **relative to `content.root`**; defaults `["**/*.{md,mdx}"]` / `["**/_*", "**/.*"]`), `sources` (staged sources: `filesystem`, `github-releases`, `notion`, `sanity`, `mdx-remote`, `custom` — OpenAPI is **not** one of these; it's the top-level `openapi` field), `pages` (custom `.astro` dir), `defaultType`. When docs sit directly under the project dir (no `docs/` subfolder), set `root` there and **scope `include` to the real content folders** instead of scanning everything — `references/monorepo.md` §1.
67
67
  - **`basePath`** (top-level): a site-wide mount point (e.g. `"/docs"`) prepended to **every** route while staying invisible to the sidebar (no wrapper group). This is the right target for a source that served all docs under a prefix (Docusaurus `routeBasePath`, a Fumadocs `baseUrl` of `/docs`) — distinct from a per-source `prefix` (which adds a nav group) and from `deployment.base` (host subdirectory).
68
68
  - **`navigation`:** `tabs`, `selectors`, `featured` (links pinned above the sidebar on every route), `sidebar` (`{ display, items }` — `display` is the global render mode above; `items` is an explicit tree), `repo`. **Avoid an explicit `navigation.sidebar` unless you have to** — lean on the filesystem-derived sidebar. It only works when the file tree roughly matches the intended sidebar layout, so reshape folders to match first; reach for `sidebar.items` only for a shape files genuinely can't express (see "Config-declared nesting" above).
69
- - **`search`** (Orama default, Pagefind opt-in), **`ai`** (llms.txt, Ask AI), **`mcp`**, **`openapi`**, **`redirects`**, **`seo`**, **`markdown`**, **`analytics`**, **`deployment`**, **`i18n`**, **`toc`**, **`lastModified`**, **`github`**.
69
+ - **`search`** (Orama default, Pagefind opt-in), **`ai`** (llms.txt, Ask AI, the MCP server), **`openapi`**, **`redirects`**, **`seo`**, **`markdown`**, **`analytics`**, **`deployment`**, **`i18n`**, **`toc`**, **`lastModified`**, **`github`**.
70
70
  - **Don't set `deployment.site`.** Blume auto-fills it: the dev server's `localhost` URL in dev, and the deployment URL (`VERCEL_PROJECT_PRODUCTION_URL`/`VERCEL_URL`) on Vercel. Hardcoding it in `blume.config.ts` overrides that auto-detection and pins the wrong absolute URL (canonical links, sitemap, OG, llms.txt) everywhere but the one host you typed — so leave it unset even when a source config had a `url`/`site` field. (Sitemap still generates in production because the deploy URL is present there.)
71
71
  - **Favicon is a filename convention, not config.** Drop `icon`/`favicon.{svg,png,ico}` (and `apple-icon.png`) in the project root or `public/` — Blume auto-detects it. There is **no** `favicon` config field. A source favicon given as `{ light, dark }` **collapses to one** — pick a single file and report the loss.
72
72
 
@@ -37,7 +37,7 @@ Resolve `$ref` includes first (Mintlify splits config across files). Map only wh
37
37
  | `variables` (`{{name}}`) | **inline into content** | Blume has no runtime `{{var}}` substitution — replace each `{{name}}` with its value in the pages |
38
38
  | `integrations.posthog` (`{ apiKey, apiHost }`) | `analytics.posthog` (`{ key, host }`) | preserve the host verbatim (e.g. `us.posthog.com` — Blume's default is `us.i.posthog.com`) |
39
39
  | `integrations` (GA, Plausible, Fathom, …) | `analytics.scripts` / `analytics.vercel` | one `scripts[]` entry per provider (`{ src, strategy, attributes }`); no first-class mapping beyond PostHog/Vercel |
40
- | `contextual` (`["copy","chatgpt","claude",…]`) | **mostly free** | Copy-as-Markdown and Open-in-chat are default page actions; `mcp` needs `mcp.enabled` + server output (report as a follow-up) |
40
+ | `contextual` (`["copy","chatgpt","claude",…]`) | **mostly free** | Copy-as-Markdown and Open-in-chat are default page actions; `mcp` needs `ai.mcp.enabled` + server output (report as a follow-up) |
41
41
  | `redirects` | `redirects: [{ from, to }]` | static only — see below |
42
42
  | `navigation.languages` | `i18n` | see i18n below |
43
43
 
@@ -107,7 +107,8 @@ Rewrite each page's MDX:
107
107
  - **Callouts → directives** (directives are **MDX-only** — Mintlify content is already `.mdx`, so this just works; never rename a page to `.md`): `<Note>`→`:::note`, `<Tip>`→`:::tip`, `<Warning>`→`:::warning`, `<Info>`→`:::info`, `<Check>`→`:::success`, `<Danger>`/`<Error>`→`:::danger`. `<Callout type="x">` maps by type (`caution`→warning, `check`→success). A `title` attr → `:::type[Title]`. Drop `icon`/color props.
108
108
  - **Accordions — container/item inversion!** Mintlify nests `<Accordion title="…">` inside `<AccordionGroup>`. Blume inverts: `<AccordionGroup>`→`<Accordion>` (container), and each Mintlify `<Accordion title="…">`→`<AccordionItem title="…">` (item).
109
109
  - **`<RequestExample>`/`<ResponseExample>`** → `<CodeGroup>` (titled-fence tabs).
110
- - **These pass through — Blume ships them natively:** `<Columns>`/`<Column>`, `<Expandable>`, `<Tooltip>`, `<Frame>`, `<Panel>`, `<Card>`/`<CardGroup>`, `<Tabs>`/`<Tab>`, `<Steps>`/`<Step>`. Keep them as-is.
110
+ - **`<Tabs>` → `<Tabs inline>`.** Mintlify renders tabs **borderless** — a strip on a full-width rule with the content flowing beneath as prose — while Blume's `<Tabs>` defaults to a bordered box. Add `inline` to each `<Tabs>` to preserve Mintlify's appearance; every child `<Tab title="…">` is unchanged. Don't add `param` — Mintlify tabs switch in place and don't deep-link to the URL, so plain `inline` is the faithful mapping.
111
+ - **These pass through — Blume ships them natively:** `<Columns>`/`<Column>`, `<Expandable>`, `<Tooltip>`, `<Frame>`, `<Panel>`, `<Card>`/`<CardGroup>`, `<Tab>`, `<Steps>`/`<Step>`. Keep them as-is.
111
112
  - **API fields → `TypeTable`.** Blume does **not** ship `<ParamField>`/`<ResponseField>`/`<RequestField>`. Convert a cluster of fields into one `<TypeTable>` (rows keyed by field name, each `{ type, required?, default?, description }`). For a fully spec'd API, prefer deleting the hand-written fields and using the [OpenAPI reference](#openapi) instead.
112
113
  - **`<Update>`** (a Mintlify changelog entry) has no component form → convert to a `type: changelog` page, or use the `github-releases` source.
113
114
  - **Snippets are inlined, not imported.** Blume has no `/snippets` import mechanism. For each `import X from "/snippets/x.mdx"` + `<X prop="v" />`, inline the snippet's body (substituting `{prop}` placeholders), then delete the import and the `/snippets` file. Named string imports (`import { foo } from "/snippets/vars.mdx"`) → inline the value at each `{foo}`.
@@ -277,7 +277,8 @@ const remapIcons = (fm) => {
277
277
  continue;
278
278
  }
279
279
  if (lucide !== raw) {
280
- fm[i] = `${m.groups.indent}icon: ${lucide}`;
280
+ const comment = m.groups.comment ? ` ${m.groups.comment}` : "";
281
+ fm[i] = `${m.groups.indent}icon: ${lucide}${comment}`;
281
282
  changes.push({ detail: `${raw} → ${lucide}`, kind: "icon-remap" });
282
283
  }
283
284
  i += 1;
@@ -301,11 +302,12 @@ const setNested = (fm, parent, child, value) => {
301
302
  return { ok: false, reason: "child-exists" };
302
303
  }
303
304
  fm.splice(start + 1, 0, ` ${child}: ${value}`);
304
- return { ok: true };
305
+ return { index: start + 1, ok: true };
305
306
  }
306
307
  // No parent block — append one at the end of the frontmatter.
308
+ const index = fm.length;
307
309
  fm.push(`${parent}:`, ` ${child}: ${value}`);
308
- return { ok: true };
310
+ return { index, ok: true };
309
311
  };
310
312
 
311
313
  // Drop unsupported keys, rename Mintlify-only keys, flag ambiguous ones.
@@ -341,14 +343,24 @@ const rewriteFields = (fm) => {
341
343
  });
342
344
  continue;
343
345
  }
346
+ // Remove the source line before inserting: setNested splices into the
347
+ // parent block, and when that block sits above the source key the insert
348
+ // would otherwise shift `start` onto the wrong line. Failure paths don't
349
+ // mutate `fm`, so the line can be restored as-is on conflict.
350
+ const [removed] = fm.splice(start, 1);
344
351
  const placed = setNested(fm, parent, child, tk.value);
345
352
  if (placed.ok) {
346
- fm.splice(start, 1);
353
+ if (placed.index < start) {
354
+ // The insert above the cursor pushed the unvisited lines down one
355
+ // slot; revisit the current index so none of them get skipped.
356
+ i += 1;
357
+ }
347
358
  changes.push({
348
359
  detail: `${tk.key} → ${parent}.${child}`,
349
360
  kind: "rename",
350
361
  });
351
362
  } else {
363
+ fm.splice(start, 0, removed);
352
364
  // Target already set, or parent is a scalar — leave the source in place so
353
365
  // no data is lost, and report it for manual resolution.
354
366
  changes.push({