blume 1.7.2 → 1.7.3

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 (101) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/dist/cli/chunk-0qymqwzz.js +164 -0
  3. package/dist/cli/chunk-0qymqwzz.js.map +15 -0
  4. package/dist/cli/{chunk-9bkjd11x.js → chunk-0xjyb285.js} +1 -1
  5. package/dist/cli/{chunk-hdm2dkd2.js → chunk-1jefwnfs.js} +13 -7
  6. package/dist/cli/{chunk-hdm2dkd2.js.map → chunk-1jefwnfs.js.map} +3 -3
  7. package/dist/cli/{chunk-n9sra6sy.js → chunk-3r45185y.js} +5 -7
  8. package/dist/cli/{chunk-n9sra6sy.js.map → chunk-3r45185y.js.map} +2 -2
  9. package/dist/cli/{chunk-cvky9gb2.js → chunk-4x36ddpw.js} +3 -3
  10. package/dist/cli/{chunk-mqb2ka8m.js → chunk-5093q3n7.js} +12 -12
  11. package/dist/cli/{chunk-9he6crym.js → chunk-5g0w1e2c.js} +4 -4
  12. package/dist/cli/{chunk-mt76t7dj.js → chunk-5qk08vmp.js} +11 -11
  13. package/dist/cli/{chunk-61j18dwk.js → chunk-7s8hm3b6.js} +7 -2
  14. package/dist/cli/{chunk-61j18dwk.js.map → chunk-7s8hm3b6.js.map} +3 -3
  15. package/dist/cli/{chunk-196vjxp9.js → chunk-8cjtbafj.js} +11 -9
  16. package/dist/cli/{chunk-196vjxp9.js.map → chunk-8cjtbafj.js.map} +2 -2
  17. package/dist/cli/{chunk-aztttvb3.js → chunk-97r59kpr.js} +4 -4
  18. package/dist/cli/{chunk-t3tj0dgr.js → chunk-ahnw3kxw.js} +8 -8
  19. package/dist/cli/{chunk-hs3gbh8p.js → chunk-b27xqwn9.js} +3 -3
  20. package/dist/cli/{chunk-450a7rcr.js → chunk-bf6bt1xt.js} +2 -2
  21. package/dist/cli/{chunk-eevwt1sc.js → chunk-bvwwhd84.js} +13 -13
  22. package/dist/cli/{chunk-12dxjqk7.js → chunk-cjtn640a.js} +17 -17
  23. package/dist/cli/{chunk-12dxjqk7.js.map → chunk-cjtn640a.js.map} +1 -1
  24. package/dist/cli/{chunk-ra1v2nc2.js → chunk-ct47dqpx.js} +14 -3
  25. package/dist/cli/{chunk-ra1v2nc2.js.map → chunk-ct47dqpx.js.map} +4 -3
  26. package/dist/cli/{chunk-ppfvdcd4.js → chunk-dwgcp5sm.js} +1 -1
  27. package/dist/cli/{chunk-3w7b2vcx.js → chunk-e7f42gdj.js} +2 -2
  28. package/dist/cli/{chunk-vkrsvbr5.js → chunk-esphfr8p.js} +8 -8
  29. package/dist/cli/{chunk-vkrsvbr5.js.map → chunk-esphfr8p.js.map} +1 -1
  30. package/dist/cli/{chunk-fmceyezb.js → chunk-ex56aa81.js} +27 -18
  31. package/dist/cli/chunk-ex56aa81.js.map +13 -0
  32. package/dist/cli/{chunk-jbj4qhfw.js → chunk-garjf5z9.js} +2 -2
  33. package/dist/cli/{chunk-wjt80jps.js → chunk-js7saxwm.js} +14 -18
  34. package/dist/cli/{chunk-wjt80jps.js.map → chunk-js7saxwm.js.map} +4 -6
  35. package/dist/cli/{chunk-5n7t497w.js → chunk-k79xp7av.js} +53 -91
  36. package/dist/cli/chunk-k79xp7av.js.map +39 -0
  37. package/dist/cli/{chunk-688e0dde.js → chunk-nn13znc2.js} +1 -1
  38. package/dist/cli/{chunk-ejjx8znq.js → chunk-ps4m1xh4.js} +15 -9
  39. package/dist/cli/{chunk-ejjx8znq.js.map → chunk-ps4m1xh4.js.map} +3 -3
  40. package/dist/cli/{chunk-xhtpx3ff.js → chunk-rqy0s5wh.js} +13 -13
  41. package/dist/cli/{chunk-30e87n55.js → chunk-rz9jmfhz.js} +4 -4
  42. package/dist/cli/{chunk-exeeb35e.js → chunk-vacwm2hv.js} +2 -2
  43. package/dist/cli/{chunk-8cd8tj54.js → chunk-yg63d42r.js} +7 -7
  44. package/dist/cli/index.js +15 -15
  45. package/dist/types/core/config-input.d.ts +33 -0
  46. package/dist/types/core/data.d.ts +2 -0
  47. package/dist/types/core/schema.d.ts +20 -0
  48. package/dist/types/core/types.d.ts +5 -0
  49. package/docs/configuration/ask-ai.mdx +14 -0
  50. package/docs/configuration/index.mdx +3 -1
  51. package/docs/content/navigation.mdx +3 -0
  52. package/docs/content/syntax.mdx +10 -0
  53. package/docs/discoverability/agent-discovery.mdx +82 -1
  54. package/docs/discoverability/index.mdx +1 -1
  55. package/docs/discoverability/llms-txt.mdx +1 -1
  56. package/docs/reference/frontmatter.mdx +2 -0
  57. package/package.json +1 -1
  58. package/src/ai/agent-readability.ts +5 -0
  59. package/src/ai/ai-catalog.ts +241 -0
  60. package/src/ai/link-headers.ts +12 -0
  61. package/src/ai/llms.ts +6 -0
  62. package/src/ai/mcp/discovery.ts +1 -1
  63. package/src/astro/generate.ts +2 -0
  64. package/src/cli/commands/build.ts +3 -1
  65. package/src/components/islands/hooks.ts +50 -1
  66. package/src/components/layout/RootLayout.astro +24 -1
  67. package/src/components/layout/analytics-client.ts +36 -7
  68. package/src/core/config-input.ts +34 -0
  69. package/src/core/data.ts +2 -0
  70. package/src/core/navigation.ts +22 -3
  71. package/src/core/schema.ts +32 -0
  72. package/src/core/types.ts +5 -0
  73. package/src/deploy/artifacts.ts +12 -1
  74. package/src/deploy/headers.ts +6 -0
  75. package/src/deploy/vercel-negotiation.ts +25 -2
  76. package/src/search/build.ts +25 -3
  77. package/src/theme/entry.ts +23 -0
  78. package/dist/cli/chunk-5n7t497w.js.map +0 -40
  79. package/dist/cli/chunk-88by27n5.js +0 -17
  80. package/dist/cli/chunk-88by27n5.js.map +0 -10
  81. package/dist/cli/chunk-fmceyezb.js.map +0 -13
  82. package/dist/cli/chunk-tqa1s0k8.js +0 -69
  83. package/dist/cli/chunk-tqa1s0k8.js.map +0 -11
  84. /package/dist/cli/{chunk-9bkjd11x.js.map → chunk-0xjyb285.js.map} +0 -0
  85. /package/dist/cli/{chunk-cvky9gb2.js.map → chunk-4x36ddpw.js.map} +0 -0
  86. /package/dist/cli/{chunk-mqb2ka8m.js.map → chunk-5093q3n7.js.map} +0 -0
  87. /package/dist/cli/{chunk-9he6crym.js.map → chunk-5g0w1e2c.js.map} +0 -0
  88. /package/dist/cli/{chunk-mt76t7dj.js.map → chunk-5qk08vmp.js.map} +0 -0
  89. /package/dist/cli/{chunk-aztttvb3.js.map → chunk-97r59kpr.js.map} +0 -0
  90. /package/dist/cli/{chunk-t3tj0dgr.js.map → chunk-ahnw3kxw.js.map} +0 -0
  91. /package/dist/cli/{chunk-hs3gbh8p.js.map → chunk-b27xqwn9.js.map} +0 -0
  92. /package/dist/cli/{chunk-450a7rcr.js.map → chunk-bf6bt1xt.js.map} +0 -0
  93. /package/dist/cli/{chunk-eevwt1sc.js.map → chunk-bvwwhd84.js.map} +0 -0
  94. /package/dist/cli/{chunk-ppfvdcd4.js.map → chunk-dwgcp5sm.js.map} +0 -0
  95. /package/dist/cli/{chunk-3w7b2vcx.js.map → chunk-e7f42gdj.js.map} +0 -0
  96. /package/dist/cli/{chunk-jbj4qhfw.js.map → chunk-garjf5z9.js.map} +0 -0
  97. /package/dist/cli/{chunk-688e0dde.js.map → chunk-nn13znc2.js.map} +0 -0
  98. /package/dist/cli/{chunk-xhtpx3ff.js.map → chunk-rqy0s5wh.js.map} +0 -0
  99. /package/dist/cli/{chunk-30e87n55.js.map → chunk-rz9jmfhz.js.map} +0 -0
  100. /package/dist/cli/{chunk-exeeb35e.js.map → chunk-vacwm2hv.js.map} +0 -0
  101. /package/dist/cli/{chunk-8cd8tj54.js.map → chunk-yg63d42r.js.map} +0 -0
@@ -869,6 +869,23 @@ const llmsTxtObjectSchema = z.strictObject({
869
869
 
870
870
  type LlmsTxtResolved = z.output<typeof llmsTxtObjectSchema>;
871
871
 
872
+ /** The object form of `ai.catalog`; a bare boolean normalizes onto it. */
873
+ const aiCatalogObjectSchema = z.strictObject({
874
+ enabled: z.boolean().default(true),
875
+ /**
876
+ * Representative queries per catalog entry, keyed by the entry's
877
+ * `<namespace>:<name>` (the identifier minus its `urn:air:<host>:` prefix,
878
+ * e.g. `mcp:docs`, `skill:blume`, `api:docs`). Replaces the generated
879
+ * defaults for that entry; 2–5 short natural-language questions the
880
+ * resource can answer, which registries embed for semantic search.
881
+ */
882
+ queries: z
883
+ .record(z.string().min(1), z.array(z.string().trim().min(1)).min(1))
884
+ .default({}),
885
+ });
886
+
887
+ type AiCatalogResolved = z.output<typeof aiCatalogObjectSchema>;
888
+
872
889
  const aiConfigSchema = z.strictObject({
873
890
  /**
874
891
  * The JSON docs API: the page index, per-page JSON, and navigation under
@@ -983,6 +1000,21 @@ const aiConfigSchema = z.strictObject({
983
1000
  }
984
1001
  })
985
1002
  .optional(),
1003
+ /**
1004
+ * The AI Catalog / ARD manifest at `/.well-known/ai-catalog.json` (mirrored
1005
+ * at `/.well-known/ard.json`): one entry per agent-facing resource the site
1006
+ * publishes — the MCP server card, each agent skill, the JSON docs API's
1007
+ * OpenAPI document, each rendered API reference, and llms.txt — so agent
1008
+ * registries can index the site from its domain alone. Needs a
1009
+ * `deployment.site` (identifiers are domain-anchored URNs). On by default;
1010
+ * the object form overrides the generated representative queries.
1011
+ */
1012
+ catalog: z
1013
+ .union([z.boolean(), aiCatalogObjectSchema])
1014
+ .default(true)
1015
+ .transform((value): AiCatalogResolved =>
1016
+ isBoolean(value) ? { enabled: value, queries: {} } : value
1017
+ ),
986
1018
  /**
987
1019
  * `llms.txt`/`llms-full.txt` emission. A bare boolean toggles it; the object
988
1020
  * form adds `openapi: false` to keep generated API reference pages out of
package/src/core/types.ts CHANGED
@@ -220,6 +220,11 @@ export type NavNode =
220
220
  */
221
221
  display: SidebarDisplay;
222
222
  icon?: string;
223
+ /**
224
+ * The group row's link: an explicit-config group's `root`, or the
225
+ * generated folder's index page route. Absent when there is no page at
226
+ * the group's own path, so the row never links to a 404.
227
+ */
223
228
  route?: string;
224
229
  /**
225
230
  * The group's URL path (its folder route prefix), even when the folder
@@ -4,6 +4,11 @@ import { mkdir, readFile, writeFile } from "node:fs/promises";
4
4
  import { dirname, join, resolve } from "pathe";
5
5
 
6
6
  import { buildAgentReadability } from "../ai/agent-readability.ts";
7
+ import {
8
+ AI_CATALOG_PATH,
9
+ ARD_MANIFEST_PATH,
10
+ buildAiCatalog,
11
+ } from "../ai/ai-catalog.ts";
7
12
  import { API_CATALOG_PATH, buildApiCatalog } from "../ai/api-catalog.ts";
8
13
  import { buildHomeLinkHeader } from "../ai/link-headers.ts";
9
14
  import { buildLlmsFiles } from "../ai/llms.ts";
@@ -234,8 +239,12 @@ const emitAgentSkills = async (
234
239
  const emitWellKnownFiles = async (
235
240
  config: ResolvedConfig,
236
241
  distDir: string,
242
+ skills: readonly SkillArtifact[],
237
243
  logger: ArtifactLogger
238
244
  ): Promise<void> => {
245
+ // One document, two paths: the ai-catalog spec's well-known URI and the
246
+ // ARD v0.91 one (see `ai/ai-catalog.ts`).
247
+ const aiCatalog = buildAiCatalog(config, skills);
239
248
  const files = [
240
249
  {
241
250
  content: buildSignaturesDirectory(config),
@@ -247,6 +256,8 @@ const emitWellKnownFiles = async (
247
256
  label: "RFC 9727",
248
257
  path: API_CATALOG_PATH,
249
258
  },
259
+ { content: aiCatalog, label: "AI Catalog", path: AI_CATALOG_PATH },
260
+ { content: aiCatalog, label: "ARD manifest", path: ARD_MANIFEST_PATH },
250
261
  ];
251
262
  for (const file of files) {
252
263
  const target = join(distDir, file.path.slice(1));
@@ -362,7 +373,7 @@ export const publishBuildArtifacts = async (
362
373
  logger.info("Generated agent-readability.json");
363
374
  }
364
375
 
365
- await emitWellKnownFiles(project.config, distDir, logger);
376
+ await emitWellKnownFiles(project.config, distDir, skills, logger);
366
377
  await emitAgentSkills(project, distDir, skills, logger);
367
378
 
368
379
  await emitRedirectFiles(project.config, distDir, logger);
@@ -1,3 +1,4 @@
1
+ import { crossOriginDiscoveryPaths } from "../ai/ai-catalog.ts";
1
2
  import {
2
3
  API_CATALOG_PATH,
3
4
  API_CATALOG_TYPE,
@@ -95,6 +96,11 @@ export const buildNetlifyHeaders = (
95
96
  `${deployBase}${SIGNATURES_DIRECTORY_PATH}\n Content-Type: ${SIGNATURES_DIRECTORY_TYPE}`
96
97
  );
97
98
  }
99
+ // Agent registries fetch the discovery documents cross-origin; a static
100
+ // host sends no CORS header unless told to.
101
+ for (const path of crossOriginDiscoveryPaths(config)) {
102
+ rules.push(`${deployBase}${path}\n Access-Control-Allow-Origin: *`);
103
+ }
98
104
  // Published skills live at the deployment base, outside `basePath` — the
99
105
  // `.md` charset rule above misses them whenever a basePath is set, and the
100
106
  // RFC wants archives served as application/gzip explicitly.
@@ -235,6 +235,26 @@ export const buildNegotiationRoutes = (
235
235
  /** The `src` of the injected homepage `Link` header route. */
236
236
  const HOME_SRC = "^/$";
237
237
 
238
+ const ALLOW_ANY_ORIGIN = "*";
239
+
240
+ /**
241
+ * A main-phase `continue` route stamping `Access-Control-Allow-Origin: *` on
242
+ * one static discovery document (see `crossOriginDiscoveryPaths`), so a
243
+ * registry reading it from another origin isn't refused by the browser.
244
+ */
245
+ const corsRoute = (path: string): VercelRoute => ({
246
+ continue: true,
247
+ headers: { "access-control-allow-origin": ALLOW_ANY_ORIGIN },
248
+ src: `^${routePattern(path)}$`,
249
+ });
250
+
251
+ /** Whether a route is a `corsRoute` — the same three-field shape test as the others. */
252
+ const isCorsRoute = (route: VercelRoute): boolean =>
253
+ route.continue === true &&
254
+ route.headers?.["access-control-allow-origin"] === ALLOW_ANY_ORIGIN &&
255
+ isString(route.src) &&
256
+ Object.keys(route).length === 3;
257
+
238
258
  /**
239
259
  * Whether a route is one this module previously injected, so re-injection
240
260
  * replaces rather than duplicates. Rewrites are identified by their `accept`
@@ -258,7 +278,8 @@ const isNegotiationRoute = (route: VercelRoute): boolean =>
258
278
  (route.continue === true &&
259
279
  isString(route.headers?.link) &&
260
280
  route.src === HOME_SRC &&
261
- Object.keys(route).length === 3);
281
+ Object.keys(route).length === 3) ||
282
+ isCorsRoute(route);
262
283
 
263
284
  /**
264
285
  * Splice the negotiation routes into a Build Output `config.json`, plus — when
@@ -287,7 +308,8 @@ export const injectNegotiationRoutes = (
287
308
  homeLinkHeader?: string | null,
288
309
  contentTypeOverrides?: Record<string, string>,
289
310
  homeTokens?: number,
290
- notFound: NotFoundVariants = {}
311
+ notFound: NotFoundVariants = {},
312
+ corsPaths: readonly string[] = []
291
313
  ): string | null => {
292
314
  const overrideEntries = Object.entries(contentTypeOverrides ?? {});
293
315
  let config: {
@@ -325,6 +347,7 @@ export const injectNegotiationRoutes = (
325
347
  src: HOME_SRC,
326
348
  });
327
349
  }
350
+ headerRoutes.push(...corsPaths.map(corsRoute));
328
351
  // Headers first: `continue` routes accumulate, so a request the rewrite
329
352
  // route then terminates (Markdown negotiation on the homepage) still carries
330
353
  // the Link header.
@@ -1,4 +1,6 @@
1
- import { join } from "pathe";
1
+ import { mkdir, writeFile } from "node:fs/promises";
2
+
3
+ import { dirname, join } from "pathe";
2
4
 
3
5
  /**
4
6
  * Build a local Pagefind search index over the built site. Pagefind indexes
@@ -6,6 +8,15 @@ import { join } from "pathe";
6
8
  * `data-pagefind-ignore`, which Blume stamps on non-indexable pages
7
9
  * (search-excluded, or hidden without the opt-in), so those stay out.
8
10
  *
11
+ * The index files are fetched with `getFiles()` and written here rather than
12
+ * with Pagefind's `writeFiles()`: in service mode Pagefind acknowledges
13
+ * `writeFiles()` once tokio has buffered the writes, not once they are on
14
+ * disk, and `close()` kills the backend, so a loaded CI host can ship a
15
+ * truncated `pagefind-entry.json` (Pagefind/pagefind#1271). Writing the bytes
16
+ * ourselves puts them on disk before the backend goes away. Fixed upstream in
17
+ * Pagefind/pagefind#1272; switch back to `writeFiles()` once a release ships
18
+ * that fix.
19
+ *
9
20
  * Returns the number of pages indexed.
10
21
  */
11
22
  export const buildSearchIndex = async (outDir: string): Promise<number> => {
@@ -17,11 +28,22 @@ export const buildSearchIndex = async (outDir: string): Promise<number> => {
17
28
  }
18
29
 
19
30
  // These awaits are strictly ordered, not independent: the directory must be
20
- // indexed before its files are written, and the index closed only after.
31
+ // indexed before its files are read, and the index closed only after.
21
32
  // oxlint-disable-next-line react-doctor/async-parallel
22
33
  const result = await index.addDirectory({ path: outDir });
23
- await index.writeFiles({ outputPath: join(outDir, "pagefind") });
34
+ const { files } = await index.getFiles();
24
35
  await pagefind.close();
25
36
 
37
+ const searchDir = join(outDir, "pagefind");
38
+ const directories = new Set(
39
+ files.map((file) => dirname(join(searchDir, file.path)))
40
+ );
41
+ await Promise.all(
42
+ [...directories].map((directory) => mkdir(directory, { recursive: true }))
43
+ );
44
+ await Promise.all(
45
+ files.map((file) => writeFile(join(searchDir, file.path), file.content))
46
+ );
47
+
26
48
  return result.page_count;
27
49
  };
@@ -298,6 +298,11 @@ ${THEME_MAPPING}
298
298
  --tw-prose-pre-bg: var(--blume-code-background);
299
299
  --tw-prose-th-borders: var(--blume-border);
300
300
  --tw-prose-td-borders: var(--blume-border);
301
+ /* Neutralized: the plugin defaults are near-black text and shadow with no
302
+ dark counterpart (Blume never applies prose-invert), and the kbd rule
303
+ below draws the badge with Blume tokens instead. */
304
+ --tw-prose-kbd: inherit;
305
+ --tw-prose-kbd-shadows: transparent;
301
306
  color: var(--blume-muted-foreground);
302
307
  font-size: 0.875rem;
303
308
  line-height: 1.7;
@@ -827,6 +832,24 @@ pre:has(.line.focused):hover .line:not(.focused) {
827
832
  content: none;
828
833
  }
829
834
 
835
+ /* Keyboard keys (<kbd>) render as the same bordered badge the search dialog
836
+ uses for its shortcut hints. Typography's own kbd rule is scoped with
837
+ :not(.not-prose *), so it never reaches a key inside <Steps>, <Callout>,
838
+ <Card>, or any other not-prose component; this rule is deliberately
839
+ unscoped from that exclusion so a key looks the same everywhere in the
840
+ content column. Unlayered, so it outranks the plugin's layered rule. */
841
+ .prose kbd {
842
+ background: var(--blume-muted);
843
+ border: 1px solid var(--blume-border);
844
+ border-radius: 0.25rem;
845
+ box-shadow: none;
846
+ color: inherit;
847
+ font-family: var(--font-mono);
848
+ font-size: 0.85em;
849
+ font-weight: 500;
850
+ padding: 0.125rem 0.25rem;
851
+ }
852
+
830
853
  /* Inline code highlighting: Shiki colors the tokens of a \`code\`{:lang} snippet
831
854
  via the same dual-theme CSS variables as fenced blocks, keeping the inline
832
855
  pill background. Always on — it only fires on the trailing {:lang} marker. */