blume 1.5.3 → 1.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.
- package/CHANGELOG.md +94 -0
- package/dist/cli/index.js +3949 -1403
- package/dist/cli/index.js.map +111 -96
- package/dist/types/ai/component-markdown.d.ts +79 -0
- package/dist/types/components/layout/nav-utils.d.ts +60 -0
- package/dist/types/core/base-path.d.ts +9 -0
- package/dist/types/core/config-input.d.ts +206 -4
- package/dist/types/core/config.d.ts +6 -4
- package/dist/types/core/data.d.ts +33 -2
- package/dist/types/core/github.d.ts +35 -0
- package/dist/types/core/i18n-ui.d.ts +10 -0
- package/dist/types/core/navigation.d.ts +69 -0
- package/dist/types/core/schema.d.ts +122 -1
- package/dist/types/core/sources/types.d.ts +31 -6
- package/dist/types/core/types.d.ts +29 -2
- package/dist/types/markdown/features.d.ts +21 -0
- package/dist/types/openapi/references.d.ts +26 -1
- package/dist/types/seo/jsonld.d.ts +105 -0
- package/dist/types/theme/fonts.d.ts +34 -4
- package/docs/07-faq.mdx +9 -9
- package/docs/_snippets/include-demo.mdx +7 -0
- package/docs/advanced/api-reference.mdx +13 -4
- package/docs/advanced/custom-pages.mdx +4 -2
- package/docs/advanced/graphql.mdx +84 -0
- package/docs/advanced/meta.ts +8 -1
- package/docs/configuration/ai.mdx +25 -3
- package/docs/configuration/index.mdx +24 -0
- package/docs/configuration/search.mdx +13 -1
- package/docs/configuration/seo.mdx +30 -3
- package/docs/configuration/theming.mdx +23 -0
- package/docs/content/components.mdx +15 -1
- package/docs/content/includes.mdx +68 -0
- package/docs/content/meta.ts +1 -0
- package/docs/content/navigation.mdx +25 -0
- package/docs/content/sources.mdx +42 -1
- package/docs/content/syntax.mdx +69 -1
- package/docs/content/versioning.mdx +15 -9
- package/docs/reference/cli.mdx +2 -1
- package/package.json +66 -57
- package/skills/blume-migrate/SKILL.md +16 -7
- package/skills/blume-migrate/references/docusaurus.md +5 -3
- package/skills/blume-migrate/references/fumadocs.md +10 -2
- package/skills/blume-migrate/references/mintlify.md +3 -2
- package/skills/blume-migrate/references/nextra.md +2 -2
- package/skills/blume-migrate/references/starlight.md +1 -1
- package/src/ai/agent-readability.ts +2 -1
- package/src/ai/ask-data.ts +2 -1
- package/src/ai/component-markdown.ts +199 -36
- package/src/ai/llms.ts +93 -6
- package/src/ai/markdown.ts +2 -2
- package/src/ai/mcp/discovery.ts +10 -2
- package/src/ai/mcp/server.ts +74 -2
- package/src/astro/examples.ts +29 -2
- package/src/astro/generate.ts +282 -177
- package/src/astro/include-hmr.ts +81 -0
- package/src/astro/include-refresh.ts +0 -0
- package/src/astro/index.ts +10 -5
- package/src/astro/markdown-negotiation.ts +1 -1
- package/src/astro/runtime-modules.ts +196 -0
- package/src/astro/templates.ts +365 -113
- package/src/cli/commands/build.ts +91 -16
- package/src/cli/commands/dev.ts +6 -3
- package/src/cli/host-args.ts +18 -0
- package/src/cli/index.ts +2 -1
- package/src/cli/init/questions.ts +1 -0
- package/src/cli/init/scaffold.ts +27 -4
- package/src/components/colors.ts +142 -0
- package/src/components/content/Badge.astro +5 -12
- package/src/components/content/Callout.astro +19 -36
- package/src/components/content/Card.astro +15 -21
- package/src/components/content/Component.astro +10 -1
- package/src/components/content/GithubInfo.astro +28 -9
- package/src/components/content/Tabs.astro +27 -5
- package/src/components/content/github-info.ts +20 -5
- package/src/components/copy-feedback.ts +93 -9
- package/src/components/dropdown-dismiss.ts +122 -0
- package/src/components/islands/ask-ai.tsx +4 -1
- package/src/components/islands/hooks.ts +3 -1
- package/src/components/layout/Fonts.astro +15 -8
- package/src/components/layout/Header.astro +44 -0
- package/src/components/layout/LanguageSwitcher.astro +9 -1
- package/src/components/layout/NavSelector.astro +12 -3
- package/src/components/layout/NavTree.astro +6 -18
- package/src/components/layout/PageActions.astro +54 -22
- package/src/components/layout/PageLayout.astro +2 -0
- package/src/components/layout/ReferenceLayout.astro +6 -1
- package/src/components/layout/RootLayout.astro +42 -15
- package/src/components/layout/Search.astro +36 -4
- package/src/components/layout/TableOfContents.astro +8 -2
- package/src/components/layout/head-scripts.ts +30 -1
- package/src/components/openapi/ApiOverview.astro +13 -3
- package/src/components/openapi/AsyncApiOperation.astro +7 -14
- package/src/components/openapi/GraphqlChip.astro +33 -0
- package/src/components/openapi/GraphqlFieldsTable.astro +111 -0
- package/src/components/openapi/GraphqlOperation.astro +186 -0
- package/src/components/openapi/GraphqlType.astro +154 -0
- package/src/components/openapi/MethodBadge.astro +3 -14
- package/src/components/openapi/Operation.astro +12 -5
- package/src/components/openapi/OperationPanel.astro +43 -0
- package/src/components/openapi/RequestPanel.astro +5 -10
- package/src/components/openapi/Responses.astro +1 -16
- package/src/components/openapi/graphql-helpers.ts +466 -0
- package/src/components/openapi/playground-client.ts +15 -0
- package/src/components/openapi/sample-panels.ts +45 -0
- package/src/components/openapi/snippets.ts +13 -35
- package/src/core/base-path.ts +11 -0
- package/src/core/config-input.ts +209 -2
- package/src/core/config.ts +6 -4
- package/src/core/content-assets.ts +15 -4
- package/src/core/data.ts +28 -3
- package/src/core/define-components.ts +2 -0
- package/src/core/diagnostics.ts +8 -0
- package/src/core/frontmatter.ts +20 -8
- package/src/core/github.ts +71 -0
- package/src/core/graph.ts +22 -8
- package/src/core/heading-markers.ts +96 -0
- package/src/core/i18n-ui.ts +12 -0
- package/src/core/includes.ts +633 -0
- package/src/core/last-modified.ts +36 -11
- package/src/core/links.ts +79 -13
- package/src/core/manifest.ts +10 -0
- package/src/core/meta.ts +2 -1
- package/src/core/nav-diagnostics.ts +11 -2
- package/src/core/navigation.ts +27 -6
- package/src/core/project-graph.ts +61 -9
- package/src/core/schema.ts +235 -36
- package/src/core/server-features.ts +5 -9
- package/src/core/sources/github-releases.ts +2 -2
- package/src/core/sources/normalize.ts +502 -115
- package/src/core/sources/notion.ts +43 -8
- package/src/core/sources/obsidian.ts +1038 -0
- package/src/core/sources/read.ts +36 -1
- package/src/core/sources/resolve.ts +34 -1
- package/src/core/sources/types.ts +28 -6
- package/src/core/sources/watch.ts +12 -8
- package/src/core/tsconfig-aliases.ts +48 -35
- package/src/core/types.ts +31 -2
- package/src/core/ui-packs/ar.ts +2 -0
- package/src/core/ui-packs/bg.ts +3 -0
- package/src/core/ui-packs/bn.ts +2 -0
- package/src/core/ui-packs/ca.ts +3 -0
- package/src/core/ui-packs/cs.ts +2 -0
- package/src/core/ui-packs/da.ts +2 -0
- package/src/core/ui-packs/de.ts +3 -0
- package/src/core/ui-packs/el.ts +3 -0
- package/src/core/ui-packs/es.ts +3 -0
- package/src/core/ui-packs/fa.ts +2 -0
- package/src/core/ui-packs/fi.ts +2 -0
- package/src/core/ui-packs/fr.ts +3 -0
- package/src/core/ui-packs/he.ts +2 -0
- package/src/core/ui-packs/hi.ts +2 -0
- package/src/core/ui-packs/hr.ts +3 -0
- package/src/core/ui-packs/hu.ts +3 -0
- package/src/core/ui-packs/id.ts +3 -0
- package/src/core/ui-packs/it.ts +2 -0
- package/src/core/ui-packs/ja.ts +3 -0
- package/src/core/ui-packs/ko.ts +3 -0
- package/src/core/ui-packs/nl.ts +3 -0
- package/src/core/ui-packs/no.ts +3 -0
- package/src/core/ui-packs/pl.ts +3 -0
- package/src/core/ui-packs/pt-br.ts +3 -0
- package/src/core/ui-packs/pt.ts +3 -0
- package/src/core/ui-packs/ro.ts +3 -0
- package/src/core/ui-packs/ru.ts +3 -0
- package/src/core/ui-packs/sk.ts +2 -0
- package/src/core/ui-packs/sr.ts +2 -0
- package/src/core/ui-packs/sv.ts +3 -0
- package/src/core/ui-packs/th.ts +2 -0
- package/src/core/ui-packs/tr.ts +3 -0
- package/src/core/ui-packs/uk.ts +3 -0
- package/src/core/ui-packs/vi.ts +2 -0
- package/src/core/ui-packs/zh-tw.ts +2 -0
- package/src/core/ui-packs/zh.ts +2 -0
- package/src/core/version-cut.ts +26 -6
- package/src/core/yaml.ts +26 -0
- package/src/deploy/function-bundle.ts +251 -0
- package/src/deploy/vercel-negotiation.ts +49 -6
- package/src/eval/schema.ts +3 -1
- package/src/markdown/code-title.ts +22 -16
- package/src/markdown/features.ts +21 -0
- package/src/markdown/fence-meta.ts +50 -0
- package/src/markdown/heading-anchors.ts +198 -37
- package/src/markdown/include.ts +247 -0
- package/src/markdown/index.ts +43 -34
- package/src/markdown/language-icon.ts +2 -2
- package/src/markdown/mdast.ts +7 -3
- package/src/markdown/ts2js.ts +264 -0
- package/src/og/card.ts +1 -1
- package/src/openapi/asyncapi.ts +4 -1
- package/src/openapi/graphql-build.ts +293 -0
- package/src/openapi/graphql.ts +212 -0
- package/src/openapi/model.ts +38 -5
- package/src/openapi/parse.ts +34 -0
- package/src/openapi/proxy.ts +30 -5
- package/src/openapi/references.ts +97 -13
- package/src/openapi/render-mdx.ts +66 -12
- package/src/openapi/scalar.ts +5 -16
- package/src/openapi/source.ts +91 -23
- package/src/registry/eject.ts +47 -17
- package/src/search/documents.ts +229 -37
- package/src/search/orama-index.ts +9 -5
- package/src/seo/jsonld.ts +293 -51
- package/src/theme/code-block-padding.ts +16 -0
- package/src/theme/entry.ts +67 -13
- package/src/theme/fonts.ts +189 -16
- package/src/theme/sources.ts +49 -0
- package/src/translate/prompts.ts +2 -0
- package/src/translate/run.ts +7 -0
- package/src/translate/work-list.ts +0 -0
|
@@ -18,19 +18,21 @@ Read `themeConfig`, `presets`, and `plugins`:
|
|
|
18
18
|
| `tagline` | `description` |
|
|
19
19
|
| `themeConfig.navbar.title` / `.logo` | `title` / `logo` (move the image into `public/`) |
|
|
20
20
|
| `themeConfig.navbar.items` (doc items) | `navigation.tabs` (for section links) |
|
|
21
|
-
| `themeConfig.navbar.items` (external/utility links — Blog,
|
|
21
|
+
| `themeConfig.navbar.items` (external/utility links — Blog, Discord…) | **`navigation.actions`** (`[{ label, href }]`, plain header links) to keep them in the header, or **`navigation.featured`** (`{ label, href, icon? }`, pinned above the sidebar on every route) if they should survive on phones; the GitHub link → the `github` config instead |
|
|
22
|
+
| `themeConfig.navbar.items` (a `className`-styled button — "Get started", "Sign up") | **`navigation.cta`** (`{ label, href }`, the header's one filled button; a route the docs don't serve must be an absolute URL) |
|
|
22
23
|
| `themeConfig.colorMode.defaultMode` | `theme.mode` (`respectPrefersColorScheme: true` → `"system"`) |
|
|
23
24
|
| `themeConfig.prism.theme` / `.darkTheme` | `markdown.codeBlocks.theme: { light, dark }` (map Prism theme names to Shiki themes, e.g. `github`/`github-dark`) |
|
|
24
25
|
| `themeConfig.metadata` / `themeConfig.image` | per-page `seo` frontmatter / `seo.og`; report what doesn't fit |
|
|
25
26
|
| `url` + `baseUrl` | **`url` → drop** (`deployment.site` auto-detects); `baseUrl` (when not `/`) → `deployment.base` |
|
|
26
27
|
| preset `docs.routeBasePath` — **including the default!** | Docusaurus serves docs at **`/docs/…` by default**; the "map only declared fields" rule does **not** apply here because the _URLs_ are load-bearing. Either keep them with top-level **`basePath: "/docs"`** (invisible to the sidebar), or intentionally move to root and emit a `redirects` entry per page. Decide explicitly and say which. (`routeBasePath: '/'` = docs-only mode — nothing to do.) |
|
|
27
|
-
| preset `docs.editUrl` | `github` (owner/repo/branch; a path after the branch → `github.dir`) |
|
|
28
|
+
| preset `docs.editUrl` | `github` (owner/repo/branch; a path after the branch → `github.dir`; **an origin other than `https://github.com` → `github.host`** — a GitHub Enterprise repo's edit links and header mark point at the public site without it) |
|
|
28
29
|
| `themeConfig.footer` | drop → Footer override (`defineComponents` layout slot) |
|
|
29
30
|
| `themeConfig.announcementBar` | `banner` (`{ content, dismissible, id }` — `isCloseable` → `dismissible`; colors drop) |
|
|
30
31
|
| `i18n.locales` / `defaultLocale` | `i18n` — translated files live at `i18n/<locale>/docusaurus-plugin-content-docs/current/…`; move them to `<locale>/…` under `content.root` |
|
|
31
32
|
| `themeConfig.algolia` | drop — Blume ships built-in search (Orama); remove the Algolia dep |
|
|
32
33
|
| `@docusaurus/plugin-client-redirects` | **static `redirects: [{from, to}]` arrays convert 1:1** to Blume `redirects` (a `from` array = one entry per item); only `createRedirects` functions are truly dynamic → host rules |
|
|
33
34
|
| `@docusaurus/theme-mermaid` | delete the dep — ` ```mermaid ` renders natively (in `.mdx`) |
|
|
35
|
+
| GraphQL doc generators (`@graphql-markdown/docusaurus`, `@edno/docusaurus2-graphql-doc-generator`) | delete the plugin **and its generated pages** — point the top-level `graphql: { enabled: true, spec, endpoint }` at the schema instead (see SKILL.md "GraphQL") |
|
|
34
36
|
| `remark-math` + `rehype-katex` | delete — block `$$…$$` renders in `.mdx` with no config (no `markdown.math` field exists); **inline `$…$` is not supported** — convert or drop (report) |
|
|
35
37
|
| Multi-instance docs plugins (`plugin-content-docs` with `id`) | one folder (and usually one `navigation.tabs` entry) per instance |
|
|
36
38
|
|
|
@@ -42,7 +44,7 @@ Every Docusaurus repo serves **`static/`** at the site root (`static/img/foo.png
|
|
|
42
44
|
|
|
43
45
|
- **Autogenerated sidebar** (`{ type: 'autogenerated', dirName: '...' }`) → Blume's default filesystem navigation. Docusaurus strips numeric prefixes (`01-`) exactly like Blume, so the convention round-trips; no config needed.
|
|
44
46
|
- **Explicit sidebar** (arrays of doc IDs, categories, links) → restructure into folders where possible; use `navigation.sidebar` only for shapes files can't express. A category `{ type: 'category', label, items }` → a folder (label → `meta.ts` `title`); `collapsed` → `meta.ts` `collapsed`, and collapsible rendering → that folder's `meta.ts` `display: "group"` (or `navigation.sidebar.display: "group"` once in config when every category collapses).
|
|
45
|
-
- **`_category_.json`** (also `.yml`) (`{ label, position, collapsed, collapsible, link, className, customProps }`) → a folder `meta.ts`: `label`→`title`, `position`→`order`, `collapsed`→`collapsed`. `link.type: 'generated-index'` → an `index` page in the folder — **and the old URL was `/docs/category/<slug>`**, so add a redirect and rewrite inbound links. `link.type: 'doc'` → make that doc the folder's `index`. `collapsible
|
|
47
|
+
- **`_category_.json`** (also `.yml`) (`{ label, position, collapsed, collapsible, link, className, customProps }`) → a folder `meta.ts`: `label`→`title`, `position`→`order`, `collapsed`→`collapsed`. `link.type: 'generated-index'` → an `index` page in the folder — **and the old URL was `/docs/category/<slug>`**, so add a redirect and rewrite inbound links. `link.type: 'doc'` → make that doc the folder's `index`. `collapsible: false` → that folder's `meta.ts` `display: "flat"`, so it stays a plain heading even under a global `"group"` mode. `className`/`customProps` → drop (report).
|
|
46
48
|
- **`src/pages/` — inventory, don't nuke.** Nearly every repo has a React landing page (`src/pages/index.tsx`) and often extra Markdown pages. Markdown pages → content pages; the React home page → rebuild as a docs index or a custom `.astro` page under `content.pages` — report either way.
|
|
47
49
|
|
|
48
50
|
## Versioned docs
|
|
@@ -37,7 +37,7 @@ Everything else is `defineConfig({ title })`.
|
|
|
37
37
|
| `root: true` | a **`navigation.tabs` entry** (see below) — **not** a `meta.ts` field |
|
|
38
38
|
| `pages: [...]` slugs | `pages: [...]` (ordering) |
|
|
39
39
|
| `description` | **drop** (folders have no description) |
|
|
40
|
-
| `collapsible: false` |
|
|
40
|
+
| `collapsible: false` | that folder's `display: "flat"` (a plain heading, even under a global `"group"` mode) |
|
|
41
41
|
|
|
42
42
|
`meta.ts` accepts **only** `title`, `icon`, `order`, `collapsed`, `pages`, `display`. Render mode is per-folder or global: a folder that needs collapsible rendering sets its own `meta.ts` `display: "group"` (drill-in is `"page"`); when the whole sidebar should collapse, set `navigation.sidebar.display: "group"` once in `blume.config.ts` instead of repeating it per folder.
|
|
43
43
|
|
|
@@ -73,6 +73,10 @@ Fumadocs icons are strings resolved by the repo's own `icon` handler in `loader(
|
|
|
73
73
|
- **No equivalent — report:** `<DynamicCodeBlock>`, `<ImageZoom>` (Blume zooms content images by default), `<InlineTOC>`.
|
|
74
74
|
- **Strip or convert every import** — not just `fumadocs-*`: `lucide-react` imports (icon JSX → string names), `next/image`/`next/link` (→ Markdown image/link), and local components. **Inventory `mdx-components.tsx` before deleting it** — components registered there are used import-free in MDX bodies; port or inline each usage first.
|
|
75
75
|
|
|
76
|
+
## Headings
|
|
77
|
+
|
|
78
|
+
Trailing heading markers — `[#custom-id]` (pinned anchor), `[!toc]` (hide from the TOC), `[toc]` (TOC-only entry) — use the same syntax in Blume. **Pass through unchanged.** One exception: Fumadocs' looser grammar accepts an id containing whitespace (`[#two words]`); Blume does not parse that as a marker, so rewrite such an id to a hyphenated one and update every link that targets it.
|
|
79
|
+
|
|
76
80
|
## Code fences
|
|
77
81
|
|
|
78
82
|
- ` ```npm ` fences (Fumadocs' remark-npm accepts both) → ` ```package-install `.
|
|
@@ -82,13 +86,17 @@ Fumadocs icons are strings resolved by the repo's own `icon` handler in `loader(
|
|
|
82
86
|
|
|
83
87
|
`fumadocs-openapi` writes **generated MDX stubs into the content tree** (`generateFiles()` output: pages containing `<APIPage>`/`<OpenAPIPage>` with `_openapi` frontmatter), plus `lib/openapi.ts` (`createOpenAPI`) and a generate script. Treat these exactly like Mintlify endpoint stubs: **delete the generated pages**, point `openapi: { enabled: true, sources: [{ spec }] }` at the spec (vendor it locally), add a `navigation.tabs` entry for the reference route, and remove `fumadocs-openapi`, `lib/openapi.ts`, and the generate script. Keep hand-written conceptual pages (intro/auth) under the reference route.
|
|
84
88
|
|
|
89
|
+
## GraphQL
|
|
90
|
+
|
|
91
|
+
`@fumadocs/graphql` works differently from the OpenAPI flow: **no stub files** — pages are virtual, served through the Loader API. The artifacts to harvest and tear down: `lib/graphql.ts` (`createGraphQL()` — read its `input` for the schema and any per-source routes/labels), the `graphql.staticSource()`/`graphql.loaderPlugin()` wiring in the source config, the `GraphQLPage` component (`createGraphQLPage()`, usually `components/api-page.tsx` — read its playground `endpoint`), and the `@fumadocs/graphql/css/preset.css` import. Map to the top-level `graphql: { enabled: true, spec, endpoint }` block (see SKILL.md "GraphQL"): SDL files/text and introspection results carry over as `spec` (vendor a URL input locally); a programmatic `GraphQLSchema` instance must be printed to SDL and committed (report); the `createGraphQLPage` playground endpoint becomes `endpoint`. When `createGraphQL()` takes several inputs, or sources carry their own routes, labels, or endpoints, map each one to an entry in `graphql.sources` (`{ spec, route, label, endpoint }`) instead of the single block-level `spec` — otherwise a schema is dropped or mounted on the wrong route. Blume groups routes the same way (`<route>/queries/<field>`, `<route>/objects/<type>`), but slugs are re-derived — rewrite inbound links and let `blume validate` catch strays. Then remove `@fumadocs/graphql`, the two lib/component files, and the CSS import. Since there are no generated pages, there is nothing to delete from the content tree — but keep any hand-written conceptual pages under the reference route.
|
|
92
|
+
|
|
85
93
|
## i18n
|
|
86
94
|
|
|
87
95
|
A `loader({ i18n })` setup (locale-suffixed files or locale dirs) → Blume `i18n: { defaultLocale, locales: [{ code, label }] }`. Locale **directories** match Blume's `dir` parser as-is; locale **file suffixes** (`page.cn.mdx`) need restructuring into locale folders. Report whichever transform you apply.
|
|
88
96
|
|
|
89
97
|
## Package.json & teardown
|
|
90
98
|
|
|
91
|
-
Repoint scripts (`dev`→`blume dev`, `build`→`blume build`, `start`→`blume preview`), remove the `fumadocs
|
|
99
|
+
Repoint scripts (`dev`→`blume dev`, `build`→`blume build`, `start`→`blume preview`), remove the `fumadocs-*`/`@fumadocs/*` deps and the host framework's deps (`next`, `react-router`, `@tanstack/*`…), add `blume`. Safe to delete after harvesting (see above for what to read first): `source.config.*`, `mdx-components.tsx`, the app/route dir, and host-framework config (`next.config.*`, `next-env.d.ts`, the `next` tsconfig plugin — or the React Router/TanStack equivalents).
|
|
92
100
|
|
|
93
101
|
## Dropped — report these
|
|
94
102
|
|
|
@@ -94,7 +94,9 @@ Mintlify's `navigation` object (`tabs`/`anchors`/`dropdowns`/`products`/`version
|
|
|
94
94
|
Call this trade out explicitly in the migration report; don't silently pick one. **The scoping is bidirectional:** on the root/landing route Blume _hides_ every tab folder and lists only untabbed top-level pages — so a Mintlify home whose single sidebar showed everything becomes a lean root list plus one sidebar per tab. That's automatic; don't try to exclude tab folders from the root by hand.
|
|
95
95
|
|
|
96
96
|
- **`dropdowns`/`products`/`versions`** → `navigation.selectors` (`{ kind, label, items: [{ label, path, icon?, description?, tag? }] }`). Use `kind` `dropdown`/`product`/`version` accordingly.
|
|
97
|
-
- **`anchors` / `navigation.global.anchors
|
|
97
|
+
- **`anchors` / `navigation.global.anchors`** (persistent sidebar-top links — Blog, Changelog, Community, Contact/Support) → **`navigation.featured`** (`{ label, href, icon? }`). These pin to the **top of the sidebar, above every section, on every route** (not tab-scoped) — the right home for Mintlify's always-visible utility links. An external `href` opens in a new tab; an internal one (`/contact`) is build-time validated against your pages. Convert the FontAwesome `icon` to Lucide as usual. (This replaces the old "drop and report" for anchors.)
|
|
98
|
+
- **`navbar.links`** (plain header links — Log in, Status, Support) → **`navigation.actions`** (`[{ label, href }]`), rendered in the header left of the icon buttons. A GitHub link → the `github` config (or `navigation.repo` as a URL when the docs repo is private) instead. Header links hide on phones; a link that must survive there belongs in `featured`.
|
|
99
|
+
- **`navbar.primary`** (the header **CTA button** — "Get Started"/"Sign Up", `type: "button"`) → **`navigation.cta`** (`{ label, href }`, singular: Blume renders exactly one filled button). A `type: "github"` primary → the `github` config instead. A route the docs don't serve (the product's own `/signup`) must be an absolute URL, or it raises `BLUME_NAV_MISSING_PAGE`.
|
|
98
100
|
- **`languages`** → `i18n`, not a selector (see below).
|
|
99
101
|
- Only fall back to an explicit `navigation.sidebar` for a shape the filesystem genuinely can't express.
|
|
100
102
|
|
|
@@ -149,7 +151,6 @@ Mintlify serves every top-level dir (e.g. `/images`) at the site root. Blume ser
|
|
|
149
151
|
|
|
150
152
|
## Dropped — report these
|
|
151
153
|
|
|
152
|
-
- **`navbar.primary`** (the prominent header **CTA button**, e.g. "Get Started"/"Sign Up") → no button equivalent; re-add via `navigation.tabs`, a `navigation.featured` link, or a Header override. (Plain `navbar.links` and `anchors` map to `navigation.featured` — see Navigation above, not here.)
|
|
153
154
|
- **`footer.socials`** → suggest the `github` config, or a Footer override.
|
|
154
155
|
- **Per-language banners** (`navigation.languages[].banner`) → no equivalent.
|
|
155
156
|
- **Dynamic redirects** (`:slug*`/`:id` params) → can't be static path-to-path; move to host rules (`_redirects`, `vercel.json`).
|
|
@@ -15,7 +15,7 @@ Nextra (on Next.js) declares navigation and per-page labels in `_meta` files —
|
|
|
15
15
|
|
|
16
16
|
None of it maps automatically — read the config surface for the generation by hand and reconstruct in `blume.config.ts`:
|
|
17
17
|
|
|
18
|
-
- **v2/v3 — `theme.config.*`:** `logo` (JSX — extract text/image) → `logo`; `project.link` → `github` (renders the header repo link); `docsRepositoryBase` → `github` (owner/repo/branch — and any trailing sub-path → `github.dir`); `banner.text`/`banner.key` → `banner.content`/`banner.id` (`dismissible` maps); `primaryHue`/`primarySaturation` → pick an equivalent `theme.accent` color; `footer` → drop (report); `faviconGlyph` → drop (Blume's favicon is a file convention); `useNextSeoProps`/head → per-page `seo` frontmatter or drop.
|
|
18
|
+
- **v2/v3 — `theme.config.*`:** `logo` (JSX — extract text/image) → `logo`; `project.link` → `github` (renders the header repo link); `docsRepositoryBase` → `github` (owner/repo/branch — and any trailing sub-path → `github.dir`; if its origin is not `https://github.com`, set `github.host` to that origin — with `host` omitted, Blume builds the repository and edit links against public GitHub); `banner.text`/`banner.key` → `banner.content`/`banner.id` (`dismissible` maps); `primaryHue`/`primarySaturation` → pick an equivalent `theme.accent` color; `footer` → drop (report); `faviconGlyph` → drop (Blume's favicon is a file convention); `useNextSeoProps`/head → per-page `seo` frontmatter or drop.
|
|
19
19
|
- **v4 — `app/layout.*` props:** the same facts moved: `<Navbar logo projectLink>`, `<Layout docsRepositoryBase editLink sidebar={{…}} toc={{…}}>`, `<Banner>`, `<Footer>`. Map them the same way; sidebar/TOC tuning mostly drops (Blume's `toc` config covers min/max heading levels).
|
|
20
20
|
- **`next.config.*`:** harvest **`redirects()`** — static entries become Blume `redirects: [{ from, to }]`; wildcard/dynamic ones move to host config (report). A `latex: true` flag means math is in play (see Math below). Then delete the file.
|
|
21
21
|
- Root `_meta` entries with **`type: "page"`** → `navigation.tabs` (`{ label, path }`, where `path` is `/` for `index`, else `/<slug>`). `type: "page"` only maps at the **root**.
|
|
@@ -32,7 +32,7 @@ For each `_meta` entry (`key` = slug, value = string title or `{ title, type, di
|
|
|
32
32
|
| `display: "hidden"` | frontmatter `sidebar.hidden: true` |
|
|
33
33
|
| `type: "separator"` | drop → recreate as a `(Group)/` folder / `meta.ts` boundary if needed |
|
|
34
34
|
| `type: "menu"` (navbar dropdown) | drop → recreate via `navigation.selectors` if wanted |
|
|
35
|
-
| `href` (external link) | **`navigation.featured`** (`{ label, href, icon? }` — pinned above the sidebar on every route); only drop deep-nested ones (report) |
|
|
35
|
+
| `href` (external link) | root-level → **`navigation.actions`** (`[{ label, href }]`, plain header links, matching Nextra's navbar placement) or **`navigation.featured`** (`{ label, href, icon? }` — pinned above the sidebar on every route, survives on phones); only drop deep-nested ones (report) |
|
|
36
36
|
| `type: "page"` (subfolder, not root) | drop (only root → tabs) |
|
|
37
37
|
| `theme: { collapsed }` on a folder | `meta.ts` `collapsed` |
|
|
38
38
|
| `theme: { layout: "full" \| sidebar: false \| … }` | drop (report — no per-page layout switches) |
|
|
@@ -22,7 +22,7 @@ Keep content where it is — set `content.root: "src/content/docs"`.
|
|
|
22
22
|
| `logo.replacesTitle` | `logo: { text: "" }` (renders the mark alone) |
|
|
23
23
|
| `favicon` | copy the file into `public/` (drop the config field — Blume auto-detects) |
|
|
24
24
|
| `social` (array of `{ label, icon, href }`; pre-0.33 legacy: `{ github: url }` object) | derive **`github: { owner, repo }`** from the GitHub entry; other socials drop (report) |
|
|
25
|
-
| `editLink.baseUrl` (`…/edit/<branch>/<subdir?>`) | `github: { owner, repo, branch }` — **and any repo sub-path after the branch → `github.dir`** (a docs-in-subfolder repo breaks every edit link without it) |
|
|
25
|
+
| `editLink.baseUrl` (`…/edit/<branch>/<subdir?>`) | `github: { owner, repo, branch }` — **and any repo sub-path after the branch → `github.dir`** (a docs-in-subfolder repo breaks every edit link without it); **an origin other than `https://github.com` → `github.host`** (a GitHub Enterprise repo otherwise links to the public site) |
|
|
26
26
|
| `sidebar` (array) | filesystem nav / `navigation.sidebar` (see below) |
|
|
27
27
|
| `tableOfContents` (`false` or `{ minHeadingLevel, maxHeadingLevel }`) | `toc` — identical shape, 1:1 |
|
|
28
28
|
| `markdown.headingLinks: false` | `markdown.headingAnchors: false` |
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { normalizeBasePath, withBasePath } from "../core/base-path.ts";
|
|
2
|
+
import { repoUrl } from "../core/github.ts";
|
|
2
3
|
import type { BlumeProject } from "../core/project-graph.ts";
|
|
3
4
|
import type { ContentSignalPolicy, ContentSignals } from "../core/schema.ts";
|
|
4
5
|
import { absoluteUrl } from "../core/site-url.ts";
|
|
@@ -175,7 +176,7 @@ export const buildAgentReadability = (
|
|
|
175
176
|
manifest.contentUsage = usage;
|
|
176
177
|
}
|
|
177
178
|
if (config.github) {
|
|
178
|
-
manifest.repository =
|
|
179
|
+
manifest.repository = repoUrl(config.github);
|
|
179
180
|
}
|
|
180
181
|
|
|
181
182
|
return manifest;
|
package/src/ai/ask-data.ts
CHANGED
|
@@ -10,7 +10,8 @@ import type { AskData } from "./ask-context.ts";
|
|
|
10
10
|
* content is kept as Markdown so grounding sees fenced code examples — the model
|
|
11
11
|
* answers "what does the config look like?" from the docs instead of declining.
|
|
12
12
|
* The reader is an AI agent, so `<Visibility>` resolves for the agents audience
|
|
13
|
-
* (web-only content removed, agents-only unwrapped)
|
|
13
|
+
* (web-only content removed, agents-only unwrapped) and components downlevel to
|
|
14
|
+
* Markdown, both matching llms-full.txt.
|
|
14
15
|
*/
|
|
15
16
|
export const buildAskData = async (project: BlumeProject): Promise<AskData> => {
|
|
16
17
|
const documents = await buildSearchDocuments(project, {
|
|
@@ -3,6 +3,7 @@ import { mdxToMdast } from "satteri";
|
|
|
3
3
|
|
|
4
4
|
import { parseYouTubeId } from "../components/content/youtube.ts";
|
|
5
5
|
import type { ExampleLookup } from "../core/types.ts";
|
|
6
|
+
import { MDX_FEATURES } from "../markdown/features.ts";
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* Downlevel Blume's MDX components to plain Markdown for agent-facing output
|
|
@@ -26,7 +27,8 @@ interface Offset {
|
|
|
26
27
|
offset: number;
|
|
27
28
|
}
|
|
28
29
|
|
|
29
|
-
|
|
30
|
+
/** The structural slice of an mdast node the downlevel walk reads. */
|
|
31
|
+
export interface MdastNode {
|
|
30
32
|
attributes?: MdxAttribute[];
|
|
31
33
|
children?: MdastNode[];
|
|
32
34
|
name?: string;
|
|
@@ -80,8 +82,28 @@ export interface ComponentMarkdownChild extends EvaluatedProps {
|
|
|
80
82
|
children: string;
|
|
81
83
|
}
|
|
82
84
|
|
|
85
|
+
/** One direct child of a component — a child component or prose — as Markdown. */
|
|
86
|
+
export interface ComponentMarkdownBlock {
|
|
87
|
+
/**
|
|
88
|
+
* The child downleveled: a serializable component's rendering (through the
|
|
89
|
+
* registry, so a user override of that component applies), or — for prose,
|
|
90
|
+
* a component with no serializer, or one that declined — its source with
|
|
91
|
+
* any serializable descendants downleveled in place.
|
|
92
|
+
*/
|
|
93
|
+
markdown: string;
|
|
94
|
+
/** The JSX name of a child component; `undefined` for prose. */
|
|
95
|
+
name?: string;
|
|
96
|
+
}
|
|
97
|
+
|
|
83
98
|
/** What a serializer receives for one component usage. */
|
|
84
99
|
export interface ComponentMarkdownContext extends EvaluatedProps {
|
|
100
|
+
/**
|
|
101
|
+
* Every direct child of the element in document order, components and
|
|
102
|
+
* prose alike, each as a block of Markdown. For a container that is nothing
|
|
103
|
+
* but its contents — `<CardGroup>` — joining these with blank lines is the
|
|
104
|
+
* whole serializer.
|
|
105
|
+
*/
|
|
106
|
+
childBlocks: () => ComponentMarkdownBlock[];
|
|
85
107
|
/** Direct child components of `name`, each with evaluated props and body. */
|
|
86
108
|
childComponents: (name: string) => ComponentMarkdownChild[];
|
|
87
109
|
/** The element's body, downleveled and dedented (empty if self-closing). */
|
|
@@ -160,9 +182,10 @@ const readProps = (
|
|
|
160
182
|
return { lossy, props };
|
|
161
183
|
};
|
|
162
184
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
185
|
+
/** A node Satteri stamped with byte offsets. */
|
|
186
|
+
type Positioned = MdastNode & { position: { end: Offset; start: Offset } };
|
|
187
|
+
|
|
188
|
+
const hasOffsets = (node: MdastNode): node is Positioned =>
|
|
166
189
|
typeof node.position?.start?.offset === "number" &&
|
|
167
190
|
typeof node.position?.end?.offset === "number";
|
|
168
191
|
|
|
@@ -352,6 +375,80 @@ const tabs: ComponentMarkdown = ({ childComponents, children }) => {
|
|
|
352
375
|
.join("\n\n");
|
|
353
376
|
};
|
|
354
377
|
|
|
378
|
+
/** Escape the brackets that would end a link's text early. */
|
|
379
|
+
const linkText = (value: string): string =>
|
|
380
|
+
value.replaceAll(/[[\]]/gu, String.raw`\$&`);
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* A link destination. Whitespace ends one, and a `)` ends one unless it is
|
|
384
|
+
* part of a balanced pair — so an href carrying either goes in the angle
|
|
385
|
+
* bracket form, where only `<` and `>` are special.
|
|
386
|
+
*/
|
|
387
|
+
const linkDestination = (href: string): string =>
|
|
388
|
+
/[\s()<>]/u.test(href)
|
|
389
|
+
? `<${href.replaceAll(/[<>]/gu, String.raw`\$&`)}>`
|
|
390
|
+
: href;
|
|
391
|
+
|
|
392
|
+
/** A text prop: a string, or a number stringified — `title={2024}` is a title. */
|
|
393
|
+
const textProp = (value: EvaluatedValue): string => {
|
|
394
|
+
if (isString(value)) {
|
|
395
|
+
return value.trim();
|
|
396
|
+
}
|
|
397
|
+
return isNumber(value) ? String(value) : "";
|
|
398
|
+
};
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* A card is a link with a blurb, so that is what it becomes: the title as the
|
|
402
|
+
* link text, the body under it, and the call to action last — the order the
|
|
403
|
+
* card renders in. Shaped like {@link tabs}, a bold label over the body,
|
|
404
|
+
* rather than a heading: a card sits inside a page whose outline its author
|
|
405
|
+
* wrote, and a heading would add a level to it.
|
|
406
|
+
*
|
|
407
|
+
* The icon and image are presentation and drop out. A card with no title
|
|
408
|
+
* falls back to its `href`, and one with neither is just its body. `lossy`
|
|
409
|
+
* declines: a title or href recovered from an expression that would not
|
|
410
|
+
* evaluate is a link pointing somewhere wrong, which is worse than the
|
|
411
|
+
* visible JSX.
|
|
412
|
+
*/
|
|
413
|
+
const card: ComponentMarkdown = ({ children, lossy, props }) => {
|
|
414
|
+
if (lossy) {
|
|
415
|
+
return null;
|
|
416
|
+
}
|
|
417
|
+
const title = textProp(props.title);
|
|
418
|
+
const href = isString(props.href) ? props.href.trim() : "";
|
|
419
|
+
const body = children.trim();
|
|
420
|
+
const cta = textProp(props.cta);
|
|
421
|
+
const label = title || href;
|
|
422
|
+
if (label === "") {
|
|
423
|
+
// Nothing to head the card with, so it is whatever text it carries.
|
|
424
|
+
const rest = [body, cta].filter(Boolean).join("\n\n");
|
|
425
|
+
// Nothing but presentation left — keep the JSX rather than delete a card.
|
|
426
|
+
return rest === "" ? null : rest;
|
|
427
|
+
}
|
|
428
|
+
const head =
|
|
429
|
+
href === ""
|
|
430
|
+
? `**${label}**`
|
|
431
|
+
: `**[${linkText(label)}](${linkDestination(href)})**`;
|
|
432
|
+
return [head, body, cta].filter(Boolean).join("\n\n");
|
|
433
|
+
};
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* `CardGroup` is the grid its cards sit in and carries no meaning of its own,
|
|
437
|
+
* so it becomes its contents: every direct child in order, each a block, a
|
|
438
|
+
* blank line between them. Built on `childBlocks` rather than extracting the
|
|
439
|
+
* cards so nothing the group holds is lost — a `Card` renders through the
|
|
440
|
+
* registry (a user override of `Card` applies in here too), a nested group
|
|
441
|
+
* recurses, prose between the cards stays, and a card that declines keeps its
|
|
442
|
+
* JSX as a block of its own instead of vanishing beside a rendered sibling.
|
|
443
|
+
* Passing the body slice through instead would leave each card's source
|
|
444
|
+
* indentation in the output and run one card's title straight on from the
|
|
445
|
+
* previous card's body as the same paragraph.
|
|
446
|
+
*/
|
|
447
|
+
const cardGroup: ComponentMarkdown = ({ childBlocks }) =>
|
|
448
|
+
childBlocks()
|
|
449
|
+
.map((block) => block.markdown)
|
|
450
|
+
.join("\n\n");
|
|
451
|
+
|
|
355
452
|
const youtube: ComponentMarkdown = ({ props }) => {
|
|
356
453
|
let input = "";
|
|
357
454
|
if (isString(props.id)) {
|
|
@@ -407,6 +504,8 @@ export const exampleComponentSerializers = (examples: ExampleLookup) =>
|
|
|
407
504
|
*/
|
|
408
505
|
const SERIALIZERS = {
|
|
409
506
|
Callout: callout,
|
|
507
|
+
Card: card,
|
|
508
|
+
CardGroup: cardGroup,
|
|
410
509
|
Steps: steps,
|
|
411
510
|
Tabs: tabs,
|
|
412
511
|
TypeTable: typeTable,
|
|
@@ -428,29 +527,37 @@ const componentHint = (registry: Record<string, ComponentMarkdown>): RegExp =>
|
|
|
428
527
|
const BUILT_IN_HINT = componentHint(SERIALIZERS);
|
|
429
528
|
|
|
430
529
|
/** One downlevel pass's inputs: the source, registry, and page metadata. */
|
|
431
|
-
|
|
530
|
+
/** What a downlevel walk needs: the serializers, the page's front matter, and the source its nodes were parsed from. */
|
|
531
|
+
export interface DownlevelWalk {
|
|
432
532
|
frontmatter: Record<string, EvaluatedValue> | undefined;
|
|
433
533
|
registry: Record<string, ComponentMarkdown>;
|
|
434
534
|
source: string;
|
|
435
535
|
}
|
|
536
|
+
type Walk = DownlevelWalk;
|
|
537
|
+
|
|
538
|
+
/** The serializer registry for a site: built-ins under a user's `ai.markdownComponents`. */
|
|
539
|
+
export const componentRegistry = (
|
|
540
|
+
components?: Record<string, ComponentMarkdown>
|
|
541
|
+
): Record<string, ComponentMarkdown> =>
|
|
542
|
+
components && Object.keys(components).length > 0
|
|
543
|
+
? { ...SERIALIZERS, ...components }
|
|
544
|
+
: SERIALIZERS;
|
|
436
545
|
|
|
437
546
|
/**
|
|
438
|
-
*
|
|
439
|
-
*
|
|
440
|
-
* Mutually recursive with {@link collectSplices} (
|
|
441
|
-
*
|
|
547
|
+
* A source slice as Markdown: the verbatim `[start, end)` range with any
|
|
548
|
+
* serializable component under `nodes` downleveled in place, dedented and
|
|
549
|
+
* trimmed. Mutually recursive with {@link collectSplices} (the nodes may hold
|
|
550
|
+
* further serializable components), hence the forward reference.
|
|
442
551
|
*/
|
|
443
|
-
const
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
const start = first.position.start.offset;
|
|
450
|
-
const end = children.at(-1)?.position.end.offset ?? start;
|
|
552
|
+
const renderSlice = (
|
|
553
|
+
walk: Walk,
|
|
554
|
+
start: number,
|
|
555
|
+
end: number,
|
|
556
|
+
nodes: MdastNode[]
|
|
557
|
+
): string => {
|
|
451
558
|
const splices: Splice[] = [];
|
|
452
559
|
// oxlint-disable-next-line no-use-before-define
|
|
453
|
-
collectSplices(walk,
|
|
560
|
+
collectSplices(walk, nodes, splices);
|
|
454
561
|
const spliced = applySplices(
|
|
455
562
|
walk.source.slice(start, end),
|
|
456
563
|
splices.map((splice) => ({
|
|
@@ -462,6 +569,45 @@ const renderChildren = (walk: Walk, node: MdastNode): string => {
|
|
|
462
569
|
return dedent(spliced).trim();
|
|
463
570
|
};
|
|
464
571
|
|
|
572
|
+
/** The element's body as Markdown: the slice covering all of its children. */
|
|
573
|
+
const renderChildren = (walk: Walk, node: MdastNode): string => {
|
|
574
|
+
const children = (node.children ?? []).filter(hasOffsets);
|
|
575
|
+
const [first] = children;
|
|
576
|
+
if (!first) {
|
|
577
|
+
return "";
|
|
578
|
+
}
|
|
579
|
+
const start = first.position.start.offset;
|
|
580
|
+
return renderSlice(
|
|
581
|
+
walk,
|
|
582
|
+
start,
|
|
583
|
+
children.at(-1)?.position.end.offset ?? start,
|
|
584
|
+
children
|
|
585
|
+
);
|
|
586
|
+
};
|
|
587
|
+
|
|
588
|
+
/**
|
|
589
|
+
* One direct child as a block of Markdown: a registered component's own
|
|
590
|
+
* rendering, else — prose, an unknown component, or one that declined — its
|
|
591
|
+
* source slice, the same choice {@link collectSplices} makes at the top level.
|
|
592
|
+
*/
|
|
593
|
+
const renderBlock = (walk: Walk, node: Positioned): string => {
|
|
594
|
+
const serializer =
|
|
595
|
+
isJsxElement(node) && node.name ? walk.registry[node.name] : undefined;
|
|
596
|
+
const text = serializer
|
|
597
|
+
? // oxlint-disable-next-line no-use-before-define
|
|
598
|
+
serializeElement(serializer, walk, node)
|
|
599
|
+
: null;
|
|
600
|
+
return (
|
|
601
|
+
text ??
|
|
602
|
+
renderSlice(
|
|
603
|
+
walk,
|
|
604
|
+
node.position.start.offset,
|
|
605
|
+
node.position.end.offset,
|
|
606
|
+
node.children ?? []
|
|
607
|
+
)
|
|
608
|
+
);
|
|
609
|
+
};
|
|
610
|
+
|
|
465
611
|
/** Serialize one component usage, or `null` to keep its JSX verbatim. */
|
|
466
612
|
const serializeElement = (
|
|
467
613
|
serializer: ComponentMarkdown,
|
|
@@ -470,6 +616,11 @@ const serializeElement = (
|
|
|
470
616
|
): string | null =>
|
|
471
617
|
serializer({
|
|
472
618
|
...readProps(node, walk.frontmatter),
|
|
619
|
+
childBlocks: () =>
|
|
620
|
+
(node.children ?? []).filter(hasOffsets).map((child) => ({
|
|
621
|
+
markdown: renderBlock(walk, child),
|
|
622
|
+
name: isJsxElement(child) ? child.name : undefined,
|
|
623
|
+
})),
|
|
473
624
|
childComponents: (name) =>
|
|
474
625
|
(node.children ?? [])
|
|
475
626
|
.filter((child) => isJsxElement(child) && child.name === name)
|
|
@@ -487,26 +638,38 @@ const serializeElement = (
|
|
|
487
638
|
* doesn't descend into it; when a serializer declines, the walk continues
|
|
488
639
|
* inside so nested serializable components still convert.
|
|
489
640
|
*/
|
|
641
|
+
/**
|
|
642
|
+
* Downlevel one parsed component to the Markdown its serializer emits, or
|
|
643
|
+
* `null` when the node isn't a component, has no serializer, or its
|
|
644
|
+
* serializer declines. `walk.source` must be the text `node` was parsed from.
|
|
645
|
+
*/
|
|
646
|
+
export const downlevelComponentNode = (
|
|
647
|
+
node: MdastNode,
|
|
648
|
+
walk: DownlevelWalk
|
|
649
|
+
): string | null => {
|
|
650
|
+
const serializer =
|
|
651
|
+
node.type === "mdxJsxFlowElement" && node.name
|
|
652
|
+
? walk.registry[node.name]
|
|
653
|
+
: undefined;
|
|
654
|
+
return serializer && hasOffsets(node)
|
|
655
|
+
? serializeElement(serializer, walk, node)
|
|
656
|
+
: null;
|
|
657
|
+
};
|
|
658
|
+
|
|
490
659
|
const collectSplices = (
|
|
491
660
|
walk: Walk,
|
|
492
661
|
nodes: MdastNode[],
|
|
493
662
|
out: Splice[]
|
|
494
663
|
): void => {
|
|
495
664
|
for (const node of nodes) {
|
|
496
|
-
const
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
:
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
end: node.position.end.offset,
|
|
505
|
-
start: node.position.start.offset,
|
|
506
|
-
text,
|
|
507
|
-
});
|
|
508
|
-
continue;
|
|
509
|
-
}
|
|
665
|
+
const text = hasOffsets(node) ? downlevelComponentNode(node, walk) : null;
|
|
666
|
+
if (text !== null && hasOffsets(node)) {
|
|
667
|
+
out.push({
|
|
668
|
+
end: node.position.end.offset,
|
|
669
|
+
start: node.position.start.offset,
|
|
670
|
+
text,
|
|
671
|
+
});
|
|
672
|
+
continue;
|
|
510
673
|
}
|
|
511
674
|
collectSplices(walk, node.children ?? [], out);
|
|
512
675
|
}
|
|
@@ -531,9 +694,9 @@ export const downlevelComponents = (
|
|
|
531
694
|
components?: Record<string, ComponentMarkdown>,
|
|
532
695
|
frontmatter?: Record<string, EvaluatedValue>
|
|
533
696
|
): string => {
|
|
534
|
-
const
|
|
535
|
-
const
|
|
536
|
-
|
|
697
|
+
const registry = componentRegistry(components);
|
|
698
|
+
const hint =
|
|
699
|
+
registry === SERIALIZERS ? BUILT_IN_HINT : componentHint(registry);
|
|
537
700
|
if (!hint.test(source)) {
|
|
538
701
|
return source;
|
|
539
702
|
}
|
|
@@ -541,7 +704,7 @@ export const downlevelComponents = (
|
|
|
541
704
|
try {
|
|
542
705
|
// SAFETY: MdastNode is a structural subset of Satteri's mdast output —
|
|
543
706
|
// every node carries `type`, and the walk reads only optional fields.
|
|
544
|
-
tree = mdxToMdast(source) as MdastNode;
|
|
707
|
+
tree = mdxToMdast(source, { features: MDX_FEATURES }) as MdastNode;
|
|
545
708
|
} catch {
|
|
546
709
|
return source;
|
|
547
710
|
}
|
package/src/ai/llms.ts
CHANGED
|
@@ -3,13 +3,16 @@ import { rewriteRelativeImages } from "../core/content-assets.ts";
|
|
|
3
3
|
import matter from "../core/frontmatter.ts";
|
|
4
4
|
import type { BlumeProject } from "../core/project-graph.ts";
|
|
5
5
|
import { absoluteUrl } from "../core/site-url.ts";
|
|
6
|
-
import {
|
|
6
|
+
import { readExpandedEntryText } from "../core/sources/read.ts";
|
|
7
7
|
import type { NavNode, Navigation, PageRecord } from "../core/types.ts";
|
|
8
8
|
import { buildRssFeeds } from "../deploy/rss.ts";
|
|
9
|
+
import { API_CATALOG_PATH, hasApiCatalog } from "./api-catalog.ts";
|
|
9
10
|
import {
|
|
10
11
|
downlevelComponents,
|
|
11
12
|
exampleComponentSerializers,
|
|
12
13
|
} from "./component-markdown.ts";
|
|
14
|
+
import { AGENT_SKILLS_DIR, AGENT_SKILLS_INDEX_PATH } from "./skills.ts";
|
|
15
|
+
import type { SkillArtifact } from "./skills.ts";
|
|
13
16
|
import { applyAgentVisibility } from "./visibility.ts";
|
|
14
17
|
|
|
15
18
|
// Routes carry `basePath`; a `deployment.base` subdirectory is layered on top —
|
|
@@ -21,6 +24,65 @@ const pageUrl = (route: string, site?: string, base = ""): string => {
|
|
|
21
24
|
return encodeURI(site ? absoluteUrl(site, path) : path);
|
|
22
25
|
};
|
|
23
26
|
|
|
27
|
+
/** Inputs only the build has: the published skills, collected once per build. */
|
|
28
|
+
export interface LlmsIndexOptions {
|
|
29
|
+
/**
|
|
30
|
+
* The Agent Skills the build publishes under `/.well-known/agent-skills/`,
|
|
31
|
+
* listed in their own section with each skill's description — which is
|
|
32
|
+
* where a skill says when to use it. Absent (empty) for the homepage
|
|
33
|
+
* Markdown mirror, which is synthesized before the skills are collected.
|
|
34
|
+
*/
|
|
35
|
+
skills?: readonly SkillArtifact[];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** A skill description on one line — SKILL.md frontmatter may wrap it. */
|
|
39
|
+
const oneLine = (text: string): string => text.replaceAll(/\s+/gu, " ").trim();
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* The "Agent resources" section: every machine-readable artifact the site
|
|
43
|
+
* publishes, so an agent that only reads llms.txt still finds the full
|
|
44
|
+
* Markdown dump, the per-page Markdown mirrors, the MCP server, the skills
|
|
45
|
+
* index, the API catalog, the readability manifest, and the sitemap. The
|
|
46
|
+
* same artifact set `agent-readability.json` indexes, in prose an agent can
|
|
47
|
+
* act on without a second fetch.
|
|
48
|
+
*/
|
|
49
|
+
const agentResourceLines = (project: BlumeProject): string[] => {
|
|
50
|
+
const { config } = project;
|
|
51
|
+
const { site } = config.deployment;
|
|
52
|
+
const url = (path: string): string =>
|
|
53
|
+
pageUrl(path, site, normalizeBasePath(config.deployment.base));
|
|
54
|
+
const lines = [
|
|
55
|
+
`- [llms-full.txt](${url("/llms-full.txt")}): The full Markdown of every page in one file.`,
|
|
56
|
+
`- [Page Markdown](${url("/index.md")}): Append \`.md\` to any page URL to fetch that page as raw Markdown.`,
|
|
57
|
+
];
|
|
58
|
+
if (config.ai.mcp.enabled) {
|
|
59
|
+
lines.push(
|
|
60
|
+
`- [MCP server](${url(config.ai.mcp.route)}): Streamable HTTP Model Context Protocol server with search_docs, get_page, list_pages, and get_navigation tools, plus every page as a resource. Discovery document: ${url("/.well-known/mcp.json")}`
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
if (config.ai.skills) {
|
|
64
|
+
lines.push(
|
|
65
|
+
`- [Agent skills](${url(AGENT_SKILLS_INDEX_PATH)}): Agent Skills discovery index of the skills this site publishes.`
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
if (hasApiCatalog(config)) {
|
|
69
|
+
lines.push(
|
|
70
|
+
`- [API catalog](${url(API_CATALOG_PATH)}): RFC 9727 linkset of the APIs documented here.`
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
if (config.seo.agentReadability) {
|
|
74
|
+
lines.push(
|
|
75
|
+
`- [agent-readability.json](${url("/agent-readability.json")}): Manifest of every agent-facing artifact on this site.`
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
if (site && config.seo.sitemap) {
|
|
79
|
+
lines.push(
|
|
80
|
+
`- [Sitemap](${url("/sitemap.xml")}): Every indexable page URL with its last-modified date.`
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
return lines;
|
|
84
|
+
};
|
|
85
|
+
|
|
24
86
|
// Drafts, hidden, and ordinary `noindex` pages are excluded. Generated API
|
|
25
87
|
// references keep crawler visibility (`noindex`) separate from LLM visibility
|
|
26
88
|
// (`ai.exclude`), and are excluded wholesale when `ai.llmsTxt.openapi` is off.
|
|
@@ -88,7 +150,10 @@ const indexedNavigations = (
|
|
|
88
150
|
* Also serves as the homepage's synthesized Markdown mirror when the home
|
|
89
151
|
* route is a landing page (see `buildRawMarkdown`).
|
|
90
152
|
*/
|
|
91
|
-
export const buildLlmsIndex = (
|
|
153
|
+
export const buildLlmsIndex = (
|
|
154
|
+
project: BlumeProject,
|
|
155
|
+
options: LlmsIndexOptions = {}
|
|
156
|
+
): string => {
|
|
92
157
|
const { config } = project;
|
|
93
158
|
const { site } = config.deployment;
|
|
94
159
|
const base = normalizeBasePath(config.deployment.base);
|
|
@@ -187,10 +252,31 @@ export const buildLlmsIndex = (project: BlumeProject): string => {
|
|
|
187
252
|
);
|
|
188
253
|
}
|
|
189
254
|
|
|
255
|
+
// The published skills, each with its description — the one place a skill
|
|
256
|
+
// states when an agent should reach for it.
|
|
257
|
+
const skills = options.skills ?? [];
|
|
258
|
+
if (skills.length > 0) {
|
|
259
|
+
blocks.push(
|
|
260
|
+
"## Agent skills",
|
|
261
|
+
skills
|
|
262
|
+
.map(
|
|
263
|
+
(skill) =>
|
|
264
|
+
`- [${skill.name}](${pageUrl(`${AGENT_SKILLS_DIR}/${skill.path}`, site, base)}): ${oneLine(skill.description)}`
|
|
265
|
+
)
|
|
266
|
+
.join("\n")
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
blocks.push("## Agent resources", agentResourceLines(project).join("\n"));
|
|
271
|
+
|
|
190
272
|
const header = config.description
|
|
191
273
|
? `# ${config.title}\n\n> ${config.description}`
|
|
192
274
|
: `# ${config.title}`;
|
|
193
|
-
|
|
275
|
+
// `details` is the llms.txt spec's free-form block between the summary and
|
|
276
|
+
// the file sections — "when to use this" guidance in the site's own words.
|
|
277
|
+
const { details } = config.ai.llmsTxt;
|
|
278
|
+
const lead = details ? `${header}\n\n${details}` : header;
|
|
279
|
+
return `${[lead, ...blocks].join("\n\n")}\n`;
|
|
194
280
|
};
|
|
195
281
|
|
|
196
282
|
/** Build `llms-full.txt`: the full Markdown body of every current-docs page. */
|
|
@@ -208,7 +294,7 @@ const buildFull = async (project: BlumeProject): Promise<string> => {
|
|
|
208
294
|
|
|
209
295
|
const sections = await Promise.all(
|
|
210
296
|
pages.map(async (page) => {
|
|
211
|
-
let raw = await
|
|
297
|
+
let raw = await readExpandedEntryText(project, page);
|
|
212
298
|
// Colocated `./image.png` references resolve to nothing for a reader of
|
|
213
299
|
// llms-full.txt; point them at the served originals instead.
|
|
214
300
|
if (page.sourcePath) {
|
|
@@ -246,8 +332,9 @@ const buildFull = async (project: BlumeProject): Promise<string> => {
|
|
|
246
332
|
|
|
247
333
|
/** Build both LLM text artifacts for a project. */
|
|
248
334
|
export const buildLlmsFiles = async (
|
|
249
|
-
project: BlumeProject
|
|
335
|
+
project: BlumeProject,
|
|
336
|
+
options: LlmsIndexOptions = {}
|
|
250
337
|
): Promise<{ index: string; full: string }> => ({
|
|
251
338
|
full: await buildFull(project),
|
|
252
|
-
index: buildLlmsIndex(project),
|
|
339
|
+
index: buildLlmsIndex(project, options),
|
|
253
340
|
});
|