blume 1.7.1 → 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 (138) hide show
  1. package/CHANGELOG.md +18 -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-8gnpdsn1.js → chunk-0xjyb285.js} +2 -2
  5. package/dist/cli/{chunk-12dzsn9b.js → chunk-1jefwnfs.js} +82 -81
  6. package/dist/cli/{chunk-12dzsn9b.js.map → chunk-1jefwnfs.js.map} +3 -3
  7. package/dist/cli/{chunk-27gtm2ym.js → chunk-2mzebbbz.js} +1 -1
  8. package/dist/cli/{chunk-s5e5jt53.js → chunk-2z47ypj8.js} +1 -1
  9. package/dist/cli/{chunk-jtb45atp.js → chunk-3r45185y.js} +10 -12
  10. package/dist/cli/{chunk-jtb45atp.js.map → chunk-3r45185y.js.map} +2 -2
  11. package/dist/cli/{chunk-6mq7qkve.js → chunk-4x36ddpw.js} +18 -24
  12. package/dist/cli/{chunk-6mq7qkve.js.map → chunk-4x36ddpw.js.map} +2 -2
  13. package/dist/cli/{chunk-he2zfgah.js → chunk-5093q3n7.js} +22 -30
  14. package/dist/cli/{chunk-he2zfgah.js.map → chunk-5093q3n7.js.map} +2 -2
  15. package/dist/cli/{chunk-k0v1f8bb.js → chunk-5g0w1e2c.js} +21 -28
  16. package/dist/cli/{chunk-k0v1f8bb.js.map → chunk-5g0w1e2c.js.map} +2 -2
  17. package/dist/cli/{chunk-5gfw0q4j.js → chunk-5qk08vmp.js} +26 -34
  18. package/dist/cli/{chunk-5gfw0q4j.js.map → chunk-5qk08vmp.js.map} +2 -2
  19. package/dist/cli/{chunk-r99hynxh.js → chunk-7s8hm3b6.js} +41 -9
  20. package/dist/cli/{chunk-r99hynxh.js.map → chunk-7s8hm3b6.js.map} +3 -3
  21. package/dist/cli/{chunk-vyqj481z.js → chunk-8cjtbafj.js} +68 -66
  22. package/dist/cli/chunk-8cjtbafj.js.map +13 -0
  23. package/dist/cli/{chunk-aqjvpd03.js → chunk-97r59kpr.js} +27 -33
  24. package/dist/cli/{chunk-aqjvpd03.js.map → chunk-97r59kpr.js.map} +2 -2
  25. package/dist/cli/{chunk-np8dmfb0.js → chunk-ahnw3kxw.js} +26 -33
  26. package/dist/cli/{chunk-np8dmfb0.js.map → chunk-ahnw3kxw.js.map} +2 -2
  27. package/dist/cli/{chunk-j5f2wrj5.js → chunk-b27xqwn9.js} +10 -15
  28. package/dist/cli/{chunk-j5f2wrj5.js.map → chunk-b27xqwn9.js.map} +2 -2
  29. package/dist/cli/{chunk-kmx2mydj.js → chunk-bf6bt1xt.js} +8 -8
  30. package/dist/cli/{chunk-kmx2mydj.js.map → chunk-bf6bt1xt.js.map} +1 -1
  31. package/dist/cli/{chunk-90pdhkpm.js → chunk-bvwwhd84.js} +23 -32
  32. package/dist/cli/{chunk-90pdhkpm.js.map → chunk-bvwwhd84.js.map} +2 -2
  33. package/dist/cli/{chunk-mfm4sjwx.js → chunk-cjtn640a.js} +32 -43
  34. package/dist/cli/{chunk-mfm4sjwx.js.map → chunk-cjtn640a.js.map} +2 -2
  35. package/dist/cli/{chunk-pxj10x8y.js → chunk-ct47dqpx.js} +14 -3
  36. package/dist/cli/{chunk-pxj10x8y.js.map → chunk-ct47dqpx.js.map} +4 -3
  37. package/dist/cli/{chunk-x66c5yjn.js → chunk-dwgcp5sm.js} +2 -2
  38. package/dist/cli/{chunk-4trphnvy.js → chunk-e7f42gdj.js} +10 -13
  39. package/dist/cli/{chunk-4trphnvy.js.map → chunk-e7f42gdj.js.map} +2 -2
  40. package/dist/cli/{chunk-82atea4k.js → chunk-esphfr8p.js} +14 -18
  41. package/dist/cli/{chunk-82atea4k.js.map → chunk-esphfr8p.js.map} +2 -2
  42. package/dist/cli/{chunk-q56730e0.js → chunk-ex56aa81.js} +53 -53
  43. package/dist/cli/chunk-ex56aa81.js.map +13 -0
  44. package/dist/cli/{chunk-ywn7t0pb.js → chunk-garjf5z9.js} +3 -3
  45. package/dist/cli/{chunk-ev67ycx0.js → chunk-jq5n4avg.js} +1 -1
  46. package/dist/cli/{chunk-ka5k7cz9.js → chunk-js7saxwm.js} +35 -39
  47. package/dist/cli/{chunk-ka5k7cz9.js.map → chunk-js7saxwm.js.map} +4 -6
  48. package/dist/cli/{chunk-x1wvw7a8.js → chunk-k79xp7av.js} +168 -208
  49. package/dist/cli/chunk-k79xp7av.js.map +39 -0
  50. package/dist/cli/{chunk-3r94j3tc.js → chunk-nn13znc2.js} +2 -2
  51. package/dist/cli/{chunk-4ae4f395.js → chunk-ps4m1xh4.js} +60 -35
  52. package/dist/cli/chunk-ps4m1xh4.js.map +15 -0
  53. package/dist/cli/{chunk-wd27zjcz.js → chunk-q4rae3bg.js} +1 -1
  54. package/dist/cli/{chunk-pdwg3q9g.js → chunk-rqy0s5wh.js} +21 -30
  55. package/dist/cli/{chunk-pdwg3q9g.js.map → chunk-rqy0s5wh.js.map} +2 -2
  56. package/dist/cli/{chunk-52cwcqvp.js → chunk-rz9jmfhz.js} +15 -24
  57. package/dist/cli/{chunk-52cwcqvp.js.map → chunk-rz9jmfhz.js.map} +2 -2
  58. package/dist/cli/{chunk-8p3xe5jv.js → chunk-vacwm2hv.js} +3 -3
  59. package/dist/cli/{chunk-cbjnx4s8.js → chunk-vh9w1sgp.js} +1 -1
  60. package/dist/cli/{chunk-sbdqrjbb.js → chunk-vrfp10qk.js} +1 -1
  61. package/dist/cli/{chunk-h9ekmtz7.js → chunk-yg63d42r.js} +28 -35
  62. package/dist/cli/{chunk-h9ekmtz7.js.map → chunk-yg63d42r.js.map} +2 -2
  63. package/dist/cli/{chunk-5hs6gb7n.js → chunk-yzhm0j9q.js} +1 -1
  64. package/dist/cli/index.js +397 -34
  65. package/dist/cli/index.js.map +12 -4
  66. package/dist/types/core/config-input.d.ts +59 -0
  67. package/dist/types/core/data.d.ts +2 -0
  68. package/dist/types/core/schema.d.ts +47 -3
  69. package/dist/types/core/types.d.ts +5 -0
  70. package/docs/configuration/ask-ai.mdx +61 -0
  71. package/docs/configuration/index.mdx +3 -1
  72. package/docs/content/navigation.mdx +3 -0
  73. package/docs/content/syntax.mdx +10 -0
  74. package/docs/discoverability/agent-discovery.mdx +82 -1
  75. package/docs/discoverability/index.mdx +1 -1
  76. package/docs/discoverability/llms-txt.mdx +1 -1
  77. package/docs/reference/frontmatter.mdx +2 -0
  78. package/package.json +1 -1
  79. package/src/ai/agent-readability.ts +5 -0
  80. package/src/ai/ai-catalog.ts +241 -0
  81. package/src/ai/cors.ts +87 -0
  82. package/src/ai/link-headers.ts +12 -0
  83. package/src/ai/llms.ts +6 -0
  84. package/src/ai/mcp/discovery.ts +1 -1
  85. package/src/astro/generate.ts +4 -0
  86. package/src/astro/templates.ts +88 -23
  87. package/src/cli/commands/build.ts +3 -1
  88. package/src/components/islands/hooks.ts +50 -1
  89. package/src/components/layout/NavTree.astro +75 -66
  90. package/src/components/layout/RootLayout.astro +28 -2
  91. package/src/components/layout/analytics-client.ts +36 -7
  92. package/src/components/layout/nav-utils.ts +17 -3
  93. package/src/core/adapter.ts +61 -0
  94. package/src/core/config-input.ts +60 -0
  95. package/src/core/data.ts +2 -0
  96. package/src/core/navigation.ts +22 -3
  97. package/src/core/schema.ts +88 -0
  98. package/src/core/types.ts +5 -0
  99. package/src/deploy/artifacts.ts +12 -1
  100. package/src/deploy/headers.ts +6 -0
  101. package/src/deploy/vercel-negotiation.ts +25 -2
  102. package/src/registry/eject.ts +2 -0
  103. package/src/search/build.ts +25 -3
  104. package/src/theme/entry.ts +23 -0
  105. package/dist/cli/chunk-2aj8ddew.js +0 -72
  106. package/dist/cli/chunk-2aj8ddew.js.map +0 -10
  107. package/dist/cli/chunk-4ae4f395.js.map +0 -15
  108. package/dist/cli/chunk-4xyggvgf.js +0 -21
  109. package/dist/cli/chunk-4xyggvgf.js.map +0 -10
  110. package/dist/cli/chunk-6kzzpsx8.js +0 -26
  111. package/dist/cli/chunk-6kzzpsx8.js.map +0 -10
  112. package/dist/cli/chunk-bcy492zc.js +0 -16
  113. package/dist/cli/chunk-bcy492zc.js.map +0 -10
  114. package/dist/cli/chunk-btfr9yvw.js +0 -41
  115. package/dist/cli/chunk-btfr9yvw.js.map +0 -10
  116. package/dist/cli/chunk-ey89bjj1.js +0 -209
  117. package/dist/cli/chunk-ey89bjj1.js.map +0 -11
  118. package/dist/cli/chunk-q56730e0.js.map +0 -13
  119. package/dist/cli/chunk-qvvpnwaz.js +0 -69
  120. package/dist/cli/chunk-qvvpnwaz.js.map +0 -11
  121. package/dist/cli/chunk-vt8fgygt.js +0 -23
  122. package/dist/cli/chunk-vt8fgygt.js.map +0 -10
  123. package/dist/cli/chunk-vxv4x1n8.js +0 -17
  124. package/dist/cli/chunk-vxv4x1n8.js.map +0 -10
  125. package/dist/cli/chunk-vyqj481z.js.map +0 -13
  126. package/dist/cli/chunk-x1wvw7a8.js.map +0 -40
  127. /package/dist/cli/{chunk-8gnpdsn1.js.map → chunk-0xjyb285.js.map} +0 -0
  128. /package/dist/cli/{chunk-27gtm2ym.js.map → chunk-2mzebbbz.js.map} +0 -0
  129. /package/dist/cli/{chunk-s5e5jt53.js.map → chunk-2z47ypj8.js.map} +0 -0
  130. /package/dist/cli/{chunk-x66c5yjn.js.map → chunk-dwgcp5sm.js.map} +0 -0
  131. /package/dist/cli/{chunk-ywn7t0pb.js.map → chunk-garjf5z9.js.map} +0 -0
  132. /package/dist/cli/{chunk-ev67ycx0.js.map → chunk-jq5n4avg.js.map} +0 -0
  133. /package/dist/cli/{chunk-3r94j3tc.js.map → chunk-nn13znc2.js.map} +0 -0
  134. /package/dist/cli/{chunk-wd27zjcz.js.map → chunk-q4rae3bg.js.map} +0 -0
  135. /package/dist/cli/{chunk-8p3xe5jv.js.map → chunk-vacwm2hv.js.map} +0 -0
  136. /package/dist/cli/{chunk-cbjnx4s8.js.map → chunk-vh9w1sgp.js.map} +0 -0
  137. /package/dist/cli/{chunk-sbdqrjbb.js.map → chunk-vrfp10qk.js.map} +0 -0
  138. /package/dist/cli/{chunk-5hs6gb7n.js.map → chunk-yzhm0j9q.js.map} +0 -0
@@ -588,6 +588,8 @@ export interface AskSuggestion {
588
588
  /** Backends that can route an Ask AI request. */
589
589
  type AskProviderGateway = "gateway" | "openrouter" | "llmgateway";
590
590
  type AskProvider = AskProviderGateway | "inkeep" | "openai-compatible";
591
+ /** How much the model reasons before answering (`ai.ask.reasoning`). */
592
+ type AskReasoning = "none" | "minimal" | "low" | "medium" | "high" | "xhigh";
591
593
  /** How much retrieved documentation each Ask AI question carries. */
592
594
  export interface AskRetrievalConfig {
593
595
  /**
@@ -620,6 +622,18 @@ export interface AskConfig {
620
622
  * overrides the built-in preset.
621
623
  */
622
624
  baseUrl?: string;
625
+ /**
626
+ * Origins allowed to call the generated endpoint from another site — a
627
+ * marketing page that embeds an ask box, for example — or `"*"` to allow
628
+ * every origin. The route answers preflight requests and names a listed
629
+ * origin on every response, errors included; every other origin stays
630
+ * subject to the browser's same-origin rule. Callers must send the body as
631
+ * JSON with a `content-type: application/json` header, or Astro's cross-site
632
+ * request check rejects the `POST` before the route runs. Only the generated
633
+ * route reads this; an external `endpoint` handles its own CORS and can't be
634
+ * combined with it.
635
+ */
636
+ cors?: string[];
623
637
  /** Turn Ask AI on. Defaults to `false`. */
624
638
  enabled?: boolean;
625
639
  /**
@@ -645,6 +659,18 @@ export interface AskConfig {
645
659
  model?: string;
646
660
  /** Which backend routes the request. Defaults to `gateway`. */
647
661
  provider?: AskProvider;
662
+ /**
663
+ * How much the model reasons before answering, from `"none"` to `"xhigh"`.
664
+ * Sent as the backend's own reasoning-effort control: the AI SDK's
665
+ * `reasoning` option on the gateway, `reasoning.effort` on OpenRouter, and
666
+ * `reasoning_effort` on OpenAI-compatible endpoints. The model has to
667
+ * support the level — OpenAI rejects one a model doesn't offer — and the
668
+ * endpoint has to accept the parameter; Inkeep has no reasoning control,
669
+ * so the field is rejected there. Omitted keeps the model's default.
670
+ * `"none"` is the fastest and cheapest for grounded docs Q&A, where the
671
+ * retrieved excerpts carry the answer.
672
+ */
673
+ reasoning?: AskReasoning;
648
674
  /**
649
675
  * How much documentation each question carries into the model's prompt.
650
676
  * Lower values cut time-to-first-token — which dominates on a self-hosted
@@ -654,6 +680,30 @@ export interface AskConfig {
654
680
  /** Starter prompts shown before the first question. */
655
681
  suggestions?: AskSuggestion[];
656
682
  }
683
+ /** What the AI Catalog (ARD) manifest carries. */
684
+ export interface AiCatalogConfig {
685
+ /** Emit `/.well-known/ai-catalog.json` and `/.well-known/ard.json`. Defaults to `true`. */
686
+ enabled?: boolean;
687
+ /**
688
+ * Representative queries per entry, keyed by the entry's `<namespace>:<name>`
689
+ * — its identifier minus the `urn:air:<host>:` prefix (`mcp:docs`,
690
+ * `skill:blume`, `api:docs`, `reference:<slug>`, `docs:llms-txt`). Each
691
+ * list replaces the generated defaults for that entry: 2–5 short
692
+ * natural-language questions the resource can answer, which agent
693
+ * registries embed for semantic search.
694
+ *
695
+ * ```ts
696
+ * ai: {
697
+ * catalog: {
698
+ * queries: {
699
+ * "mcp:acme": ["how do I install Acme", "search the Acme docs"],
700
+ * },
701
+ * },
702
+ * }
703
+ * ```
704
+ */
705
+ queries?: Record<string, string[]>;
706
+ }
657
707
  /** What the `llms.txt`/`llms-full.txt` files include. */
658
708
  export interface LlmsTxtConfig {
659
709
  /**
@@ -708,6 +758,15 @@ export interface AiConfig {
708
758
  api?: boolean;
709
759
  /** The Ask AI chat assistant. */
710
760
  ask?: AskConfig;
761
+ /**
762
+ * The AI Catalog / ARD manifest (`/.well-known/ai-catalog.json`, mirrored
763
+ * at `/.well-known/ard.json`): a domain-level index of the agent-facing
764
+ * resources the site publishes — MCP server, agent skills, the JSON docs
765
+ * API, API references, llms.txt — for agent registries. Needs a
766
+ * `deployment.site`. Defaults to `true`; the object form overrides the
767
+ * generated representative queries per entry.
768
+ */
769
+ catalog?: boolean | AiCatalogConfig;
711
770
  /**
712
771
  * Emit `llms.txt` (an index of the docs for LLMs). Defaults to `true`.
713
772
  * The object form adds knobs for what the files include.
@@ -143,6 +143,8 @@ export interface BlumeDataConfig {
143
143
  */
144
144
  discovery: {
145
145
  agentReadability: boolean;
146
+ /** Whether the AI Catalog / ARD manifest is published (`ai.catalog`). */
147
+ aiCatalog: boolean;
146
148
  /** Whether the JSON docs API and its `/openapi.json` are published. */
147
149
  api: boolean;
148
150
  llmsTxt: boolean;
@@ -327,6 +327,11 @@ export type SidebarItemConfig = string | {
327
327
  root?: string;
328
328
  };
329
329
  export declare const searchProviders: readonly ["orama", "pagefind", "flexsearch", "algolia", "orama-cloud", "typesense", "mixedbread", "none"];
330
+ /**
331
+ * The `ai.ask.reasoning` levels: the AI SDK's top-level `reasoning` values
332
+ * minus `provider-default`, which is what omitting the field means.
333
+ */
334
+ export declare const askReasoningLevels: readonly ["none", "minimal", "low", "medium", "high", "xhigh"];
330
335
  /** Ask AI backends. `gateway` (default) routes through the Vercel AI Gateway. */
331
336
  export declare const askAiProviders: readonly ["gateway", "openrouter", "llmgateway", "inkeep", "openai-compatible"];
332
337
  declare const aiConfigSchema: z.ZodObject<{
@@ -334,6 +339,7 @@ declare const aiConfigSchema: z.ZodObject<{
334
339
  ask: z.ZodOptional<z.ZodObject<{
335
340
  apiKeyEnv: z.ZodOptional<z.ZodString>;
336
341
  baseUrl: z.ZodOptional<z.ZodURL>;
342
+ cors: z.ZodOptional<z.ZodArray<z.ZodUnion<readonly [z.ZodLiteral<"*">, z.ZodPipe<z.ZodURL, z.ZodTransform<string, string>>]>>>;
337
343
  enabled: z.ZodDefault<z.ZodBoolean>;
338
344
  endpoint: z.ZodOptional<z.ZodString>;
339
345
  headers: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
@@ -346,6 +352,14 @@ declare const aiConfigSchema: z.ZodObject<{
346
352
  inkeep: "inkeep";
347
353
  "openai-compatible": "openai-compatible";
348
354
  }>>;
355
+ reasoning: z.ZodOptional<z.ZodEnum<{
356
+ none: "none";
357
+ minimal: "minimal";
358
+ low: "low";
359
+ medium: "medium";
360
+ high: "high";
361
+ xhigh: "xhigh";
362
+ }>>;
349
363
  retrieval: z.ZodOptional<z.ZodObject<{
350
364
  contextBudget: z.ZodOptional<z.ZodNumber>;
351
365
  excerptChars: z.ZodOptional<z.ZodNumber>;
@@ -356,6 +370,16 @@ declare const aiConfigSchema: z.ZodObject<{
356
370
  label: z.ZodString;
357
371
  }, z.core.$strict>>>;
358
372
  }, z.core.$strict>>;
373
+ catalog: z.ZodPipe<z.ZodDefault<z.ZodUnion<readonly [z.ZodBoolean, z.ZodObject<{
374
+ enabled: z.ZodDefault<z.ZodBoolean>;
375
+ queries: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodArray<z.ZodString>>>;
376
+ }, z.core.$strict>]>>, z.ZodTransform<{
377
+ enabled: boolean;
378
+ queries: Record<string, string[]>;
379
+ }, boolean | {
380
+ enabled: boolean;
381
+ queries: Record<string, string[]>;
382
+ }>>;
359
383
  llmsTxt: z.ZodPipe<z.ZodDefault<z.ZodUnion<readonly [z.ZodBoolean, z.ZodObject<{
360
384
  details: z.ZodOptional<z.ZodString>;
361
385
  enabled: z.ZodDefault<z.ZodBoolean>;
@@ -391,6 +415,7 @@ declare const aiConfigSchema: z.ZodObject<{
391
415
  webmcp: z.ZodDefault<z.ZodBoolean>;
392
416
  }, z.core.$strict>;
393
417
  export type AskAiProvider = (typeof askAiProviders)[number];
418
+ export type AskReasoning = (typeof askReasoningLevels)[number];
394
419
  export type AskAiConfig = NonNullable<z.infer<typeof aiConfigSchema>["ask"]>;
395
420
  export { openInChatProviders } from "./open-in-chat.ts";
396
421
  export type { OpenInChatProvider } from "./open-in-chat.ts";
@@ -505,9 +530,9 @@ declare const contentSignalsSchema: z.ZodPipe<z.ZodUnion<readonly [z.ZodBoolean,
505
530
  declare const dateFormatConfigSchema: z.ZodObject<{
506
531
  calendar: z.ZodOptional<z.ZodString>;
507
532
  dateStyle: z.ZodOptional<z.ZodEnum<{
533
+ medium: "medium";
508
534
  full: "full";
509
535
  long: "long";
510
- medium: "medium";
511
536
  short: "short";
512
537
  }>>;
513
538
  day: z.ZodOptional<z.ZodEnum<{
@@ -576,6 +601,7 @@ export declare const blumeConfigSchema: z.ZodObject<{
576
601
  ask: z.ZodOptional<z.ZodObject<{
577
602
  apiKeyEnv: z.ZodOptional<z.ZodString>;
578
603
  baseUrl: z.ZodOptional<z.ZodURL>;
604
+ cors: z.ZodOptional<z.ZodArray<z.ZodUnion<readonly [z.ZodLiteral<"*">, z.ZodPipe<z.ZodURL, z.ZodTransform<string, string>>]>>>;
579
605
  enabled: z.ZodDefault<z.ZodBoolean>;
580
606
  endpoint: z.ZodOptional<z.ZodString>;
581
607
  headers: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
@@ -588,6 +614,14 @@ export declare const blumeConfigSchema: z.ZodObject<{
588
614
  inkeep: "inkeep";
589
615
  "openai-compatible": "openai-compatible";
590
616
  }>>;
617
+ reasoning: z.ZodOptional<z.ZodEnum<{
618
+ none: "none";
619
+ minimal: "minimal";
620
+ low: "low";
621
+ medium: "medium";
622
+ high: "high";
623
+ xhigh: "xhigh";
624
+ }>>;
591
625
  retrieval: z.ZodOptional<z.ZodObject<{
592
626
  contextBudget: z.ZodOptional<z.ZodNumber>;
593
627
  excerptChars: z.ZodOptional<z.ZodNumber>;
@@ -598,6 +632,16 @@ export declare const blumeConfigSchema: z.ZodObject<{
598
632
  label: z.ZodString;
599
633
  }, z.core.$strict>>>;
600
634
  }, z.core.$strict>>;
635
+ catalog: z.ZodPipe<z.ZodDefault<z.ZodUnion<readonly [z.ZodBoolean, z.ZodObject<{
636
+ enabled: z.ZodDefault<z.ZodBoolean>;
637
+ queries: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodArray<z.ZodString>>>;
638
+ }, z.core.$strict>]>>, z.ZodTransform<{
639
+ enabled: boolean;
640
+ queries: Record<string, string[]>;
641
+ }, boolean | {
642
+ enabled: boolean;
643
+ queries: Record<string, string[]>;
644
+ }>>;
601
645
  llmsTxt: z.ZodPipe<z.ZodDefault<z.ZodUnion<readonly [z.ZodBoolean, z.ZodObject<{
602
646
  details: z.ZodOptional<z.ZodString>;
603
647
  enabled: z.ZodDefault<z.ZodBoolean>;
@@ -773,9 +817,9 @@ export declare const blumeConfigSchema: z.ZodObject<{
773
817
  dateFormat: z.ZodDefault<z.ZodObject<{
774
818
  calendar: z.ZodOptional<z.ZodString>;
775
819
  dateStyle: z.ZodOptional<z.ZodEnum<{
820
+ medium: "medium";
776
821
  full: "full";
777
822
  long: "long";
778
- medium: "medium";
779
823
  short: "short";
780
824
  }>>;
781
825
  day: z.ZodOptional<z.ZodEnum<{
@@ -1061,6 +1105,7 @@ export declare const blumeConfigSchema: z.ZodObject<{
1061
1105
  label: z.ZodString;
1062
1106
  }, z.core.$strict>>>;
1063
1107
  provider: z.ZodDefault<z.ZodEnum<{
1108
+ none: "none";
1064
1109
  algolia: "algolia";
1065
1110
  mixedbread: "mixedbread";
1066
1111
  orama: "orama";
@@ -1068,7 +1113,6 @@ export declare const blumeConfigSchema: z.ZodObject<{
1068
1113
  flexsearch: "flexsearch";
1069
1114
  "orama-cloud": "orama-cloud";
1070
1115
  typesense: "typesense";
1071
- none: "none";
1072
1116
  }>>;
1073
1117
  typesense: z.ZodOptional<z.ZodObject<{
1074
1118
  collection: z.ZodString;
@@ -208,6 +208,11 @@ export type NavNode = {
208
208
  */
209
209
  display: SidebarDisplay;
210
210
  icon?: string;
211
+ /**
212
+ * The group row's link: an explicit-config group's `root`, or the
213
+ * generated folder's index page route. Absent when there is no page at
214
+ * the group's own path, so the row never links to a 404.
215
+ */
211
216
  route?: string;
212
217
  /**
213
218
  * The group's URL path (its folder route prefix), even when the folder
@@ -109,6 +109,37 @@ Blume sends the same `POST` body as its built-in route:
109
109
 
110
110
  Return a successful response whose body is a plain UTF-8 text stream. If the endpoint is on another origin, allow the docs origin with CORS: accept `OPTIONS` and `POST`, permit the `content-type` request header, and return the CORS headers on both the preflight and streamed response. With `endpoint` set, Blume generates the chat UI but no server route, grounding snapshot, provider dependency, or provider-secret warning; your backend owns retrieval, authentication, rate limiting, model access, and citations.
111
111
 
112
+ ## Cross-origin callers
113
+
114
+ The generated endpoint answers the in-page assistant on its own origin. To call it from another site as well — a marketing page with an ask box, say — list that site's origin in `cors`:
115
+
116
+ ```ts blume.config.ts lineNumbers
117
+ ai: {
118
+ ask: {
119
+ enabled: true,
120
+ cors: ["https://www.example.com"],
121
+ },
122
+ }
123
+ ```
124
+
125
+ The route then answers the browser's `OPTIONS` preflight and names a listed origin on every response — the streamed answer and the error statuses alike, so the caller can tell a rejected body from a provider failure. Origins that aren't listed get no header and stay subject to the browser's same-origin rule. Each entry is reduced to its origin, so `https://www.example.com/docs/` and `https://www.example.com` mean the same thing. To let any page call the route, list `"*"` instead of origins.
126
+
127
+ The caller sends the same `POST` body the [external endpoint](#external-endpoint) contract describes and reads back the same text stream. Send it as JSON with a `content-type: application/json` header:
128
+
129
+ ```ts
130
+ const response = await fetch("https://docs.example.com/api/ask", {
131
+ body: JSON.stringify({
132
+ messages: [{ role: "user", content: "How do I deploy?" }],
133
+ }),
134
+ headers: { "content-type": "application/json" },
135
+ method: "POST",
136
+ });
137
+ ```
138
+
139
+ The content type matters: Astro's cross-site request check rejects a cross-origin `POST` that has no content type, or a form-like one such as `text/plain`, with a 403 before the route runs, and that response carries no CORS headers, so the browser reports it as a network error rather than a status. The preflight allows whatever request headers the caller asks for, so a fetch wrapper that adds its own headers needs no extra configuration.
140
+
141
+ `cors` only affects the generated route; with an external `endpoint`, CORS is that backend's job, and setting both is a config error. The endpoint stays unauthenticated either way, so the [rate limiting](#rate-limiting) advice applies to cross-origin traffic too.
142
+
112
143
  ## Server output required
113
144
 
114
145
  Blume's built-in Ask AI backend is a server route (`POST /api/ask`), so it can't run on a static build. Switch to server output and pick an adapter:
@@ -188,6 +219,36 @@ The values are written into the generated route as-is, so keep secrets in `apiKe
188
219
 
189
220
  Keys are read through Astro's [`getSecret()`](https://docs.astro.build/en/guides/environment-variables/#retrieving-secrets-programmatically), so each adapter supplies them its own way: environment variables on Node, Vercel, and Netlify, and the Worker's [bindings](https://docs.astro.build/en/guides/integrations-guide/cloudflare/#environment-variables-and-secrets) on Cloudflare. Enabling Ask AI also turns on React for the in-page island — see [Customization](/docs/configuration/customization#interactive-islands).
190
221
 
222
+ ## Reasoning
223
+
224
+ Reasoning models think before they answer, and how much they do so by default varies by model. For grounded docs Q&A the retrieved excerpts carry the answer, so most of that thinking is latency the reader waits through. `reasoning` sets how much the model reasons: `"none"`, `"minimal"`, `"low"`, `"medium"`, `"high"`, or `"xhigh"`:
225
+
226
+ ```ts blume.config.ts lineNumbers
227
+ ai: {
228
+ ask: {
229
+ enabled: true,
230
+ model: "openai/gpt-5.5",
231
+ reasoning: "none",
232
+ },
233
+ }
234
+ ```
235
+
236
+ The value is sent as the backend's own reasoning-effort control. Through the gateway it travels as the [AI SDK's `reasoning` option](https://ai-sdk.dev/docs/ai-sdk-core/reasoning), which the gateway maps to the model's setting — OpenAI's `reasoning_effort`, for example. On OpenRouter it is sent as `reasoning.effort`, and on LLMGateway or a custom `openai-compatible` endpoint as `reasoning_effort` in the request, so the endpoint has to accept that parameter. The model has to support the level you pick: OpenAI rejects a level a model doesn't offer (`"none"` and `"xhigh"` exist only on some), so check the model's documentation before setting one. Inkeep runs its own QA pipeline and has no reasoning control, so setting `reasoning` with that backend is a config error. Leave it unset to keep the model's default. Like [retrieval size](#retrieval-size), it trades thoroughness for time-to-first-token, and answers stay grounded either way.
237
+
238
+ ## Analytics
239
+
240
+ With an [analytics provider](/docs/configuration/analytics) configured, the assistant reports its usage through the same `track()` the page feedback widget uses, so questions land next to your pageviews:
241
+
242
+ | Event | When | Properties |
243
+ | --- | --- | --- |
244
+ | `ask` | A question is sent | `path`, `questionChars` |
245
+ | `ask_answer` | The answer finishes streaming | `path`, `questionChars`, `ms`, `chars` |
246
+ | `ask_error` | The request fails, breaks, or comes back empty | `path`, `questionChars`, `ms`, `status` |
247
+
248
+ `path` is the page the reader asked from (the served pathname, so it matches the feedback widget and your pageviews under a `base`), `questionChars` the question's length, `ms` the time from sending the question to the last chunk, and `chars` the answer's length. `status` is the HTTP status: `0` when no response arrived at all (offline, DNS, CORS), and `200` when the response was fine but its stream broke mid-answer — how a provider or credential error surfaces, since the backend has already sent its headers — or delivered nothing. Clearing the conversation mid-answer reports neither outcome.
249
+
250
+ The question's text never reaches a provider: it is free-form reader input (pasted keys, error logs, names) that would breach most providers' terms and per-value size limits. It travels only on the `blume:track` DOM event, as `question` in `detail.props`, so a listener you write can forward it wherever you decide it belongs. A custom chat UI built on `useAskAI` from `blume/hooks` reports the same events. With no provider configured the built-in provider calls are no-ops, but the `blume:track` event still fires, so a custom integration listening for it receives them.
251
+
191
252
  ## Rate limiting
192
253
 
193
254
  The `POST /api/ask` endpoint is **unauthenticated** — it has to be, so the in-page assistant can call it. Blume validates each request — rejecting malformed bodies, capping it to 1–40 messages, and accepting only `user`/`assistant` roles so a caller can't inject their own system prompt and repurpose the route as a general LLM proxy — to bound how much a single call can spend against your model, but it can't stop someone from calling the endpoint repeatedly. If cost abuse is a concern, put the route behind a rate limiter — your host's (e.g. Vercel's) edge rate limiting, a middleware, or your model provider's per-key spend limits.
@@ -65,9 +65,11 @@ export default defineConfig({
65
65
  },
66
66
  },
67
67
 
68
- // AI — llms.txt, MCP; see the Discoverability section
68
+ // AI — llms.txt, MCP, the AI catalog; see the Discoverability section
69
69
  ai: {
70
70
  llmsTxt: true,
71
+ // AI Catalog / ARD manifest at /.well-known/ai-catalog.json (needs deployment.site)
72
+ catalog: true,
71
73
  // MCP server (needs server output)
72
74
  mcp: {
73
75
  enabled: false,
@@ -12,6 +12,7 @@ By default the sidebar mirrors your content tree:
12
12
  - folders become **groups**, files become **pages**
13
13
  - a page's label is its frontmatter `title`; a group's label is the humanized folder name
14
14
  - items sort by [numeric prefix](/docs/content), then alphabetically, and a folder's `index` page comes first
15
+ - a folder with an `index` page links its group row to that page, so clicking the section name opens the section's landing page
15
16
 
16
17
  That's enough for many sites — everything below is opt-in.
17
18
 
@@ -133,6 +134,8 @@ sidebar:
133
134
  hidden: true
134
135
  ```
135
136
 
137
+ A folder's `index` page appears both as the group row's link and as the first row inside the group. Hide the index page to keep only the linked header: the group row still opens the landing page, and previous/next links still pass through it.
138
+
136
139
  ## Tabs
137
140
 
138
141
  Render top-level sections as tabs in the header, useful for splitting a large site into distinct areas — say adapters, an API, and AI guides. A tab is highlighted when the current route falls under its `path`:
@@ -59,6 +59,16 @@ Inline formatting for stressing words, marking deletions, and showing code or ke
59
59
  **Bold**, _italic_, ~~strikethrough~~, and `inline code`.
60
60
  ```
61
61
 
62
+ ## Keyboard keys
63
+
64
+ For shortcuts and keystrokes. A `<kbd>` element renders as the same bordered key badge the search dialog uses, in Markdown, MDX, and inside components like `<Steps>` and `<Callout>`.
65
+
66
+ Press <kbd>⌘</kbd> <kbd>K</kbd> to open search, or <kbd>Esc</kbd> to close it.
67
+
68
+ ```md
69
+ Press <kbd>⌘</kbd> <kbd>K</kbd> to open search, or <kbd>Esc</kbd> to close it.
70
+ ```
71
+
62
72
  ## Superscript and subscript
63
73
 
64
74
  For footnote markers, ordinals, and scientific or chemical notation inline.
@@ -55,13 +55,14 @@ Agents that probe a site don't know to look for the manifest — so Blume also a
55
55
 
56
56
  ```http
57
57
  Link: </.well-known/api-catalog>; rel="api-catalog"; type="application/linkset+json",
58
+ </.well-known/ai-catalog.json>; rel="ai-catalog"; type="application/ai-catalog+json",
58
59
  </openapi.json>; rel="service-desc"; type="application/json",
59
60
  </agent-readability.json>; rel="describedby"; type="application/json",
60
61
  </llms.txt>; rel="describedby"; type="text/plain",
61
62
  </index.md>; rel="alternate"; type="text/markdown"
62
63
  ```
63
64
 
64
- Each entry appears only when its feature is on. The `alternate` link points at the homepage's Markdown mirror — the page's own [raw Markdown](/docs/discoverability/markdown) when the home route is a content page, or the synthesized `llms.txt` fallback when it's a landing page. The `service-desc` link ([RFC 8631](https://www.rfc-editor.org/rfc/rfc8631)) points at the [JSON API](/docs/discoverability/json-api)'s OpenAPI description, and `api-catalog` at the [generated API catalog](#api-catalog). The header rides on every surface Blume controls: the dev server (check it with `curl -I localhost:4321`), static builds via the emitted `_headers` file (Netlify and Cloudflare), and Vercel server builds via the deploy's routing rules.
65
+ Each entry appears only when its feature is on. The `alternate` link points at the homepage's Markdown mirror — the page's own [raw Markdown](/docs/discoverability/markdown) when the home route is a content page, or the synthesized `llms.txt` fallback when it's a landing page. The `service-desc` link ([RFC 8631](https://www.rfc-editor.org/rfc/rfc8631)) points at the [JSON API](/docs/discoverability/json-api)'s OpenAPI description, `api-catalog` at the [generated API catalog](#api-catalog), and `ai-catalog` at the [AI catalog](#ai-catalog). The header rides on every surface Blume controls: the dev server (check it with `curl -I localhost:4321`), static builds via the emitted `_headers` file (Netlify and Cloudflare), and Vercel server builds via the deploy's routing rules.
65
66
 
66
67
  Not every agent enters through the root, though — one following a search result or a shared link lands on a deep page and never sees the homepage header. So every rendered page also carries the same discovery links in its HTML `<head>`, using the same IANA-registered relations:
67
68
 
@@ -72,6 +73,12 @@ Not every agent enters through the root, though — one following a search resul
72
73
  type="application/json"
73
74
  />
74
75
  <link rel="describedby" href="/llms.txt" type="text/plain" />
76
+ <link
77
+ rel="ai-catalog"
78
+ href="/.well-known/ai-catalog.json"
79
+ type="application/ai-catalog+json"
80
+ />
81
+ <link rel="ard" href="/.well-known/ard.json" type="application/json" />
75
82
  <link rel="alternate" href="/docs/example.md" type="text/markdown" />
76
83
  ```
77
84
 
@@ -121,6 +128,80 @@ When the site publishes APIs, Blume generates an [RFC 9727](https://www.rfc-edit
121
128
 
122
129
  A site with no API references, no MCP server, and the [JSON API](/docs/discoverability/json-api) turned off emits no catalog — there'd be nothing in it. As everywhere, a `public/.well-known/api-catalog` file you ship yourself wins over the generated one.
123
130
 
131
+ ## AI catalog
132
+
133
+ The API catalog lists APIs. The **AI catalog** lists everything an agent could pick up from the site — the MCP server, each published skill, the JSON API, each rendered API reference, and `llms.txt` — in the format agent registries index: an [AI Catalog](https://github.com/Agent-Card/ai-catalog) document at `/.well-known/ai-catalog.json`, which is also the manifest [Agentic Resource Discovery (ARD)](https://agenticresourcediscovery.org/) consumers resolve. Each entry carries a domain-anchored `urn:air:<host>:<namespace>:<name>` identifier, a display name, the artifact's media type, its URL, and a handful of `representativeQueries` — sample questions the resource can answer, which registries embed for semantic search:
134
+
135
+ ```json .well-known/ai-catalog.json
136
+ {
137
+ "specVersion": "1.0",
138
+ "host": {
139
+ "displayName": "Acme",
140
+ "identifier": "did:web:docs.example.com",
141
+ "documentationUrl": "https://docs.example.com/"
142
+ },
143
+ "entries": [
144
+ {
145
+ "identifier": "urn:air:docs.example.com:mcp:acme",
146
+ "displayName": "Acme",
147
+ "type": "application/mcp-server-card+json",
148
+ "url": "https://docs.example.com/.well-known/mcp/server-card.json",
149
+ "capabilities": [
150
+ "search_docs",
151
+ "get_page",
152
+ "list_pages",
153
+ "get_navigation"
154
+ ],
155
+ "representativeQueries": [
156
+ "search the Acme documentation",
157
+ "get a Acme docs page as Markdown",
158
+ "list every page in the Acme docs"
159
+ ]
160
+ },
161
+ {
162
+ "identifier": "urn:air:docs.example.com:skill:acme",
163
+ "displayName": "acme",
164
+ "type": "application/agent-skills+md",
165
+ "url": "https://docs.example.com/.well-known/agent-skills/acme/SKILL.md",
166
+ "representativeQueries": [
167
+ "load the acme agent skill",
168
+ "how do I use acme"
169
+ ]
170
+ },
171
+ {
172
+ "identifier": "urn:air:docs.example.com:api:docs",
173
+ "displayName": "Acme docs API",
174
+ "type": "application/vnd.oai.openapi+json",
175
+ "url": "https://docs.example.com/openapi.json",
176
+ "representativeQueries": [
177
+ "fetch a Acme docs page as JSON",
178
+ "list the pages in the Acme docs",
179
+ "get the Acme docs navigation tree"
180
+ ]
181
+ }
182
+ ]
183
+ }
184
+ ```
185
+
186
+ ARD's current revision reads the manifest from `/.well-known/ard.json` and calls `ai-catalog.json` the predecessor path, so Blume writes the same document to both, advertises it under both link relations (`ai-catalog` and `ard`) in every page's head, and lists it in `llms.txt` and `agent-readability.json`. The catalog and its `.well-known` neighbors (the API catalog, the MCP discovery files) are served with `Access-Control-Allow-Origin: *` on every build surface, so a registry reading them from another origin isn't blocked.
187
+
188
+ Entry identifiers are anchored on your domain, so the catalog needs a [`deployment.site`](/docs/deployment) — without one nothing is emitted. It's on by default; `ai.catalog: false` turns it off. The generated queries are derived from the site title and each entry's own description. To write your own for an entry, key them by the identifier's tail (`<namespace>:<name>`):
189
+
190
+ ```ts blume.config.ts
191
+ export default defineConfig({
192
+ ai: {
193
+ catalog: {
194
+ queries: {
195
+ "mcp:acme": ["how do I install Acme", "search the Acme docs"],
196
+ "skill:acme": ["set up an Acme project", "write an Acme plugin"],
197
+ },
198
+ },
199
+ },
200
+ });
201
+ ```
202
+
203
+ A list replaces the generated queries for that entry; entries you don't name keep theirs. As everywhere, a `public/.well-known/ai-catalog.json` (or `ard.json`) you ship yourself wins over the generated one.
204
+
124
205
  ## WebMCP
125
206
 
126
207
  [WebMCP](https://webmachinelearning.github.io/webmcp/) is an emerging browser API that lets a page register tools directly with an agentic browser — no separate server connection needed. Every Blume page registers the docs' read-only surface on the page's model context: `search_docs` (site search), `get_page` (a page's [raw Markdown](/docs/discoverability/markdown)), and `list_pages` (the [`llms.txt`](/docs/discoverability/llms-txt) index). The script is tiny, loads no search machinery until a tool is actually called, and silently no-ops in every browser without the API — which today is all of them outside [Chrome's early preview](https://developer.chrome.com/blog/webmcp-epp). It registers on whichever surface the in-flux spec exposes (`navigator.modelContext` or `document.modelContext`), via `provideContext` or per-tool `registerTool`.
@@ -37,7 +37,7 @@ Most of this is sharper with an absolute site URL — set [`deployment.site`](/d
37
37
  | Raw Markdown mirrors, content negotiation, Copy as Markdown, Open in chat | `/<route>.md` | on | [Markdown for agents](/docs/discoverability/markdown) |
38
38
  | JSON API and its OpenAPI description | `/api/docs/…`, `/openapi.json` | on | [JSON API](/docs/discoverability/json-api) |
39
39
  | MCP server | `/mcp` | opt-in, server output | [MCP server](/docs/discoverability/mcp) |
40
- | `agent-readability.json`, `Link` headers, API catalog, WebMCP, skills, Web Bot Auth | site root, `/.well-known/…` | on | [Agent discovery](/docs/discoverability/agent-discovery) |
40
+ | `agent-readability.json`, `Link` headers, API catalog, AI catalog (ARD), WebMCP, skills, Web Bot Auth | site root, `/.well-known/…` | on | [Agent discovery](/docs/discoverability/agent-discovery) |
41
41
 
42
42
  Every generated file yields to one you ship yourself: drop a `robots.txt`, `sitemap.xml`, `llms.txt`, `openapi.json`, or `agent-readability.json` in `public/` and Blume serves yours in its place.
43
43
 
@@ -47,7 +47,7 @@ ai: {
47
47
 
48
48
  ## Generated sections
49
49
 
50
- `llms.txt` closes with two generated sections that need no configuration. **Agent skills** lists each skill published through [`ai.skills`](/docs/discoverability/agent-discovery#skills-discovery) with its description (where a skill says when to use it). **Agent resources** links every machine-readable artifact the build emits — `llms-full.txt`, the per-page [raw Markdown](/docs/discoverability/markdown) mirror, the [MCP server](/docs/discoverability/mcp) and its discovery document, the skills index, the [API catalog](/docs/discoverability/agent-discovery#api-catalog), [`agent-readability.json`](/docs/discoverability/agent-discovery#agent-readability), and the [sitemap](/docs/discoverability/sitemap-and-robots#sitemap) — each only when it exists, so an agent that reads nothing but `llms.txt` still finds the whole surface.
50
+ `llms.txt` closes with two generated sections that need no configuration. **Agent skills** lists each skill published through [`ai.skills`](/docs/discoverability/agent-discovery#skills-discovery) with its description (where a skill says when to use it). **Agent resources** links every machine-readable artifact the build emits — `llms-full.txt`, the per-page [raw Markdown](/docs/discoverability/markdown) mirror, the [MCP server](/docs/discoverability/mcp) and its discovery document, the skills index, the [API catalog](/docs/discoverability/agent-discovery#api-catalog), the [AI catalog](/docs/discoverability/agent-discovery#ai-catalog), [`agent-readability.json`](/docs/discoverability/agent-discovery#agent-readability), and the [sitemap](/docs/discoverability/sitemap-and-robots#sitemap) — each only when it exists, so an agent that reads nothing but `llms.txt` still finds the whole surface.
51
51
 
52
52
  ## Excluding a page
53
53
 
@@ -49,6 +49,8 @@ sidebar:
49
49
  display: page
50
50
  ```
51
51
 
52
+ `hidden` removes the page from the sidebar and from previous/next pagination. On a folder's `index` page it removes only the page's own row: the group row keeps linking to the page, and previous/next links still pass through it.
53
+
52
54
  `display` sets the render mode of the page's folder group ([per-group overrides](/docs/content/navigation#per-group-overrides)) and is only meaningful on a folder's `index` page under the generated sidebar — anywhere else (a non-index page, the content root's own `index` page, or any page under an explicit `navigation.sidebar`) it has no group to configure, and Blume warns with `BLUME_SIDEBAR_DISPLAY_IGNORED`.
53
55
 
54
56
  ## SEO
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "blume",
3
- "version": "1.7.1",
3
+ "version": "1.7.3",
4
4
  "description": "Documentation that's fast, AI-ready, and zero-config.",
5
5
  "keywords": [
6
6
  "astro",
@@ -4,6 +4,7 @@ import type { BlumeProject } from "../core/project-graph.ts";
4
4
  import type { ContentSignalPolicy, ContentSignals } from "../core/schema.ts";
5
5
  import { absoluteUrl } from "../core/site-url.ts";
6
6
  import { buildRssFeeds } from "../deploy/rss.ts";
7
+ import { AI_CATALOG_PATH, hasAiCatalog } from "./ai-catalog.ts";
7
8
  import { hasApiCatalog } from "./api-catalog.ts";
8
9
  import { API_PAGES_PATH, API_SEARCH_PATH, OPENAPI_PATH } from "./api/paths.ts";
9
10
 
@@ -52,6 +53,7 @@ const askApiUrl = (
52
53
  /** The `.well-known` discovery URLs a site can publish. */
53
54
  interface WellKnownArtifacts {
54
55
  httpMessageSignaturesDirectory?: string;
56
+ aiCatalog?: string;
55
57
  apiCatalog?: string;
56
58
  agentSkills?: string;
57
59
  }
@@ -138,6 +140,9 @@ const wellKnownArtifacts = (
138
140
  "/.well-known/http-message-signatures-directory"
139
141
  );
140
142
  }
143
+ if (hasAiCatalog(config)) {
144
+ artifacts.aiCatalog = abs(AI_CATALOG_PATH);
145
+ }
141
146
  if (hasApiCatalog(config)) {
142
147
  artifacts.apiCatalog = abs("/.well-known/api-catalog");
143
148
  }