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.
- package/dist/cli/index.js +3349 -7024
- package/dist/cli/index.js.map +39 -69
- package/dist/types/core/config.d.ts +0 -8
- package/dist/types/core/data.d.ts +6 -2
- package/dist/types/core/i18n-ui.d.ts +50 -0
- package/dist/types/core/schema.d.ts +379 -485
- package/dist/types/core/types.d.ts +8 -6
- package/docs/advanced/meta.ts +1 -8
- package/docs/advanced/skills.mdx +28 -0
- package/docs/configuration/ai.mdx +58 -0
- package/docs/configuration/index.mdx +13 -17
- package/docs/configuration/seo.mdx +59 -1
- package/docs/configuration/theming.mdx +15 -18
- package/docs/content/components.mdx +2 -53
- package/docs/content/i18n.mdx +0 -4
- package/docs/content/meta.mdx +3 -17
- package/docs/content/navigation.mdx +41 -4
- package/docs/content/syntax.mdx +1 -1
- package/docs/index.mdx +0 -1
- package/docs/reference/cli.mdx +12 -13
- package/package.json +6 -6
- package/skills/blume/SKILL.md +71 -0
- package/skills/blume-update-docs/SKILL.md +52 -0
- package/skills/blume-update-docs/references/audit-checklist.md +46 -0
- package/src/ai/agent-readability.ts +97 -0
- package/src/ai/ask-context.ts +131 -8
- package/src/ai/ask-data.ts +4 -1
- package/src/astro/generate.ts +19 -12
- package/src/astro/integration.ts +0 -21
- package/src/astro/templates.ts +33 -21
- package/src/cli/commands/build.ts +15 -0
- package/src/cli/commands/dev.ts +31 -20
- package/src/cli/commands/validate.ts +0 -2
- package/src/cli/dev-lock.ts +94 -21
- package/src/cli/index.ts +0 -2
- package/src/components/BlumePage.astro +0 -6
- package/src/components/Icon.astro +1 -12
- package/src/components/content/AccordionItem.astro +3 -6
- package/src/components/content/Badge.astro +1 -3
- package/src/components/content/Callout.astro +3 -9
- package/src/components/content/Card.astro +2 -3
- package/src/components/content/ColorItem.astro +2 -2
- package/src/components/content/Column.astro +1 -1
- package/src/components/content/GithubInfo.astro +11 -10
- package/src/components/content/Prompt.astro +1 -1
- package/src/components/content/Step.astro +3 -4
- package/src/components/content/Tab.astro +2 -3
- package/src/components/content/TypeTable.astro +13 -8
- package/src/components/content/Update.astro +1 -1
- package/src/components/islands/AskAI.astro +66 -2
- package/src/components/islands/ask-ai.tsx +289 -53
- package/src/components/layout/Header.astro +27 -4
- package/src/components/layout/Logo.astro +5 -1
- package/src/components/layout/NavSelector.astro +1 -1
- package/src/components/layout/NavTree.astro +15 -15
- package/src/components/layout/PageActions.astro +73 -30
- package/src/components/layout/PageLayout.astro +42 -0
- package/src/components/layout/ReferenceLayout.astro +1 -0
- package/src/components/layout/RootLayout.astro +79 -4
- package/src/components/layout/Search.astro +5 -5
- package/src/components/layout/nav-utils.ts +9 -4
- package/src/components/openapi/ApiOverview.astro +4 -50
- package/src/components/openapi/ApiTagOperations.astro +42 -0
- package/src/core/builtin-tags.ts +1 -3
- package/src/core/config.ts +5 -28
- package/src/core/data.ts +6 -2
- package/src/core/graph.ts +8 -6
- package/src/core/i18n-ui.ts +5 -0
- package/src/core/links.ts +5 -19
- package/src/core/meta.ts +1 -1
- package/src/core/nav-diagnostics.ts +7 -0
- package/src/core/navigation.ts +38 -17
- package/src/core/project-graph.ts +0 -5
- package/src/core/schema.ts +133 -95
- package/src/core/sources/filesystem.ts +5 -1
- package/src/core/sources/resolve.ts +0 -13
- package/src/core/sources/watch.ts +43 -11
- package/src/core/types.ts +8 -6
- package/src/deploy/robots.ts +37 -4
- package/src/openapi/parse.ts +197 -14
- package/src/openapi/render-mdx.ts +44 -10
- package/src/openapi/scalar.ts +1 -1
- package/src/openapi/source.ts +19 -2
- package/src/search/documents.ts +9 -2
- package/src/theme/entry.ts +45 -17
- package/src/theme/icons.ts +18 -109
- package/src/theme/palette.ts +25 -51
- package/src/theme/twoslash.ts +6 -1
- package/dist/types/core/bridge.d.ts +0 -24
- package/dist/types/core/package-json.d.ts +0 -12
- package/dist/types/migrate/mintlify/assets.d.ts +0 -8
- package/dist/types/migrate/mintlify/config.d.ts +0 -16
- package/dist/types/migrate/mintlify/i18n.d.ts +0 -7
- package/dist/types/migrate/shared.d.ts +0 -153
- package/docs/advanced/bridge.mdx +0 -76
- package/docs/advanced/migrate.mdx +0 -124
- package/src/astro/static-assets.ts +0 -124
- package/src/cli/commands/migrate.ts +0 -39
- package/src/components/content/ApiField.astro +0 -75
- package/src/components/content/ParamField.astro +0 -39
- package/src/components/content/RequestField.astro +0 -23
- package/src/components/content/ResponseField.astro +0 -23
- package/src/components/content/Warning.astro +0 -9
- package/src/core/assets.ts +0 -31
- package/src/core/bridge.ts +0 -102
- package/src/core/sources/mintlify.ts +0 -190
- package/src/migrate/fumadocs/config.ts +0 -155
- package/src/migrate/fumadocs/content.ts +0 -376
- package/src/migrate/fumadocs/frontmatter.ts +0 -18
- package/src/migrate/fumadocs/groups.ts +0 -237
- package/src/migrate/fumadocs/index.ts +0 -355
- package/src/migrate/fumadocs/meta.ts +0 -244
- package/src/migrate/migrate.ts +0 -53
- package/src/migrate/mintlify/assets.ts +0 -46
- package/src/migrate/mintlify/config.ts +0 -954
- package/src/migrate/mintlify/content.ts +0 -120
- package/src/migrate/mintlify/frontmatter.ts +0 -126
- package/src/migrate/mintlify/i18n.ts +0 -51
- package/src/migrate/mintlify/icons.ts +0 -128
- package/src/migrate/mintlify/index.ts +0 -459
- package/src/migrate/mintlify/snippets.ts +0 -315
- package/src/migrate/mintlify/transform.ts +0 -82
- package/src/migrate/nextra/content.ts +0 -46
- package/src/migrate/nextra/frontmatter.ts +0 -40
- package/src/migrate/nextra/index.ts +0 -389
- package/src/migrate/nextra/meta.ts +0 -266
- package/src/migrate/shared.ts +0 -801
- package/src/migrate/starlight/config.ts +0 -455
- package/src/migrate/starlight/content.ts +0 -75
- package/src/migrate/starlight/frontmatter.ts +0 -111
- package/src/migrate/starlight/i18n.ts +0 -54
- package/src/migrate/starlight/index.ts +0 -131
package/src/core/schema.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
180
|
-
|
|
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
|
|
498
|
-
|
|
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:
|
|
501
|
-
|
|
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
|
-
|
|
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
|
-
|
|
650
|
-
|
|
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
|
-
|
|
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
|
-
/**
|
|
661
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
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
|
|
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
|
|
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
|
-
/**
|
|
184
|
-
export interface
|
|
185
|
-
|
|
186
|
-
|
|
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
|
}
|
package/src/deploy/robots.ts
CHANGED
|
@@ -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
|
-
*
|
|
5
|
-
*
|
|
6
|
-
|
|
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: *"
|
|
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`);
|