blume 0.5.3 → 0.6.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 (132) hide show
  1. package/dist/cli/index.js +3349 -7024
  2. package/dist/cli/index.js.map +39 -69
  3. package/dist/types/core/config.d.ts +0 -8
  4. package/dist/types/core/data.d.ts +6 -2
  5. package/dist/types/core/i18n-ui.d.ts +50 -0
  6. package/dist/types/core/schema.d.ts +379 -485
  7. package/dist/types/core/types.d.ts +8 -6
  8. package/docs/advanced/meta.ts +1 -8
  9. package/docs/advanced/skills.mdx +28 -0
  10. package/docs/configuration/ai.mdx +58 -0
  11. package/docs/configuration/index.mdx +13 -17
  12. package/docs/configuration/seo.mdx +59 -1
  13. package/docs/configuration/theming.mdx +15 -18
  14. package/docs/content/components.mdx +2 -53
  15. package/docs/content/i18n.mdx +0 -4
  16. package/docs/content/meta.mdx +3 -17
  17. package/docs/content/navigation.mdx +41 -4
  18. package/docs/content/syntax.mdx +1 -1
  19. package/docs/index.mdx +0 -1
  20. package/docs/reference/cli.mdx +12 -13
  21. package/package.json +6 -6
  22. package/skills/blume/SKILL.md +71 -0
  23. package/skills/blume-update-docs/SKILL.md +52 -0
  24. package/skills/blume-update-docs/references/audit-checklist.md +46 -0
  25. package/src/ai/agent-readability.ts +97 -0
  26. package/src/ai/ask-context.ts +131 -8
  27. package/src/ai/ask-data.ts +4 -1
  28. package/src/astro/generate.ts +19 -12
  29. package/src/astro/integration.ts +0 -21
  30. package/src/astro/templates.ts +33 -21
  31. package/src/cli/commands/build.ts +15 -0
  32. package/src/cli/commands/dev.ts +31 -20
  33. package/src/cli/commands/validate.ts +0 -2
  34. package/src/cli/dev-lock.ts +94 -21
  35. package/src/cli/index.ts +0 -2
  36. package/src/components/BlumePage.astro +0 -6
  37. package/src/components/Icon.astro +1 -12
  38. package/src/components/content/AccordionItem.astro +3 -6
  39. package/src/components/content/Badge.astro +1 -3
  40. package/src/components/content/Callout.astro +3 -9
  41. package/src/components/content/Card.astro +2 -3
  42. package/src/components/content/ColorItem.astro +2 -2
  43. package/src/components/content/Column.astro +1 -1
  44. package/src/components/content/GithubInfo.astro +11 -10
  45. package/src/components/content/Prompt.astro +1 -1
  46. package/src/components/content/Step.astro +3 -4
  47. package/src/components/content/Tab.astro +2 -3
  48. package/src/components/content/TypeTable.astro +13 -8
  49. package/src/components/content/Update.astro +1 -1
  50. package/src/components/islands/AskAI.astro +66 -2
  51. package/src/components/islands/ask-ai.tsx +289 -53
  52. package/src/components/layout/Header.astro +27 -4
  53. package/src/components/layout/Logo.astro +5 -1
  54. package/src/components/layout/NavSelector.astro +1 -1
  55. package/src/components/layout/NavTree.astro +15 -15
  56. package/src/components/layout/PageActions.astro +73 -30
  57. package/src/components/layout/PageLayout.astro +42 -0
  58. package/src/components/layout/ReferenceLayout.astro +1 -0
  59. package/src/components/layout/RootLayout.astro +79 -4
  60. package/src/components/layout/Search.astro +5 -5
  61. package/src/components/layout/nav-utils.ts +9 -4
  62. package/src/components/openapi/ApiOverview.astro +4 -50
  63. package/src/components/openapi/ApiTagOperations.astro +42 -0
  64. package/src/core/builtin-tags.ts +1 -3
  65. package/src/core/config.ts +5 -28
  66. package/src/core/data.ts +6 -2
  67. package/src/core/graph.ts +8 -6
  68. package/src/core/i18n-ui.ts +5 -0
  69. package/src/core/links.ts +5 -19
  70. package/src/core/meta.ts +1 -1
  71. package/src/core/nav-diagnostics.ts +7 -0
  72. package/src/core/navigation.ts +38 -17
  73. package/src/core/project-graph.ts +0 -5
  74. package/src/core/schema.ts +133 -95
  75. package/src/core/sources/filesystem.ts +5 -1
  76. package/src/core/sources/resolve.ts +0 -13
  77. package/src/core/sources/watch.ts +43 -11
  78. package/src/core/types.ts +8 -6
  79. package/src/deploy/robots.ts +37 -4
  80. package/src/openapi/parse.ts +197 -14
  81. package/src/openapi/render-mdx.ts +44 -10
  82. package/src/openapi/scalar.ts +1 -1
  83. package/src/openapi/source.ts +19 -2
  84. package/src/search/documents.ts +9 -2
  85. package/src/theme/entry.ts +45 -17
  86. package/src/theme/icons.ts +18 -109
  87. package/src/theme/palette.ts +25 -51
  88. package/src/theme/twoslash.ts +6 -1
  89. package/dist/types/core/bridge.d.ts +0 -24
  90. package/dist/types/core/package-json.d.ts +0 -12
  91. package/dist/types/migrate/mintlify/assets.d.ts +0 -8
  92. package/dist/types/migrate/mintlify/config.d.ts +0 -16
  93. package/dist/types/migrate/mintlify/i18n.d.ts +0 -7
  94. package/dist/types/migrate/shared.d.ts +0 -153
  95. package/docs/advanced/bridge.mdx +0 -76
  96. package/docs/advanced/migrate.mdx +0 -124
  97. package/src/astro/static-assets.ts +0 -124
  98. package/src/cli/commands/migrate.ts +0 -39
  99. package/src/components/content/ApiField.astro +0 -75
  100. package/src/components/content/ParamField.astro +0 -39
  101. package/src/components/content/RequestField.astro +0 -23
  102. package/src/components/content/ResponseField.astro +0 -23
  103. package/src/components/content/Warning.astro +0 -9
  104. package/src/core/assets.ts +0 -31
  105. package/src/core/bridge.ts +0 -102
  106. package/src/core/sources/mintlify.ts +0 -190
  107. package/src/migrate/fumadocs/config.ts +0 -155
  108. package/src/migrate/fumadocs/content.ts +0 -376
  109. package/src/migrate/fumadocs/frontmatter.ts +0 -18
  110. package/src/migrate/fumadocs/groups.ts +0 -237
  111. package/src/migrate/fumadocs/index.ts +0 -355
  112. package/src/migrate/fumadocs/meta.ts +0 -244
  113. package/src/migrate/migrate.ts +0 -53
  114. package/src/migrate/mintlify/assets.ts +0 -46
  115. package/src/migrate/mintlify/config.ts +0 -954
  116. package/src/migrate/mintlify/content.ts +0 -120
  117. package/src/migrate/mintlify/frontmatter.ts +0 -126
  118. package/src/migrate/mintlify/i18n.ts +0 -51
  119. package/src/migrate/mintlify/icons.ts +0 -128
  120. package/src/migrate/mintlify/index.ts +0 -459
  121. package/src/migrate/mintlify/snippets.ts +0 -315
  122. package/src/migrate/mintlify/transform.ts +0 -82
  123. package/src/migrate/nextra/content.ts +0 -46
  124. package/src/migrate/nextra/frontmatter.ts +0 -40
  125. package/src/migrate/nextra/index.ts +0 -389
  126. package/src/migrate/nextra/meta.ts +0 -266
  127. package/src/migrate/shared.ts +0 -801
  128. package/src/migrate/starlight/config.ts +0 -455
  129. package/src/migrate/starlight/content.ts +0 -75
  130. package/src/migrate/starlight/frontmatter.ts +0 -111
  131. package/src/migrate/starlight/i18n.ts +0 -54
  132. package/src/migrate/starlight/index.ts +0 -131
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "blume",
3
- "version": "0.5.3",
3
+ "version": "0.6.0",
4
4
  "description": "Documentation that's fast, AI-ready, and zero-config.",
5
5
  "keywords": [
6
6
  "astro",
@@ -28,7 +28,8 @@
28
28
  "dist",
29
29
  "src",
30
30
  "bin",
31
- "docs"
31
+ "docs",
32
+ "skills"
32
33
  ],
33
34
  "type": "module",
34
35
  "types": "./dist/types/index.d.ts",
@@ -70,11 +71,7 @@
70
71
  "@astrojs/node": "^11.0.0",
71
72
  "@astrojs/react": "^6.0.0",
72
73
  "@astrojs/vercel": "^11.0.0",
73
- "@iconify-json/fa6-brands": "^1.2.6",
74
- "@iconify-json/fa6-regular": "^1.2.4",
75
- "@iconify-json/fa6-solid": "^1.2.4",
76
74
  "@iconify-json/lucide": "^1.2.115",
77
- "@iconify-json/tabler": "^1.2.35",
78
75
  "@iconify/types": "^2.0.0",
79
76
  "@iconify/utils": "^3.1.3",
80
77
  "@modelcontextprotocol/sdk": "^1.29.0",
@@ -95,12 +92,14 @@
95
92
  "citty": "^0.1.6",
96
93
  "consola": "^3.4.0",
97
94
  "deepmerge": "^4.3.1",
95
+ "dompurify": "^3.4.11",
98
96
  "epub-gen-memory": "^1.1.2",
99
97
  "github-slugger": "^2.0.0",
100
98
  "gray-matter": "^4.0.3",
101
99
  "jiti": "^2.4.0",
102
100
  "js-yaml": "^4.1.0",
103
101
  "katex": "^0.17.0",
102
+ "marked": "^18.0.5",
104
103
  "mermaid": "^11.15.0",
105
104
  "pagefind": "^1.3.0",
106
105
  "pathe": "^2.0.0",
@@ -111,6 +110,7 @@
111
110
  "tailwindcss": "^4",
112
111
  "tinyglobby": "^0.2.10",
113
112
  "typescript": "^5.7.0",
113
+ "undici": "^8.6.0",
114
114
  "zod": "^3.24.0"
115
115
  },
116
116
  "devDependencies": {
@@ -0,0 +1,71 @@
1
+ ---
2
+ name: blume
3
+ description: Build and maintain documentation sites with Blume, the markdown-first docs framework on Astro and Vite. Use when working in a project that depends on `blume`, when scaffolding or configuring a docs site, writing Markdown/MDX content, tuning navigation/search/theming/SEO/AI features, running the `blume` CLI (init, dev, build, eject), or editing `blume.config.ts` and `meta.ts` files.
4
+ ---
5
+
6
+ # Blume
7
+
8
+ Blume is an open-source, **markdown-first** documentation framework built on Astro and Vite. Drop Markdown or MDX into a folder, run `blume dev`, and get a production-grade docs site — navigation, search, theming, Open Graph images, and a rich component library — with no app boilerplate to write or maintain.
9
+
10
+ The core idea: **the framework _is_ the template.** There's no starter to clone and no project to own before you've written a word. The only thing you touch is your content. When you outgrow the defaults, you add configuration one file at a time — and you can `blume eject` to a plain Astro project the day you want full control.
11
+
12
+ ## What makes it different
13
+
14
+ - **Fast by default** — Static HTML on Astro/Vite. The core theme ships no client framework JS so pages score well on Core Web Vitals out of the box. You opt into server features only when you need them.
15
+ - **AI-ready out of the box** — Emits `llms.txt`/`llms-full.txt`, serves any page's raw Markdown by appending `.md` to its URL, offers **Copy as Markdown** and **Open in chat** on every page, and can host an optional **Ask AI** assistant or an **MCP server** so coding agents read your docs directly.
16
+ - **Zero configuration — even the template** — A folder of docs is a complete project. Navigation is inferred from files, search works in dev and production with no hosted service, and theming is a handful of tokens.
17
+ - **Type-safe to the core** — `blume.config.ts` and every `meta.ts` are real TypeScript, validated by a schema and authored with `defineConfig` and `defineMeta`. Your editor autocompletes options and catches mistakes before a build.
18
+
19
+ ## Quickstart
20
+
21
+ Blume needs **Node.js 22 or newer**. From an empty or existing project:
22
+
23
+ ```bash
24
+ npm i blume # install the package
25
+ blume init # scaffold: docs/index.mdx + blume.config.ts
26
+ blume dev # dev server with hot reload
27
+ blume build # static HTML to dist/, with a local search index
28
+ ```
29
+
30
+ Blume works with any package manager and never requires you to set up Astro or Tailwind yourself.
31
+
32
+ ### Writing a page
33
+
34
+ Every page is Markdown or MDX with a little frontmatter. The `title` and `description` render as the page heading and intro automatically; built-in components (callouts, cards, tabs, steps, and more) need **no imports**.
35
+
36
+ ```mdx
37
+ ---
38
+ title: Introduction
39
+ description: Welcome to my docs.
40
+ ---
41
+
42
+ Welcome! Use **Markdown** and built-in components — no imports required:
43
+
44
+ :::note Blume ships callouts, cards, tabs, steps, and more. :::
45
+ ```
46
+
47
+ Navigation, search, and page metadata are inferred from your files as you add them.
48
+
49
+ ## What's included
50
+
51
+ - **Components** — callouts, cards, steps, tabs, accordions, badges, file trees, and parameter tables, usable in MDX with no imports.
52
+ - **Local search** — Orama in dev and production; Pagefind is one flag away for large sites. No hosted index.
53
+ - **AI** — `llms.txt`, raw Markdown URLs, Copy as Markdown, Open in chat, an Ask AI assistant, and a hosted MCP server.
54
+ - **Navigation** — inferred from files, refined with `meta.ts` or config.
55
+ - **SEO** — metadata, Open Graph images, RSS feeds, and JSON-LD.
56
+ - **Customization** — component overrides, React islands, custom pages, theme tokens, and a source-component registry via `blume add`.
57
+ - **Eject** — `blume eject` produces a standalone Astro project that still uses the `blume` package.
58
+
59
+ ## How it works
60
+
61
+ The Blume CLI discovers your content, builds a content graph, and generates a hidden Astro project under `.blume/` that it drives for dev and build. The generated runtime is an implementation detail — you write Markdown, Blume handles the rest — until you choose to eject and own it.
62
+
63
+ ## Full documentation
64
+
65
+ This is a high-level overview. For complete, authoritative docs — configuration reference, every CLI command and flag, component APIs, content authoring, navigation, search, SEO, AI features, theming, and deployment — read the bundled docs in the installed package:
66
+
67
+ ```
68
+ node_modules/blume/docs
69
+ ```
70
+
71
+ Start with `node_modules/blume/docs/index.mdx` (Introduction) and `node_modules/blume/docs/01-quickstart.mdx`, then browse the `configuration/`, `content/`, `reference/`, and `advanced/` sections for specifics.
@@ -0,0 +1,52 @@
1
+ ---
2
+ name: blume-update-docs
3
+ description: Keep a Blume docs site in sync with the product it documents. Audit recently merged pull requests, changelogs, config schemas, CLI help, and public APIs against the docs content, update only pages that are factually stale, verify the docs build, and open (or update) a maintenance pull request — or report a clean no-op. Use when asked to check docs for drift, refresh stale documentation, run a scheduled docs audit, or keep docs current after a release.
4
+ ---
5
+
6
+ # Update Blume Docs
7
+
8
+ Blume is a **markdown-first** documentation framework on Astro/Vite: content lives as Markdown/MDX under a content root (default `docs/`), navigation derives from the file tree plus optional `meta.ts` files, and `blume build` validates links, anchors, frontmatter, and routes.
9
+
10
+ Your job is **docs maintenance, not docs authorship**: find where shipped, user-facing behavior has drifted from what the docs claim, fix exactly that, prove the site still builds, and deliver the result as a pull request. A run that finds nothing actionable ends with a short report and **no branch, no commit, no PR** — prefer a no-op over a noisy PR.
11
+
12
+ ## Ground rules
13
+
14
+ - **Only document what shipped.** Never invent features, timelines, pricing, APIs, or compatibility claims. Work behind a feature flag is not ready for docs unless the flag is enabled for the documented audience or the repo explicitly documents unreleased behavior.
15
+ - **Facts over polish.** Edit when a command, option, default, route, prop, or workflow is wrong or missing. Skip subjective rewording, marketing polish, restructuring, and formatting-only churn.
16
+ - **Smallest correct diff.** Touch the fewest pages that remove the drift. Preserve the site's voice, frontmatter style, component usage, and `meta.ts` navigation patterns.
17
+ - **Exact source-of-truth wording** for commands, flags, config keys, environment variables, routes, and version numbers — copy them from code, don't paraphrase from memory.
18
+ - **Respect the repo.** Follow `AGENTS.md`/`CLAUDE.md` conventions, don't touch generated output (`.blume/`, `dist/`), and never overwrite unrelated local changes.
19
+
20
+ ## Workflow
21
+
22
+ 1. **Establish context.**
23
+ - Read the repo's agent/contributor instructions (`AGENTS.md`, `CLAUDE.md`, contribution docs) and honor them.
24
+ - Locate the docs app and content root: `blume.config.ts` (`content.root`), the directory of `.md`/`.mdx` pages, `meta.ts` files, and the package manager + docs build command.
25
+ - If this run was configured with a trigger, lookback window, docs path, target branch, or PR policy, honor those. Use the defaults below only where the prompt is silent.
26
+
27
+ 2. **Reuse or create a maintenance branch.**
28
+ - If an open docs-maintenance PR from a previous run exists (head branch starting with `blume/`), check out and update that branch instead of opening a duplicate.
29
+ - Otherwise branch from the default branch as `blume/docs-refresh-YYYY-MM-DD`. Create the branch only once you know an edit is needed.
30
+
31
+ 3. **Find drift.** Read `references/audit-checklist.md` for the full source list and change criteria, then:
32
+ - Review PRs merged into the default branch within the lookback window (default: the last 7 days) and extract the user-facing changes.
33
+ - Compare those changes — plus changelogs, release notes, config schemas, exported APIs, CLI help, and examples — against the docs content.
34
+ - Check external links only when a checked page depends on them; prefer official docs and release notes over secondary sources.
35
+ - Keep notes: what you checked, what changed upstream, and why each edit is (or isn't) needed.
36
+
37
+ 4. **Update the docs.**
38
+ - Fix the stale pages. Add, rename, or remove `meta.ts` entries when pages are added, renamed, or deleted.
39
+ - Match the surrounding pages: frontmatter shape, Blume components already in use, code-fence style, root-relative internal links.
40
+
41
+ 5. **Verify.**
42
+ - Run the docs build (`blume build` or the repo's documented docs QA) — it validates links, anchors, frontmatter, and duplicate routes.
43
+ - Run lint/format/typecheck when the repo's conventions call for them on docs changes.
44
+ - Fix failures your edits caused; report pre-existing failures separately instead of fixing them in this PR.
45
+
46
+ 6. **Deliver.**
47
+ - **Changes made:** commit only the maintenance edits, push the `blume/*` branch, and open or update a PR against the default branch titled like `blume: refresh docs for YYYY-MM-DD`. In the body list sources checked, docs changed, verification commands and results, skipped checks, and residual risk.
48
+ - **No changes needed:** report the PRs and docs areas checked and the no-op result. Do not create a branch, commit, or PR.
49
+
50
+ ## Resources
51
+
52
+ - `references/audit-checklist.md` — the source checklist, edit/skip criteria, and Blume-specific editing guidance. Read it before making docs changes.
@@ -0,0 +1,46 @@
1
+ # Audit checklist
2
+
3
+ Use this checklist to decide whether a docs-maintenance run should make changes.
4
+
5
+ ## Sources to check
6
+
7
+ - **Repo instructions:** `AGENTS.md`, `CLAUDE.md`, contribution docs, release docs, PR templates, and package scripts.
8
+ - **Blume config:** `blume.config.ts` — `content.root`, navigation (`meta.ts` files, tabs, selectors), site settings, AI/MCP settings, search, OpenAPI/AsyncAPI sources, theme, and export settings.
9
+ - **Public surface area:** exported package entrypoints, config schemas, component props, CLI commands and help text, route handlers, environment variables, and registry items.
10
+ - **User workflows:** quickstarts, examples, migration guides, deployment guides, screenshots, sample projects, and README snippets.
11
+ - **Recent merge signals:** PRs merged within the lookback window (default 7 days), changelogs, release notes, changesets, tags, and package version bumps.
12
+ - **Docs content:** every `.md`/`.mdx` page under the configured content root, plus custom pages and blog/changelog entries.
13
+ - **External dependencies:** official provider docs and release notes for linked integrations — only when the docs mention them or the dependency changed.
14
+ - **Generated docs surfaces:** `llms.txt`, raw Markdown URLs, MCP tools, OpenAPI pages, search, sitemap, robots, RSS, and OG behavior when relevant.
15
+
16
+ ## Change criteria
17
+
18
+ Make a docs edit when at least one condition is true:
19
+
20
+ - A command, config option, environment variable, route, CLI flag, component prop, or default value changed.
21
+ - A documented workflow no longer works or misses a required step.
22
+ - A page promises a feature, provider, adapter, or integration the code no longer supports.
23
+ - A new user-facing capability shipped but is absent from the appropriate docs page.
24
+ - A link points at moved, removed, or outdated primary documentation.
25
+ - A changelog or release page needs an entry for shipped user-facing behavior.
26
+
27
+ Skip the edit when the only available change is subjective polish, wording preference, duplicated information, speculative future work, or behavior still hidden behind a feature flag.
28
+
29
+ ## Blume editing guidance
30
+
31
+ - Keep frontmatter short and factual; use `title` and `description` consistently with nearby pages. Blume's frontmatter schema is **strict** — unknown keys are build errors.
32
+ - Preserve existing page order and `defineMeta` style; update `pages` arrays when adding, renaming, or removing pages.
33
+ - Use the Blume components already present in the docs (callout directives, steps, cards) instead of inventing new markup patterns.
34
+ - Match nearby code fences: filenames, language tags, and line numbers where the surrounding docs use them.
35
+ - Keep internal links root-relative (`/docs/...`).
36
+ - Do not edit generated `.blume/` or `dist/` output.
37
+
38
+ ## PR notes
39
+
40
+ Include these sections in the PR body or no-op summary:
41
+
42
+ - Sources checked
43
+ - Docs changed
44
+ - Verification run (commands and results)
45
+ - Skipped checks, with reasons
46
+ - Remaining risk or follow-up
@@ -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
+ };
@@ -26,16 +26,77 @@ export interface AskData {
26
26
  /** Documents retrieved per question and injected into the system prompt. */
27
27
  const MAX_RESULTS = 6;
28
28
  /** Characters kept per injected excerpt. */
29
- const EXCERPT_CHARS = 1500;
29
+ const EXCERPT_CHARS = 2000;
30
30
  /** Overall cap on injected documentation characters. */
31
- const CONTEXT_BUDGET = 8000;
31
+ const CONTEXT_BUDGET = 10_000;
32
+ /** Chars of lead-in kept before the matched region, for heading/sentence context. */
33
+ const EXCERPT_LEAD = 160;
34
+
35
+ /**
36
+ * Common words dropped from the retrieval query before locating the relevant
37
+ * excerpt region, so short filler ("how does…", "what is…") doesn't drag the
38
+ * window toward incidental matches instead of the meaningful terms.
39
+ */
40
+ const STOPWORDS = new Set([
41
+ "about",
42
+ "and",
43
+ "are",
44
+ "as",
45
+ "at",
46
+ "be",
47
+ "but",
48
+ "by",
49
+ "can",
50
+ "do",
51
+ "does",
52
+ "for",
53
+ "from",
54
+ "how",
55
+ "in",
56
+ "into",
57
+ "is",
58
+ "it",
59
+ "its",
60
+ "my",
61
+ "of",
62
+ "on",
63
+ "or",
64
+ "our",
65
+ "that",
66
+ "the",
67
+ "these",
68
+ "this",
69
+ "those",
70
+ "to",
71
+ "use",
72
+ "used",
73
+ "using",
74
+ "was",
75
+ "were",
76
+ "what",
77
+ "when",
78
+ "where",
79
+ "which",
80
+ "who",
81
+ "why",
82
+ "with",
83
+ "you",
84
+ "your",
85
+ ]);
86
+
87
+ /** Distinct, meaningful lowercase terms from a query (drops stopwords). */
88
+ const queryTerms = (query: string): string[] =>
89
+ [...new Set(query.toLowerCase().match(/[a-z0-9]+/gu))].filter(
90
+ (term) => term.length >= 2 && !STOPWORDS.has(term)
91
+ );
32
92
 
33
93
  /**
34
94
  * The grounding preamble. The model is told to answer strictly from the injected
35
- * excerpts and to cite the pages it used, so answers stay tied to the docs.
95
+ * excerpts and to cite the pages it used as Markdown links (each excerpt is
96
+ * headed by `## Title (/route)`), so citations render as real links in the panel.
36
97
  */
37
98
  const BASE_INSTRUCTION =
38
- "You are a helpful documentation assistant for this project. Answer the user's question using ONLY the documentation excerpts below. If the answer is not covered by them, say you don't know and suggest where in the docs to look — do not invent details. Cite the page titles you drew from.";
99
+ "You are a helpful documentation assistant for this project. Answer the user's question using ONLY the documentation excerpts below. Each excerpt is headed by its page as `## Page Title (/route)`. If the answer is not covered by the excerpts, say you don't know and suggest where in the docs to look — do not invent details. Always cite the pages you drew from, and write every citation as a Markdown link to that page using its route, e.g. [Page Title](/route).";
39
100
 
40
101
  /** Normalize a page path to a document `route` (`/`, `/a/b`, no trailing slash). */
41
102
  const normalizeRoute = (input: string): string => {
@@ -55,10 +116,68 @@ const lastUserMessage = (messages: AskMessage[]): string => {
55
116
  return "";
56
117
  };
57
118
 
58
- /** Trim a document body to `max` characters, marking truncation with an ellipsis. */
59
- const excerpt = (content: string, max: number): string => {
119
+ /**
120
+ * Excerpt the region of `content` most relevant to `query`, not just its head.
121
+ *
122
+ * Pages are indexed whole (one document each), so a naive head slice of a long
123
+ * page returns its intro and misses sections below the fold — the exact failure
124
+ * where "How does Ask AI work?" retrieves the right page but only sees its
125
+ * opening paragraph. This centers the window on the densest cluster of query
126
+ * terms so the injected text is the part that actually answers the question.
127
+ */
128
+ const relevantExcerpt = (
129
+ content: string,
130
+ query: string,
131
+ max: number
132
+ ): string => {
60
133
  const trimmed = content.trim();
61
- return trimmed.length > max ? `${trimmed.slice(0, max)}…` : trimmed;
134
+ if (trimmed.length <= max) {
135
+ return trimmed;
136
+ }
137
+ const withEllipsis = (start: number): string => {
138
+ const slice = trimmed.slice(start, start + max).trim();
139
+ const prefix = start > 0 ? "…" : "";
140
+ const suffix = start + max < trimmed.length ? "…" : "";
141
+ return `${prefix}${slice}${suffix}`;
142
+ };
143
+
144
+ const lower = trimmed.toLowerCase();
145
+ const positions: number[] = [];
146
+ for (const term of queryTerms(query)) {
147
+ let idx = lower.indexOf(term);
148
+ while (idx !== -1) {
149
+ positions.push(idx);
150
+ idx = lower.indexOf(term, idx + term.length);
151
+ }
152
+ }
153
+ // No query terms hit this doc — nothing to center on, so keep the head.
154
+ if (positions.length === 0) {
155
+ return withEllipsis(0);
156
+ }
157
+
158
+ // Pick the term hit whose following `max`-char window covers the most hits.
159
+ // `positions` is non-empty here, so the first window (count ≥ 1) always wins
160
+ // over the initial 0 and assigns a real offset to `best`.
161
+ positions.sort((a, b) => a - b);
162
+ let best = 0;
163
+ let bestCount = 0;
164
+ for (const start of positions) {
165
+ const end = start + max;
166
+ let count = 0;
167
+ for (const pos of positions) {
168
+ if (pos >= end) {
169
+ break;
170
+ }
171
+ if (pos >= start) {
172
+ count += 1;
173
+ }
174
+ }
175
+ if (count > bestCount) {
176
+ bestCount = count;
177
+ best = start;
178
+ }
179
+ }
180
+ return withEllipsis(Math.max(0, best - EXCERPT_LEAD));
62
181
  };
63
182
 
64
183
  /**
@@ -111,7 +230,11 @@ export const createAskContext = (
111
230
  return;
112
231
  }
113
232
  seen.add(doc.route);
114
- const body = excerpt(doc.content, Math.min(EXCERPT_CHARS, budget));
233
+ const body = relevantExcerpt(
234
+ doc.content,
235
+ query,
236
+ Math.min(EXCERPT_CHARS, budget)
237
+ );
115
238
  budget -= body.length;
116
239
  sections.push(`## ${doc.title} (${doc.route})${label}\n${body}`);
117
240
  };
@@ -6,10 +6,13 @@ import type { AskData } from "./ask-context.ts";
6
6
  * Build the grounding snapshot the Ask AI endpoint serves. Like the MCP server,
7
7
  * Ask AI is independent of on-page search, so documents are indexed even when the
8
8
  * search provider is `none` (`includeWhenDisabled`). `locale` is kept (unlike the
9
- * MCP snapshot) so retrieval can be filtered to the current page's language.
9
+ * MCP snapshot) so retrieval can be filtered to the current page's language, and
10
+ * content is kept as Markdown so grounding sees fenced code examples — the model
11
+ * answers "what does the config look like?" from the docs instead of declining.
10
12
  */
11
13
  export const buildAskData = async (project: BlumeProject): Promise<AskData> => {
12
14
  const documents = await buildSearchDocuments(project, {
15
+ content: "markdown",
13
16
  includeWhenDisabled: true,
14
17
  });
15
18
  return {
@@ -432,11 +432,16 @@ const resolveLogo = (project: BlumeProject): BlumeLogo | null => {
432
432
  if (!logo) {
433
433
  return null;
434
434
  }
435
- const config = typeof logo === "string" ? { light: logo } : logo;
436
- const light = config.light ?? config.dark;
437
- const dark = config.dark ?? config.light;
438
- const alt = config.alt ?? "";
439
- const href = config.href ?? "/";
435
+ const config = typeof logo === "string" ? { image: logo } : logo;
436
+ // `text` is passed through verbatim: `undefined` lets the brand fall back to
437
+ // the site title, `""` renders the mark alone (a logo with the wordmark baked
438
+ // in).
439
+ const { href, image: source, text } = config;
440
+ const image = typeof source === "string" ? { light: source } : source;
441
+ const light = image?.light ?? image?.dark;
442
+ const dark = image?.dark ?? image?.light;
443
+ const alt = image?.alt ?? "";
444
+ const brandHref = href ?? "/";
440
445
 
441
446
  if (light && light === dark && light.toLowerCase().endsWith(".svg")) {
442
447
  const rel = light.replace(/^\//u, "");
@@ -445,10 +450,10 @@ const resolveLogo = (project: BlumeProject): BlumeLogo | null => {
445
450
  join(project.context.root, rel),
446
451
  ].find((path) => existsSync(path));
447
452
  if (file) {
448
- return { alt, href, svg: readFileSync(file, "utf-8") };
453
+ return { alt, href: brandHref, svg: readFileSync(file, "utf-8"), text };
449
454
  }
450
455
  }
451
- return { alt, dark, href, light };
456
+ return { alt, dark, href: brandHref, light, text };
452
457
  };
453
458
 
454
459
  /**
@@ -615,7 +620,7 @@ export const buildRuntimeData = (project: BlumeProject): string => {
615
620
  code,
616
621
  withReferenceTabs(
617
622
  graph.navigationByLocale[code] ?? {
618
- chromeVariants: [],
623
+ featured: [],
619
624
  selectors: [],
620
625
  sidebar: [],
621
626
  tabs: [],
@@ -629,6 +634,9 @@ export const buildRuntimeData = (project: BlumeProject): string => {
629
634
  config: {
630
635
  analytics: config.analytics ?? null,
631
636
  appleIcon: resolveAppleIcon(project),
637
+ ask: config.ai.ask?.enabled
638
+ ? { suggestions: config.ai.ask.suggestions }
639
+ : null,
632
640
  banner: resolveBanner(config),
633
641
  codeWrap: config.markdown.code.wrap,
634
642
  description: config.description,
@@ -648,7 +656,6 @@ export const buildRuntimeData = (project: BlumeProject): string => {
648
656
  })),
649
657
  }
650
658
  : null,
651
- icons: config.icons,
652
659
  imageZoom: config.markdown.imageZoom,
653
660
  logo: resolveLogo(project),
654
661
  mcp: config.mcp.enabled
@@ -974,9 +981,9 @@ export const generateRuntime = async (
974
981
  const staged = collectStaged(project);
975
982
  const hasStaged = staged.size > 0;
976
983
  // Only emit a project-scanning `docs` collection when a filesystem source
977
- // actually feeds it. Bridge mode has just the staged Mintlify source, so the
978
- // `docs` glob would otherwise scan (and watch) the whole project root for
979
- // nothing — see contentConfigTemplate.
984
+ // actually feeds it. An all-staged project (openapi/notion/…) has only staged
985
+ // sources, so the `docs` glob would otherwise scan (and watch) the whole
986
+ // project root for nothing — see contentConfigTemplate.
980
987
  const hasFilesystemSource = project.sources.some((source) => !source.staged);
981
988
 
982
989
  const structural = await Promise.all([
@@ -1,13 +1,10 @@
1
1
  import type { IncomingMessage, ServerResponse } from "node:http";
2
- import { fileURLToPath } from "node:url";
3
2
 
4
3
  import type { AstroIntegration } from "astro";
5
4
 
6
- import type { AssetMount } from "../core/assets.ts";
7
5
  import { enrichDiagnostic } from "../core/diagnostics.ts";
8
6
  import type { Diagnostic } from "../core/types.ts";
9
7
  import { markdownVariantUrl, prefersMarkdown } from "./markdown-negotiation.ts";
10
- import { copyAssetMounts, serveAssetMounts } from "./static-assets.ts";
11
8
 
12
9
  /** The dev server's HMR channel — either `.ws` (Vite ≤5) or `.hot` (Vite 6+). */
13
10
  interface OverlayChannel {
@@ -77,11 +74,6 @@ export interface BlumeIntegrationOptions {
77
74
  contentRoutes: string[];
78
75
  /** Configured `deployment.base`, stripped from dev URLs before matching. */
79
76
  base?: string;
80
- /**
81
- * `content.assets` mounts: top-level dirs served at the site root in dev and
82
- * copied into the build output, so root-served assets need no relocation.
83
- */
84
- assets?: AssetMount[];
85
77
  }
86
78
 
87
79
  /**
@@ -118,12 +110,6 @@ export const blumeIntegration = (
118
110
  options: BlumeIntegrationOptions
119
111
  ): AstroIntegration => ({
120
112
  hooks: {
121
- "astro:build:done": async ({ dir }) => {
122
- // Copy in-place asset mounts into the build output, mirroring publicDir.
123
- if (options.assets?.length) {
124
- await copyAssetMounts(options.assets, fileURLToPath(dir));
125
- }
126
- },
127
113
  "astro:config:setup": ({ injectRoute }) => {
128
114
  for (const page of options.pages) {
129
115
  injectRoute({
@@ -143,13 +129,6 @@ export const blumeIntegration = (
143
129
  handle: negotiateMarkdown(new Set(options.contentRoutes), options.base),
144
130
  route: "",
145
131
  });
146
- // Serve `content.assets` mounts (Astro only serves publicDir in dev).
147
- if (options.assets?.length) {
148
- server.middlewares.stack.unshift({
149
- handle: serveAssetMounts(options.assets),
150
- route: "",
151
- });
152
- }
153
132
  },
154
133
  },
155
134
  name: "blume",