blume 0.5.3 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (132) hide show
  1. package/dist/cli/index.js +3349 -7024
  2. package/dist/cli/index.js.map +39 -69
  3. package/dist/types/core/config.d.ts +0 -8
  4. package/dist/types/core/data.d.ts +6 -2
  5. package/dist/types/core/i18n-ui.d.ts +50 -0
  6. package/dist/types/core/schema.d.ts +379 -485
  7. package/dist/types/core/types.d.ts +8 -6
  8. package/docs/advanced/meta.ts +1 -8
  9. package/docs/advanced/skills.mdx +28 -0
  10. package/docs/configuration/ai.mdx +58 -0
  11. package/docs/configuration/index.mdx +13 -17
  12. package/docs/configuration/seo.mdx +59 -1
  13. package/docs/configuration/theming.mdx +15 -18
  14. package/docs/content/components.mdx +2 -53
  15. package/docs/content/i18n.mdx +0 -4
  16. package/docs/content/meta.mdx +3 -17
  17. package/docs/content/navigation.mdx +41 -4
  18. package/docs/content/syntax.mdx +1 -1
  19. package/docs/index.mdx +0 -1
  20. package/docs/reference/cli.mdx +12 -13
  21. package/package.json +6 -6
  22. package/skills/blume/SKILL.md +71 -0
  23. package/skills/blume-update-docs/SKILL.md +52 -0
  24. package/skills/blume-update-docs/references/audit-checklist.md +46 -0
  25. package/src/ai/agent-readability.ts +97 -0
  26. package/src/ai/ask-context.ts +131 -8
  27. package/src/ai/ask-data.ts +4 -1
  28. package/src/astro/generate.ts +19 -12
  29. package/src/astro/integration.ts +0 -21
  30. package/src/astro/templates.ts +33 -21
  31. package/src/cli/commands/build.ts +15 -0
  32. package/src/cli/commands/dev.ts +31 -20
  33. package/src/cli/commands/validate.ts +0 -2
  34. package/src/cli/dev-lock.ts +94 -21
  35. package/src/cli/index.ts +0 -2
  36. package/src/components/BlumePage.astro +0 -6
  37. package/src/components/Icon.astro +1 -12
  38. package/src/components/content/AccordionItem.astro +3 -6
  39. package/src/components/content/Badge.astro +1 -3
  40. package/src/components/content/Callout.astro +3 -9
  41. package/src/components/content/Card.astro +2 -3
  42. package/src/components/content/ColorItem.astro +2 -2
  43. package/src/components/content/Column.astro +1 -1
  44. package/src/components/content/GithubInfo.astro +11 -10
  45. package/src/components/content/Prompt.astro +1 -1
  46. package/src/components/content/Step.astro +3 -4
  47. package/src/components/content/Tab.astro +2 -3
  48. package/src/components/content/TypeTable.astro +13 -8
  49. package/src/components/content/Update.astro +1 -1
  50. package/src/components/islands/AskAI.astro +66 -2
  51. package/src/components/islands/ask-ai.tsx +289 -53
  52. package/src/components/layout/Header.astro +27 -4
  53. package/src/components/layout/Logo.astro +5 -1
  54. package/src/components/layout/NavSelector.astro +1 -1
  55. package/src/components/layout/NavTree.astro +15 -15
  56. package/src/components/layout/PageActions.astro +73 -30
  57. package/src/components/layout/PageLayout.astro +42 -0
  58. package/src/components/layout/ReferenceLayout.astro +1 -0
  59. package/src/components/layout/RootLayout.astro +79 -4
  60. package/src/components/layout/Search.astro +5 -5
  61. package/src/components/layout/nav-utils.ts +9 -4
  62. package/src/components/openapi/ApiOverview.astro +4 -50
  63. package/src/components/openapi/ApiTagOperations.astro +42 -0
  64. package/src/core/builtin-tags.ts +1 -3
  65. package/src/core/config.ts +5 -28
  66. package/src/core/data.ts +6 -2
  67. package/src/core/graph.ts +8 -6
  68. package/src/core/i18n-ui.ts +5 -0
  69. package/src/core/links.ts +5 -19
  70. package/src/core/meta.ts +1 -1
  71. package/src/core/nav-diagnostics.ts +7 -0
  72. package/src/core/navigation.ts +38 -17
  73. package/src/core/project-graph.ts +0 -5
  74. package/src/core/schema.ts +133 -95
  75. package/src/core/sources/filesystem.ts +5 -1
  76. package/src/core/sources/resolve.ts +0 -13
  77. package/src/core/sources/watch.ts +43 -11
  78. package/src/core/types.ts +8 -6
  79. package/src/deploy/robots.ts +37 -4
  80. package/src/openapi/parse.ts +197 -14
  81. package/src/openapi/render-mdx.ts +44 -10
  82. package/src/openapi/scalar.ts +1 -1
  83. package/src/openapi/source.ts +19 -2
  84. package/src/search/documents.ts +9 -2
  85. package/src/theme/entry.ts +45 -17
  86. package/src/theme/icons.ts +18 -109
  87. package/src/theme/palette.ts +25 -51
  88. package/src/theme/twoslash.ts +6 -1
  89. package/dist/types/core/bridge.d.ts +0 -24
  90. package/dist/types/core/package-json.d.ts +0 -12
  91. package/dist/types/migrate/mintlify/assets.d.ts +0 -8
  92. package/dist/types/migrate/mintlify/config.d.ts +0 -16
  93. package/dist/types/migrate/mintlify/i18n.d.ts +0 -7
  94. package/dist/types/migrate/shared.d.ts +0 -153
  95. package/docs/advanced/bridge.mdx +0 -76
  96. package/docs/advanced/migrate.mdx +0 -124
  97. package/src/astro/static-assets.ts +0 -124
  98. package/src/cli/commands/migrate.ts +0 -39
  99. package/src/components/content/ApiField.astro +0 -75
  100. package/src/components/content/ParamField.astro +0 -39
  101. package/src/components/content/RequestField.astro +0 -23
  102. package/src/components/content/ResponseField.astro +0 -23
  103. package/src/components/content/Warning.astro +0 -9
  104. package/src/core/assets.ts +0 -31
  105. package/src/core/bridge.ts +0 -102
  106. package/src/core/sources/mintlify.ts +0 -190
  107. package/src/migrate/fumadocs/config.ts +0 -155
  108. package/src/migrate/fumadocs/content.ts +0 -376
  109. package/src/migrate/fumadocs/frontmatter.ts +0 -18
  110. package/src/migrate/fumadocs/groups.ts +0 -237
  111. package/src/migrate/fumadocs/index.ts +0 -355
  112. package/src/migrate/fumadocs/meta.ts +0 -244
  113. package/src/migrate/migrate.ts +0 -53
  114. package/src/migrate/mintlify/assets.ts +0 -46
  115. package/src/migrate/mintlify/config.ts +0 -954
  116. package/src/migrate/mintlify/content.ts +0 -120
  117. package/src/migrate/mintlify/frontmatter.ts +0 -126
  118. package/src/migrate/mintlify/i18n.ts +0 -51
  119. package/src/migrate/mintlify/icons.ts +0 -128
  120. package/src/migrate/mintlify/index.ts +0 -459
  121. package/src/migrate/mintlify/snippets.ts +0 -315
  122. package/src/migrate/mintlify/transform.ts +0 -82
  123. package/src/migrate/nextra/content.ts +0 -46
  124. package/src/migrate/nextra/frontmatter.ts +0 -40
  125. package/src/migrate/nextra/index.ts +0 -389
  126. package/src/migrate/nextra/meta.ts +0 -266
  127. package/src/migrate/shared.ts +0 -801
  128. package/src/migrate/starlight/config.ts +0 -455
  129. package/src/migrate/starlight/content.ts +0 -75
  130. package/src/migrate/starlight/frontmatter.ts +0 -111
  131. package/src/migrate/starlight/i18n.ts +0 -54
  132. package/src/migrate/starlight/index.ts +0 -131
@@ -99,25 +99,15 @@ const pageMetaBaseSchema = z
99
99
  deprecated: z.boolean().default(false),
100
100
  description: z.string().optional(),
101
101
  draft: z.boolean().default(false),
102
- groups: z.union([z.string(), z.array(z.string())]).optional(),
103
102
  hidden: z.boolean().default(false),
104
- hideApiMarker: z.boolean().default(false),
105
- hideFooterPagination: z.boolean().optional(),
106
103
  icon: iconName.optional(),
107
- iconType: z.string().optional(),
108
- keywords: z.array(z.string()).optional(),
109
104
  /** Overrides the git-derived last-modified date when `lastModified` is on. */
110
105
  lastModified: dateSchema.optional(),
111
- mode: z.string().optional(),
112
106
  noindex: z.boolean().default(false),
113
- public: z.boolean().optional(),
114
- rss: z.boolean().optional(),
115
107
  search: searchMetaSchema.default({}),
116
108
  seo: seoMetaSchema.default({}),
117
109
  sidebar: sidebarMetaSchema.default({}),
118
- sidebarTitle: z.string().optional(),
119
110
  slug: z.string().optional(),
120
- tag: z.string().optional(),
121
111
  title: z.string().optional(),
122
112
  type: z.string().default("doc"),
123
113
  })
@@ -145,7 +135,6 @@ export type SidebarDisplay = z.infer<typeof sidebarDisplaySchema>;
145
135
  export const folderMetaSchema = z
146
136
  .object({
147
137
  collapsed: z.boolean().optional(),
148
- display: sidebarDisplaySchema.optional(),
149
138
  icon: iconName.optional(),
150
139
  order: z.number().optional(),
151
140
  /** Explicit child ordering by slug segment (without numeric prefix). */
@@ -160,45 +149,41 @@ export type FolderMeta = z.infer<typeof folderMetaSchema>;
160
149
  // Project config (blume.config.ts)
161
150
  // ---------------------------------------------------------------------------
162
151
 
163
- const logoConfigSchema = z.union([
152
+ /** The logo mark: a single image path/URL, or light/dark variants with alt text. */
153
+ const logoImageSchema = z.union([
164
154
  z.string(),
165
155
  z
166
156
  .object({
167
157
  alt: z.string().optional(),
168
158
  dark: z.string().optional(),
169
- href: z.string().optional(),
170
159
  light: z.string().optional(),
171
160
  })
172
161
  .strict(),
173
162
  ]);
174
163
 
175
- const faviconConfigSchema = z.union([
164
+ /**
165
+ * Site logo. A bare string is the image shorthand. The object form splits the
166
+ * brand into an optional `image` mark and optional wordmark `text` so a site can
167
+ * show an image-only logo (a mark with the wordmark baked in), a text-only logo,
168
+ * or both. Omit `text` to fall back to the site title; set `text: ""` to render
169
+ * the mark alone. `href` overrides the brand link (defaults to `/`).
170
+ */
171
+ const logoConfigSchema = z.union([
176
172
  z.string(),
177
173
  z
178
174
  .object({
179
- dark: z.string().optional(),
180
- light: z.string().optional(),
175
+ href: z.string().optional(),
176
+ image: logoImageSchema.optional(),
177
+ text: z.string().optional(),
181
178
  })
182
179
  .strict(),
183
180
  ]);
184
181
 
185
- const bannerColorSchema = z
186
- .object({
187
- dark: z.string().optional(),
188
- light: z.string().optional(),
189
- })
190
- .strict()
191
- .refine((value) => value.dark !== undefined || value.light !== undefined, {
192
- message: "Banner color requires at least one of light or dark.",
193
- });
194
-
195
182
  /** Site-wide announcement banner: a string, or text with an optional link. */
196
183
  const bannerConfigSchema = z.union([
197
184
  z.string(),
198
185
  z
199
186
  .object({
200
- /** Background color override (Mintlify compatibility). */
201
- color: bannerColorSchema.optional(),
202
187
  content: z.string(),
203
188
  /** Show a dismiss button; the choice is remembered per visitor. */
204
189
  dismissible: z.boolean().default(false),
@@ -208,8 +193,6 @@ const bannerConfigSchema = z.union([
208
193
  .object({ href: z.string(), text: z.string() })
209
194
  .strict()
210
195
  .optional(),
211
- /** Tone (Mintlify compatibility). */
212
- type: z.enum(["info", "warning", "critical"]).optional(),
213
196
  })
214
197
  .strict(),
215
198
  ]);
@@ -329,30 +312,6 @@ const githubReleasesSourceSchema = z
329
312
  })
330
313
  .strict();
331
314
 
332
- /**
333
- * In-place Mintlify content (`docs.json` + MDX). Powers "bridge mode": Blume
334
- * reads an unconverted Mintlify project, transforming each page to Blume MDX at
335
- * scan time. Injected automatically by `loadConfig` when a `docs.json` is found
336
- * and no `blume.config.*` exists; can also be configured explicitly.
337
- */
338
- const mintlifySourceSchema = z
339
- .object({
340
- /** Absolute path of the `docs.json`/`mint.json`, watched for changes in dev. */
341
- configFile: z.string().optional(),
342
- /** Patterns excluded from page discovery (Mintlify defaults are merged in). */
343
- exclude: z.array(z.string()).default([]),
344
- /** Glob patterns for Mintlify content files. */
345
- include: z.array(z.string()).default(["**/*.{md,mdx}"]),
346
- /** Namespaces the source's routes under `/<prefix>/`. */
347
- prefix: z.string().optional(),
348
- /** Content root, absolute or relative to the project root (Mintlify: `.`). */
349
- root: z.string().default("."),
350
- type: z.literal("mintlify"),
351
- /** `docs.json` variables, inlined into content (`{{name}}`) at scan time. */
352
- variables: z.record(z.string(), z.string()).default({}),
353
- })
354
- .strict();
355
-
356
315
  /**
357
316
  * A user-provided `ContentSource` instance, passed straight through from
358
317
  * `blume.config.ts`. This is the extension point that lets adapters with custom
@@ -377,7 +336,6 @@ const contentSourceSchema = z.discriminatedUnion("type", [
377
336
  githubReleasesSourceSchema,
378
337
  sanitySourceSchema,
379
338
  notionSourceSchema,
380
- mintlifySourceSchema,
381
339
  customSourceSchema,
382
340
  ]);
383
341
 
@@ -386,13 +344,6 @@ export type ContentSourceConfig = z.infer<typeof contentSourceSchema>;
386
344
 
387
345
  const contentConfigSchema = z
388
346
  .object({
389
- /**
390
- * Extra top-level directories (relative to the project root) served as
391
- * static assets at the site root, alongside `public/`. Lets projects keep
392
- * root-served asset folders in place — e.g. a Mintlify migration keeps
393
- * `images/` where it is instead of relocating it under `public/`.
394
- */
395
- assets: z.array(z.string()).default([]),
396
347
  defaultType: z.string().default("doc"),
397
348
  exclude: z.array(z.string()).default(["**/_*", "**/.*"]),
398
349
  include: z.array(z.string()).default(["**/*.{md,mdx}"]),
@@ -483,25 +434,42 @@ const sidebarItemSchema: z.ZodType<SidebarItemConfig> = z.lazy(() =>
483
434
  ])
484
435
  );
485
436
 
486
- const variablesConfigSchema = z
487
- .record(z.string().regex(/^[A-Za-z0-9-]+$/u), z.string())
488
- .default({});
489
-
490
437
  /** A curated Google Font slug (see `theme/fonts.ts`). */
491
438
  const fontSlug = z.string().refine(isFontSlug, (value) => ({
492
439
  message: `Unknown font "${value}". Supported fonts: ${FONT_SLUGS.join(", ")}.`,
493
440
  }));
494
441
 
442
+ /**
443
+ * An optional per-mode theme value: a string applies to both color modes; a
444
+ * `{ light, dark }` object sets each mode individually (either may be
445
+ * omitted to override a single mode).
446
+ */
447
+ const perModeValueSchema = z
448
+ .union([
449
+ z.string(),
450
+ z
451
+ .object({ dark: z.string().optional(), light: z.string().optional() })
452
+ .strict(),
453
+ ])
454
+ .optional()
455
+ .transform((value) =>
456
+ typeof value === "string" ? { dark: value, light: value } : value
457
+ );
458
+
495
459
  const themeConfigSchema = z
496
460
  .object({
497
- accent: z.string().default("blue"),
498
- accentDark: z.string().optional(),
461
+ accent: z
462
+ .union([
463
+ z.string(),
464
+ z.object({ dark: z.string(), light: z.string() }).strict(),
465
+ ])
466
+ .default("blue")
467
+ .transform((value) =>
468
+ typeof value === "string" ? { dark: value, light: value } : value
469
+ ),
499
470
  action: z.string().optional(),
500
- background: z.string().optional(),
501
- backgroundDark: z.string().optional(),
502
- backgroundDecoration: z.enum(["gradient", "grid", "windows"]).optional(),
503
- backgroundImage: z.string().optional(),
504
- backgroundImageDark: z.string().optional(),
471
+ background: perModeValueSchema,
472
+ backgroundImage: perModeValueSchema,
505
473
  fonts: z
506
474
  .object({
507
475
  body: fontSlug.default("inter"),
@@ -584,7 +552,6 @@ const searchConfigSchema = z
584
552
  .default({}),
585
553
  mixedbread: mixedbreadSearchSchema.optional(),
586
554
  oramaCloud: oramaCloudSearchSchema.optional(),
587
- prompt: z.string().optional(),
588
555
  provider: z.enum(searchProviders).default("orama"),
589
556
  typesense: typesenseSearchSchema.optional(),
590
557
  })
@@ -625,6 +592,18 @@ const aiConfigSchema = z
625
592
  enabled: z.boolean().default(false),
626
593
  model: z.string().default("openai/gpt-5.5"),
627
594
  provider: z.enum(askAiProviders).default("gateway"),
595
+ // Empty-state prompts shown before the first question. Each renders as a
596
+ // clickable suggestion; `icon` is an optional Lucide name beside it.
597
+ suggestions: z
598
+ .array(
599
+ z
600
+ .object({
601
+ icon: iconName.optional(),
602
+ label: z.string().min(1),
603
+ })
604
+ .strict()
605
+ )
606
+ .default([]),
628
607
  })
629
608
  .strict()
630
609
  .superRefine((value, ctx) => {
@@ -644,21 +623,48 @@ const aiConfigSchema = z
644
623
  })
645
624
  .strict();
646
625
 
647
- const chromeVariantSchema = z
626
+ /**
627
+ * A pinned link rendered above the sidebar sections — a blog, changelog, or
628
+ * contact page that should always be reachable, regardless of the active tab.
629
+ * `href` may be an external URL or an internal route.
630
+ */
631
+ const featuredLinkSchema = z
648
632
  .object({
649
- banner: bannerConfigSchema.optional(),
650
- path: z.string(),
633
+ href: z.string(),
634
+ icon: iconName.optional(),
635
+ label: z.string(),
651
636
  })
652
637
  .strict();
653
638
 
654
639
  const navigationConfigSchema = z
655
640
  .object({
656
- chromeVariants: z.array(chromeVariantSchema).default([]),
641
+ /** Pinned links shown above the generated sidebar sections. */
642
+ featured: z.array(featuredLinkSchema).default([]),
657
643
  /** Show a GitHub repo link in the header (requires `github` configured). */
658
644
  repo: z.boolean().default(true),
659
645
  selectors: z.array(navSelectorSchema).default([]),
660
- /** Explicit sidebar override; when omitted the sidebar is generated. */
661
- sidebar: z.array(sidebarItemSchema).optional(),
646
+ /**
647
+ * Sidebar behavior. `display` sets how every group renders (a group in an
648
+ * explicit `items` config may still override it); `items` is an explicit
649
+ * sidebar — when omitted the sidebar is generated from the content tree.
650
+ * A bare array is shorthand for `{ items }`.
651
+ */
652
+ sidebar: z
653
+ .union([
654
+ z.array(sidebarItemSchema),
655
+ z
656
+ .object({
657
+ display: sidebarDisplaySchema.default("flat"),
658
+ items: z.array(sidebarItemSchema).optional(),
659
+ })
660
+ .strict(),
661
+ ])
662
+ .default({})
663
+ .transform((value) =>
664
+ Array.isArray(value)
665
+ ? { display: "flat" as const, items: value }
666
+ : value
667
+ ),
662
668
  tabs: z.array(navTabSchema).optional(),
663
669
  })
664
670
  .strict();
@@ -821,10 +827,52 @@ const rssConfigSchema = z
821
827
  })
822
828
  .strict();
823
829
 
830
+ /**
831
+ * robots.txt `Content-Signal` preferences — the emerging content-usage
832
+ * declaration for how crawlers may reuse the site. Each field maps to one
833
+ * signal:
834
+ * - `search` → `search` (traditional and AI search indexing)
835
+ * - `aiInput` → `ai-input` (grounding / RAG use at answer time)
836
+ * - `aiTrain` → `ai-train` (model training)
837
+ */
838
+ const contentSignalsObjectSchema = z
839
+ .object({
840
+ aiInput: z.boolean().default(true),
841
+ aiTrain: z.boolean().default(true),
842
+ search: z.boolean().default(true),
843
+ })
844
+ .strict();
845
+
846
+ /**
847
+ * Content signals accept a boolean shorthand or a per-signal object, and
848
+ * normalize to `{ search, aiInput, aiTrain }` — or `null` when disabled, so
849
+ * robots.txt omits the declaration entirely. On by default (`true`): Blume
850
+ * declares the docs open to search and agents. `false` opts out; an object
851
+ * restricts individual signals (unset signals stay `yes`).
852
+ */
853
+ const contentSignalsSchema = z
854
+ .union([z.boolean(), contentSignalsObjectSchema])
855
+ .transform((value) => {
856
+ if (value === true) {
857
+ return contentSignalsObjectSchema.parse({});
858
+ }
859
+ if (value === false) {
860
+ return null;
861
+ }
862
+ return value;
863
+ });
864
+
824
865
  /** Discoverability features: OG images, feeds, sitemap, structured data. */
825
866
  const seoConfigSchema = z
826
867
  .object({
827
- metatags: z.record(z.string(), z.string()).default({}),
868
+ /**
869
+ * Emit `agent-readability.json` at the site root: a manifest that indexes
870
+ * the agent-facing surface (llms.txt, Markdown mirrors, MCP server, feeds)
871
+ * so agents can discover it without scraping HTML.
872
+ */
873
+ agentReadability: z.boolean().default(true),
874
+ /** robots.txt `Content-Signal` usage declaration (on by default). */
875
+ contentSignals: contentSignalsSchema.default(true),
828
876
  og: ogConfigSchema.default({}),
829
877
  /** Generate robots.txt (with a Sitemap reference when available). */
830
878
  robots: z.boolean().default(true),
@@ -1001,17 +1049,6 @@ const tocConfigSchema = z
1001
1049
  };
1002
1050
  });
1003
1051
 
1004
- /**
1005
- * Which icon library bare `icon` names resolve against (mirrors Mintlify's
1006
- * `icons.library`). Names can always opt into a specific set with an explicit
1007
- * `prefix:name` (`lucide:rocket`, `fa6-brands:github`) regardless of this.
1008
- */
1009
- const iconsConfigSchema = z
1010
- .object({
1011
- library: z.enum(["lucide", "fontawesome", "tabler"]).default("lucide"),
1012
- })
1013
- .strict();
1014
-
1015
1052
  export const blumeConfigSchema = z
1016
1053
  .object({
1017
1054
  ai: aiConfigSchema.default({}),
@@ -1036,11 +1073,9 @@ export const blumeConfigSchema = z
1036
1073
  */
1037
1074
  examples: z.string().default("examples"),
1038
1075
  export: exportConfigSchema.default(false),
1039
- favicon: faviconConfigSchema.optional(),
1040
1076
  feedback: z.boolean().default(true),
1041
1077
  github: githubConfigSchema.optional(),
1042
1078
  i18n: i18nConfigSchema.optional(),
1043
- icons: iconsConfigSchema.default({}),
1044
1079
  lastModified: lastModifiedConfigSchema.default(false),
1045
1080
  logo: logoConfigSchema.optional(),
1046
1081
  markdown: markdownConfigSchema.default({}),
@@ -1053,7 +1088,6 @@ export const blumeConfigSchema = z
1053
1088
  theme: themeConfigSchema.default({}),
1054
1089
  title: z.string().default("Documentation"),
1055
1090
  toc: tocConfigSchema,
1056
- variables: variablesConfigSchema,
1057
1091
  })
1058
1092
  .strict();
1059
1093
 
@@ -1067,3 +1101,7 @@ export type LocaleConfig = z.infer<typeof localeSchema>;
1067
1101
  export type BlumeConfig = z.input<typeof blumeConfigSchema>;
1068
1102
  /** A configured search backend. */
1069
1103
  export type SearchProvider = (typeof searchProviders)[number];
1104
+ /** Resolved robots.txt `Content-Signal` preferences (`null` when disabled). */
1105
+ export type ContentSignals = z.infer<typeof contentSignalsSchema>;
1106
+ /** The resolved per-signal policy object (present when signals are enabled). */
1107
+ export type ContentSignalPolicy = NonNullable<ContentSignals>;
@@ -8,6 +8,7 @@ import { BlumeError } from "../diagnostics.ts";
8
8
  import matter from "../frontmatter.ts";
9
9
  import type { ContentSource, SourceEntry, SourceLoadResult } from "./types.ts";
10
10
  import {
11
+ baselineScanIgnore,
11
12
  BLUME_WATCH_IGNORE_DIRS,
12
13
  excludeDirSegments,
13
14
  ignoringWatchListener,
@@ -45,7 +46,10 @@ export const filesystemSource = (
45
46
  const files = await glob(options.include, {
46
47
  absolute: true,
47
48
  cwd: contentRoot,
48
- ignore: options.exclude,
49
+ // Union the user's `exclude` with the baseline never-content dirs so a
50
+ // broadly-scoped root (`.` or an app dir) can't glob `node_modules`,
51
+ // `dist`, `.blume`, etc. — even when the user overrode `exclude`.
52
+ ignore: [...options.exclude, ...baselineScanIgnore()],
49
53
  onlyFiles: true,
50
54
  });
51
55
  files.sort();
@@ -7,7 +7,6 @@ import type { ProjectContext } from "../types.ts";
7
7
  import { filesystemSource } from "./filesystem.ts";
8
8
  import { githubReleasesSource } from "./github-releases.ts";
9
9
  import { mdxRemoteSource } from "./mdx-remote.ts";
10
- import { mintlifySource } from "./mintlify.ts";
11
10
  import { notionSource } from "./notion.ts";
12
11
  import { sanitySource } from "./sanity.ts";
13
12
  import type { ContentSource, SourceContext } from "./types.ts";
@@ -64,18 +63,6 @@ const buildSource = (
64
63
  root: def.root,
65
64
  });
66
65
  }
67
- if (def.type === "mintlify") {
68
- return mintlifySource({
69
- configFile: def.configFile,
70
- exclude: def.exclude,
71
- include: def.include,
72
- name,
73
- prefix: def.prefix,
74
- projectRoot: context.root,
75
- root: def.root,
76
- variables: def.variables,
77
- });
78
- }
79
66
  if (def.type === "custom") {
80
67
  // A user-provided instance manages its own context/caching; we only ensure
81
68
  // its name is unique across the project for id namespacing.
@@ -1,19 +1,51 @@
1
1
  import type { WatchListener } from "node:fs";
2
2
 
3
3
  /**
4
- * Directory segments a recursive dev watcher must never react to. When a
5
- * source's content root is the project root — a migrated `.`-rooted project or a
6
- * Mintlify bridge — a naive recursive `fs.watch` also sees Blume's own `.blume/`
7
- * output, which the dev server rewrites on every render (e.g.
8
- * `.blume/.astro/data-store.json`). Left unfiltered, each such write re-triggers
9
- * a rescan + runtime regeneration whose writes land back under `.blume/` and
10
- * fire the watcher again: a self-sustaining loop that stalls page renders and
11
- * floods the console (and, mid-render, corrupts Astro's dev module graph so
12
- * `astro:server-app.js` fails to load). `.git`/`node_modules` are here for the
13
- * same reason — churn that is never page content. `fs.watch` has no ignore
4
+ * Directory segments that are never authored content: VCS, dependency trees,
5
+ * Blume's own generated project and build output, and framework/deploy caches.
6
+ * Both the content scan and the dev watcher skip these unconditionally, on top
7
+ * of the user's `content.exclude`.
8
+ *
9
+ * The scan needs them because a broadly-scoped `content.root` — `"."` or an app
10
+ * dir that also holds `node_modules`/`dist`, the common shape when migrating a
11
+ * docs app that lives at the repo or app root — would otherwise glob thousands
12
+ * of stray markdown files out of dependencies and build artifacts. `content.root`
13
+ * defaults to `docs/` where this rarely bites, but any wider root hits it.
14
+ *
15
+ * The watcher needs them because a recursive `fs.watch` rooted at the project
16
+ * also sees Blume's own `.blume/` output, which the dev server rewrites on every
17
+ * render (e.g. `.blume/.astro/data-store.json`). Left unfiltered, each such write
18
+ * re-triggers a rescan + runtime regeneration whose writes land back under
19
+ * `.blume/` and fire the watcher again: a self-sustaining loop that stalls page
20
+ * renders and floods the console (and, mid-render, corrupts Astro's dev module
21
+ * graph so `astro:server-app.js` fails to load). `fs.watch` has no ignore
14
22
  * option, so we filter by the changed path in the callback.
15
23
  */
16
- export const BLUME_WATCH_IGNORE_DIRS = [".blume", ".git", "node_modules"];
24
+ export const BLUME_IGNORE_DIRS = [
25
+ ".blume",
26
+ ".cache",
27
+ ".git",
28
+ ".next",
29
+ ".turbo",
30
+ ".vercel",
31
+ "dist",
32
+ "node_modules",
33
+ ];
34
+
35
+ /**
36
+ * Baseline scan-ignore globs applied to every filesystem source, unioned with
37
+ * the user's `content.exclude`. Kept in sync with the watcher via the shared
38
+ * {@link BLUME_IGNORE_DIRS} so `load()` and `watch()` never disagree on what is
39
+ * content. `**\/<dir>/**` matches the directory at the content root or nested.
40
+ */
41
+ export const baselineScanIgnore = (): string[] =>
42
+ BLUME_IGNORE_DIRS.map((dir) => `**/${dir}/**`);
43
+
44
+ /**
45
+ * Alias retained for the dev watcher's call site and its tests; the watcher and
46
+ * the scan share the same never-content directory set.
47
+ */
48
+ export const BLUME_WATCH_IGNORE_DIRS = BLUME_IGNORE_DIRS;
17
49
 
18
50
  /** Extract single-segment ignore dirs (`foo`) from `foo/**`-style excludes. */
19
51
  export const excludeDirSegments = (patterns: readonly string[]): string[] =>
package/src/core/types.ts CHANGED
@@ -173,25 +173,27 @@ export interface NavSelectorItem {
173
173
  tag?: string;
174
174
  }
175
175
 
176
- /** Top-level Mintlify-style partition selectors. */
176
+ /** Top-level partition selectors (products, versions, languages). */
177
177
  export interface NavSelector {
178
178
  label: string;
179
179
  kind: "dropdown" | "language" | "product" | "version";
180
180
  items: NavSelectorItem[];
181
181
  }
182
182
 
183
- /** Chrome overrides used when the current route belongs to a nav partition. */
184
- export interface NavChromeVariant {
185
- path: string;
186
- banner?: ResolvedConfig["banner"];
183
+ /** A pinned link rendered above the sidebar sections (external or internal). */
184
+ export interface FeaturedLink {
185
+ label: string;
186
+ href: string;
187
+ icon?: string;
187
188
  }
188
189
 
189
190
  /** The complete navigation model derived from the content graph. */
190
191
  export interface Navigation {
191
192
  tabs: NavTab[];
192
193
  selectors: NavSelector[];
193
- chromeVariants: NavChromeVariant[];
194
194
  sidebar: NavNode[];
195
+ /** Pinned links shown above the sidebar sections, unscoped by tab. */
196
+ featured: FeaturedLink[];
195
197
  /** Repo URL for the header link, or null when hidden (`navigation.repo`). */
196
198
  repoUrl?: string | null;
197
199
  }
@@ -1,9 +1,36 @@
1
1
  import type { BlumeProject } from "../core/project-graph.ts";
2
+ import type { ContentSignalPolicy, ContentSignals } from "../core/schema.ts";
2
3
 
3
4
  /**
4
- * Build a robots.txt that allows all crawlers and points to the sitemap when
5
- * one is available (a `site` is set and the sitemap is enabled). Returns null
6
- * when robots generation is disabled.
5
+ * Ordered mapping from config field to its `Content-Signal` token. The order
6
+ * fixes the emitted sequence (`search`, then `ai-input`, then `ai-train`).
7
+ */
8
+ const SIGNAL_TOKENS: [keyof ContentSignalPolicy, string][] = [
9
+ ["search", "search"],
10
+ ["aiInput", "ai-input"],
11
+ ["aiTrain", "ai-train"],
12
+ ];
13
+
14
+ /**
15
+ * The `Content-Signal:` line declaring how crawlers may reuse the site, or null
16
+ * when the declaration is disabled (`contentSignals: false`). Otherwise every
17
+ * signal is emitted with its resolved yes/no value.
18
+ */
19
+ const contentSignalLine = (signals: ContentSignals): string | null => {
20
+ if (!signals) {
21
+ return null;
22
+ }
23
+ const tokens = SIGNAL_TOKENS.map(
24
+ ([key, token]) => `${token}=${signals[key] ? "yes" : "no"}`
25
+ );
26
+ return `Content-Signal: ${tokens.join(", ")}`;
27
+ };
28
+
29
+ /**
30
+ * Build a robots.txt that allows all crawlers, declares any configured
31
+ * `Content-Signal` usage preferences, and points to the sitemap when one is
32
+ * available (a `site` is set and the sitemap is enabled). Returns null when
33
+ * robots generation is disabled.
7
34
  */
8
35
  export const buildRobots = (project: BlumeProject): string | null => {
9
36
  const { config } = project;
@@ -11,7 +38,13 @@ export const buildRobots = (project: BlumeProject): string | null => {
11
38
  return null;
12
39
  }
13
40
 
14
- const lines = ["User-agent: *", "Allow: /"];
41
+ const lines = ["User-agent: *"];
42
+ const signal = contentSignalLine(config.seo.contentSignals);
43
+ if (signal) {
44
+ lines.push(signal);
45
+ }
46
+ lines.push("Allow: /");
47
+
15
48
  const { site } = config.deployment;
16
49
  if (site && config.seo.sitemap) {
17
50
  lines.push("", `Sitemap: ${site.replace(/\/$/u, "")}/sitemap.xml`);