blume 1.3.1 → 1.4.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/CHANGELOG.md +50 -0
- package/dist/cli/index.js +3221 -201
- package/dist/cli/index.js.map +73 -61
- package/dist/types/core/base-path.d.ts +5 -0
- package/dist/types/core/config-input.d.ts +82 -6
- package/dist/types/core/i18n-ui.d.ts +2 -0
- package/dist/types/core/schema.d.ts +19 -2
- package/dist/types/core/sources/types.d.ts +5 -0
- package/dist/types/core/types.d.ts +4 -3
- package/docs/02-deployment.mdx +1 -1
- package/docs/configuration/ai.mdx +15 -1
- package/docs/configuration/index.mdx +26 -0
- package/docs/configuration/search.mdx +1 -3
- package/docs/content/i18n.mdx +13 -1
- package/docs/content/navigation.mdx +11 -0
- package/docs/reference/cli.mdx +4 -0
- package/docs/reference/frontmatter.mdx +33 -0
- package/docs/reference/meta.ts +1 -1
- package/docs/reference/translate.mdx +80 -0
- package/package.json +1 -1
- package/src/ai/agent-readability.ts +7 -4
- package/src/ai/ask-context.ts +3 -6
- package/src/ai/mcp/data.ts +10 -4
- package/src/ai/mcp/server.ts +74 -3
- package/src/ai/mcp/tools.ts +2 -2
- package/src/astro/integration.ts +3 -1
- package/src/astro/markdown-negotiation.ts +5 -0
- package/src/astro/templates.ts +66 -18
- package/src/audit/url.ts +5 -10
- package/src/cli/commands/build.ts +129 -24
- package/src/cli/commands/translate.ts +300 -0
- package/src/cli/index.ts +2 -0
- package/src/components/Icon.astro +2 -7
- package/src/components/content/Step.astro +3 -8
- package/src/components/content/Tab.astro +20 -1
- package/src/components/layout/LanguageSwitcher.astro +2 -1
- package/src/components/layout/Logo.astro +4 -4
- package/src/components/layout/PageActions.astro +12 -7
- package/src/components/layout/Search.astro +15 -20
- package/src/components/layout/search/orama.ts +3 -1
- package/src/core/base-path.ts +9 -0
- package/src/core/config-input.ts +84 -6
- package/src/core/graph.ts +46 -2
- package/src/core/i18n-ui.ts +2 -0
- package/src/core/i18n.ts +31 -0
- package/src/core/nav-diagnostics.ts +13 -34
- package/src/core/project-graph.ts +13 -2
- package/src/core/schema.ts +174 -74
- package/src/core/sources/normalize.ts +25 -12
- package/src/core/sources/types.ts +5 -0
- package/src/core/types.ts +4 -3
- package/src/core/ui-packs/ar.ts +42 -1
- package/src/core/ui-packs/bg.ts +42 -1
- package/src/core/ui-packs/bn.ts +42 -1
- package/src/core/ui-packs/ca.ts +44 -1
- package/src/core/ui-packs/cs.ts +42 -1
- package/src/core/ui-packs/da.ts +42 -1
- package/src/core/ui-packs/de.ts +42 -1
- package/src/core/ui-packs/el.ts +44 -1
- package/src/core/ui-packs/es.ts +44 -1
- package/src/core/ui-packs/fa.ts +42 -1
- package/src/core/ui-packs/fi.ts +42 -1
- package/src/core/ui-packs/fr.ts +44 -1
- package/src/core/ui-packs/he.ts +42 -1
- package/src/core/ui-packs/hi.ts +42 -1
- package/src/core/ui-packs/hr.ts +42 -1
- package/src/core/ui-packs/hu.ts +42 -1
- package/src/core/ui-packs/id.ts +42 -1
- package/src/core/ui-packs/it.ts +44 -1
- package/src/core/ui-packs/ja.ts +44 -1
- package/src/core/ui-packs/ko.ts +44 -1
- package/src/core/ui-packs/nl.ts +42 -1
- package/src/core/ui-packs/no.ts +42 -1
- package/src/core/ui-packs/pl.ts +42 -1
- package/src/core/ui-packs/pt-br.ts +44 -1
- package/src/core/ui-packs/pt.ts +44 -1
- package/src/core/ui-packs/ro.ts +42 -1
- package/src/core/ui-packs/ru.ts +42 -1
- package/src/core/ui-packs/sk.ts +42 -1
- package/src/core/ui-packs/sr.ts +42 -1
- package/src/core/ui-packs/sv.ts +42 -1
- package/src/core/ui-packs/th.ts +44 -1
- package/src/core/ui-packs/tr.ts +42 -1
- package/src/core/ui-packs/uk.ts +42 -1
- package/src/core/ui-packs/vi.ts +44 -1
- package/src/core/ui-packs/zh-tw.ts +44 -1
- package/src/core/ui-packs/zh.ts +44 -1
- package/src/deploy/adapter-output.ts +44 -5
- package/src/deploy/cloudflare-negotiation.ts +527 -0
- package/src/deploy/redirects.ts +13 -0
- package/src/eval/agents.ts +1 -1
- package/src/search/documents.ts +11 -0
- package/src/search/facets.ts +33 -0
- package/src/search/orama-index.ts +48 -6
- package/src/search/popular-icon.ts +33 -0
- package/src/theme/icon-kind.ts +20 -0
- package/src/translate/agents.ts +51 -0
- package/src/translate/ledger.ts +148 -0
- package/src/translate/meta.ts +149 -0
- package/src/translate/prompts.ts +95 -0
- package/src/translate/report.ts +360 -0
- package/src/translate/run.ts +376 -0
- package/src/translate/validate.ts +171 -0
- package/src/translate/work-list.ts +0 -0
package/src/core/config-input.ts
CHANGED
|
@@ -269,20 +269,88 @@ export interface ContentConfig {
|
|
|
269
269
|
* Releases, Sanity, Notion, or a custom `ContentSource`.
|
|
270
270
|
*/
|
|
271
271
|
sources?: ContentSourceInput[];
|
|
272
|
+
/**
|
|
273
|
+
* Per-type content definitions, keyed by the frontmatter `type` they apply
|
|
274
|
+
* to (including `defaultType`, for pages that set none):
|
|
275
|
+
*
|
|
276
|
+
* ```ts
|
|
277
|
+
* import { z } from "zod";
|
|
278
|
+
*
|
|
279
|
+
* content: {
|
|
280
|
+
* types: {
|
|
281
|
+
* rfc: {
|
|
282
|
+
* frontmatter: {
|
|
283
|
+
* domain: z.string(),
|
|
284
|
+
* status: z.enum(["draft", "enforced"]),
|
|
285
|
+
* },
|
|
286
|
+
* },
|
|
287
|
+
* },
|
|
288
|
+
* },
|
|
289
|
+
* ```
|
|
290
|
+
*/
|
|
291
|
+
types?: Record<string, ContentTypeConfig>;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* A per-type content definition: configuration that applies only to pages
|
|
296
|
+
* whose resolved frontmatter `type` matches the map key.
|
|
297
|
+
*/
|
|
298
|
+
export interface ContentTypeConfig {
|
|
299
|
+
/**
|
|
300
|
+
* Custom frontmatter keys whose values become filterable facets for pages
|
|
301
|
+
* of this type. Faceted values ride along on search documents
|
|
302
|
+
* (`blume-search.json` and the MCP index), and the MCP `search_docs` and
|
|
303
|
+
* `list_pages` tools accept a `filters` input matching against them:
|
|
304
|
+
*
|
|
305
|
+
* ```ts
|
|
306
|
+
* content: {
|
|
307
|
+
* types: {
|
|
308
|
+
* rfc: {
|
|
309
|
+
* facets: ["domain", "status"],
|
|
310
|
+
* frontmatter: { domain: z.string(), status: z.string() },
|
|
311
|
+
* },
|
|
312
|
+
* },
|
|
313
|
+
* },
|
|
314
|
+
* ```
|
|
315
|
+
*
|
|
316
|
+
* Each name must be a custom key declared for the type — in its
|
|
317
|
+
* `frontmatter` map or the site-wide `frontmatter.extend`. String values
|
|
318
|
+
* facet as-is; numbers and booleans are stringified; anything else
|
|
319
|
+
* (objects, arrays, transformed dates) does not facet.
|
|
320
|
+
*/
|
|
321
|
+
facets?: string[];
|
|
322
|
+
/**
|
|
323
|
+
* Custom frontmatter keys for pages of this type, layered on top of the
|
|
324
|
+
* site-wide `frontmatter.extend` (a key can be declared in one or the
|
|
325
|
+
* other, not both). Schemas follow the same rules as `extend`: any
|
|
326
|
+
* Standard Schema library works, every declared key is validated on every
|
|
327
|
+
* page of the type — absent ones included — so a required schema enforces
|
|
328
|
+
* the key type-wide (mark it `.optional()` to validate only when present),
|
|
329
|
+
* and validated values land on the page record's `custom` field. Built-in
|
|
330
|
+
* frontmatter fields cannot be redeclared.
|
|
331
|
+
*/
|
|
332
|
+
frontmatter?: Record<string, StandardSchema>;
|
|
272
333
|
}
|
|
273
334
|
|
|
274
335
|
// ---------------------------------------------------------------------------
|
|
275
336
|
// Navigation
|
|
276
337
|
// ---------------------------------------------------------------------------
|
|
277
338
|
|
|
339
|
+
/**
|
|
340
|
+
* A header label, optionally per locale: a plain string, or a map of locale
|
|
341
|
+
* code to label (`{ en: "Docs", ja: "ドキュメント" }`). The active locale's
|
|
342
|
+
* entry wins, then the default locale's, then the map's first entry.
|
|
343
|
+
*/
|
|
344
|
+
export type LocalizableLabel = string | Record<string, string>;
|
|
345
|
+
|
|
278
346
|
/** A single item inside a header tab's dropdown. */
|
|
279
347
|
export interface NavTabItem {
|
|
280
348
|
/** Secondary line under the label. */
|
|
281
349
|
description?: string;
|
|
282
350
|
/** Lucide icon name shown beside the label. */
|
|
283
351
|
icon?: string;
|
|
284
|
-
/** Item label. */
|
|
285
|
-
label:
|
|
352
|
+
/** Item label, optionally per locale. */
|
|
353
|
+
label: LocalizableLabel;
|
|
286
354
|
/** Route the item links to. */
|
|
287
355
|
path: string;
|
|
288
356
|
/** Short tag/pill (e.g. `New`, `Beta`). */
|
|
@@ -304,8 +372,8 @@ export interface NavTab {
|
|
|
304
372
|
icon?: string;
|
|
305
373
|
/** Dropdown items; omit for a plain link tab. */
|
|
306
374
|
items?: NavTabItem[];
|
|
307
|
-
/** Tab label. */
|
|
308
|
-
label:
|
|
375
|
+
/** Tab label, optionally per locale. */
|
|
376
|
+
label: LocalizableLabel;
|
|
309
377
|
/** Route the tab links to. */
|
|
310
378
|
path: string;
|
|
311
379
|
}
|
|
@@ -512,7 +580,10 @@ export interface MixedbreadSearch {
|
|
|
512
580
|
export interface SearchPopularLink {
|
|
513
581
|
/** Internal route or external URL. */
|
|
514
582
|
href: string;
|
|
515
|
-
/**
|
|
583
|
+
/**
|
|
584
|
+
* Icon shown beside the label — a built-in name, image path/URL, or inline
|
|
585
|
+
* SVG (same as nav icons). Defaults to the file glyph.
|
|
586
|
+
*/
|
|
516
587
|
icon?: string;
|
|
517
588
|
/** Link label shown in the dialog. */
|
|
518
589
|
label: string;
|
|
@@ -728,6 +799,12 @@ export interface LocaleConfigInput {
|
|
|
728
799
|
dir?: "ltr" | "rtl";
|
|
729
800
|
/** Human-readable name shown in the switcher. */
|
|
730
801
|
label: string;
|
|
802
|
+
/**
|
|
803
|
+
* Freeform style guidance for `blume translate`, e.g. "Brazilian
|
|
804
|
+
* Portuguese, informal você". Pins register and dialect from the first
|
|
805
|
+
* translation and wins over an existing translation's style on reruns.
|
|
806
|
+
*/
|
|
807
|
+
style?: string;
|
|
731
808
|
}
|
|
732
809
|
|
|
733
810
|
/**
|
|
@@ -1145,7 +1222,8 @@ export interface FrontmatterConfig {
|
|
|
1145
1222
|
* so a required schema enforces the key site-wide; mark it `.optional()`
|
|
1146
1223
|
* to validate only when present. Validated values are preserved on each
|
|
1147
1224
|
* page record's `custom` field. Built-in frontmatter fields cannot be
|
|
1148
|
-
* redeclared.
|
|
1225
|
+
* redeclared. To scope a key to one content type instead, declare it under
|
|
1226
|
+
* `content.types.<type>.frontmatter`.
|
|
1149
1227
|
*/
|
|
1150
1228
|
extend?: Record<string, StandardSchema>;
|
|
1151
1229
|
}
|
package/src/core/graph.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { validateNavIcons, validateNavStructure } from "./nav-diagnostics.ts";
|
|
|
4
4
|
import { buildNavigation } from "./navigation.ts";
|
|
5
5
|
import type {
|
|
6
6
|
FolderMeta,
|
|
7
|
+
LocalizableLabel,
|
|
7
8
|
ResolvedConfig,
|
|
8
9
|
ResolvedI18nConfig,
|
|
9
10
|
} from "./schema.ts";
|
|
@@ -11,6 +12,7 @@ import type {
|
|
|
11
12
|
ContentGraph,
|
|
12
13
|
Diagnostic,
|
|
13
14
|
Navigation,
|
|
15
|
+
NavTab,
|
|
14
16
|
PageRecord,
|
|
15
17
|
} from "./types.ts";
|
|
16
18
|
|
|
@@ -79,6 +81,42 @@ const localePagesFor = (
|
|
|
79
81
|
return [...real, ...filled];
|
|
80
82
|
};
|
|
81
83
|
|
|
84
|
+
/**
|
|
85
|
+
* Resolve a possibly-per-locale header label to the string a locale renders:
|
|
86
|
+
* the active locale's entry, else the default locale's, else the map's first
|
|
87
|
+
* entry (which is also what a single-locale site gets).
|
|
88
|
+
*/
|
|
89
|
+
const resolveLabel = (
|
|
90
|
+
label: LocalizableLabel,
|
|
91
|
+
locale: string,
|
|
92
|
+
defaultLocale?: string
|
|
93
|
+
): string => {
|
|
94
|
+
if (typeof label === "string") {
|
|
95
|
+
return label;
|
|
96
|
+
}
|
|
97
|
+
return (
|
|
98
|
+
label[locale] ??
|
|
99
|
+
(defaultLocale === undefined ? undefined : label[defaultLocale]) ??
|
|
100
|
+
Object.values(label)[0] ??
|
|
101
|
+
""
|
|
102
|
+
);
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
/** Resolve every localizable label in the configured tabs for one locale. */
|
|
106
|
+
const resolveTabLabels = (
|
|
107
|
+
tabs: BuildContentGraphOptions["navigation"]["tabs"],
|
|
108
|
+
locale: string,
|
|
109
|
+
defaultLocale?: string
|
|
110
|
+
): NavTab[] =>
|
|
111
|
+
(tabs ?? []).map((tab) => ({
|
|
112
|
+
...tab,
|
|
113
|
+
items: tab.items?.map((item) => ({
|
|
114
|
+
...item,
|
|
115
|
+
label: resolveLabel(item.label, locale, defaultLocale),
|
|
116
|
+
})),
|
|
117
|
+
label: resolveLabel(tab.label, locale, defaultLocale),
|
|
118
|
+
}));
|
|
119
|
+
|
|
82
120
|
/** Build one locale's navigation tree from its own pages and folder meta. */
|
|
83
121
|
const buildLocaleNavigation = (
|
|
84
122
|
code: string,
|
|
@@ -95,7 +133,11 @@ const buildLocaleNavigation = (
|
|
|
95
133
|
// selector's items intentionally target specific locales.
|
|
96
134
|
const localizePath = (path: string): string =>
|
|
97
135
|
path.startsWith("/") ? localizeRoute(path, code, i18n) : path;
|
|
98
|
-
const tabs =
|
|
136
|
+
const tabs = resolveTabLabels(
|
|
137
|
+
options.navigation.tabs,
|
|
138
|
+
code,
|
|
139
|
+
i18n.defaultLocale
|
|
140
|
+
).map((tab) => ({
|
|
99
141
|
...tab,
|
|
100
142
|
...(tab.href ? { href: localizePath(tab.href) } : {}),
|
|
101
143
|
items: tab.items?.map((item) => ({
|
|
@@ -220,7 +262,9 @@ export const buildContentGraph = (
|
|
|
220
262
|
selectors: options.navigation.selectors,
|
|
221
263
|
sharedFolderMeta: options.sharedFolderMeta,
|
|
222
264
|
sidebar: options.navigation.sidebar.items,
|
|
223
|
-
|
|
265
|
+
// No locale to prefer: a per-locale label map resolves to its first
|
|
266
|
+
// entry on a single-locale site.
|
|
267
|
+
tabs: resolveTabLabels(options.navigation.tabs, ""),
|
|
224
268
|
}),
|
|
225
269
|
navigationByLocale: {} as Record<string, Navigation>,
|
|
226
270
|
};
|
package/src/core/i18n-ui.ts
CHANGED
|
@@ -28,6 +28,8 @@ const uiStringsObject = z.object({
|
|
|
28
28
|
exportEpub: z.string().default("Export to EPUB"),
|
|
29
29
|
exportPdf: z.string().default("Export to PDF"),
|
|
30
30
|
generating: z.string().default("Generating…"),
|
|
31
|
+
// `{name}` is replaced with the provider's brand name at render time.
|
|
32
|
+
openIn: z.string().default("Open in {name}"),
|
|
31
33
|
openInChat: z.string().default("Open in chat"),
|
|
32
34
|
scrollToTop: z.string().default("Scroll to top"),
|
|
33
35
|
})
|
package/src/core/i18n.ts
CHANGED
|
@@ -148,6 +148,37 @@ export const localePlacement = (
|
|
|
148
148
|
return { locales: [locale], navPath: rest.join("/") };
|
|
149
149
|
};
|
|
150
150
|
|
|
151
|
+
/**
|
|
152
|
+
* The inverse of {@link localePlacement}: where a default-locale file's
|
|
153
|
+
* translation into `locale` lives, by parser:
|
|
154
|
+
* - `dir`: a leading locale directory (`guides/x.mdx` → `fr/guides/x.mdx`)
|
|
155
|
+
* - `dot`: a filename suffix (`guides/x.mdx` → `guides/x.fr.mdx`)
|
|
156
|
+
*
|
|
157
|
+
* Under `dot`, a source authored with an explicit default-locale suffix
|
|
158
|
+
* (`x.en.mdx`) swaps it for the target's (`x.fr.mdx`) — same case-insensitive
|
|
159
|
+
* last-dot-inside-filename logic as `localePlacement`, so both spellings of a
|
|
160
|
+
* default-locale file resolve to one canonical target.
|
|
161
|
+
*/
|
|
162
|
+
export const localeTargetPath = (
|
|
163
|
+
rel: string,
|
|
164
|
+
ext: string,
|
|
165
|
+
locale: string,
|
|
166
|
+
i18n: ResolvedI18nConfig
|
|
167
|
+
): string => {
|
|
168
|
+
if (i18n.parser === "dot") {
|
|
169
|
+
let base = rel.slice(0, rel.length - ext.length);
|
|
170
|
+
const lastDot = base.lastIndexOf(".");
|
|
171
|
+
if (lastDot > base.lastIndexOf("/")) {
|
|
172
|
+
const suffix = base.slice(lastDot + 1).toLowerCase();
|
|
173
|
+
if (i18n.locales.some((entry) => entry.code.toLowerCase() === suffix)) {
|
|
174
|
+
base = base.slice(0, lastDot);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
return `${base}.${locale}${ext}`;
|
|
178
|
+
}
|
|
179
|
+
return `${locale}/${rel}`;
|
|
180
|
+
};
|
|
181
|
+
|
|
151
182
|
/**
|
|
152
183
|
* Warn about top-level content folders that look like a locale (a code Blume
|
|
153
184
|
* recognizes) but aren't declared in `i18n.locales`. Without this they're
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isAssetIcon } from "../theme/icon-kind.ts";
|
|
1
2
|
import { hasIcon } from "../theme/icons.ts";
|
|
2
3
|
import type { Diagnostic, NavNode, Navigation, PageRecord } from "./types.ts";
|
|
3
4
|
|
|
@@ -8,12 +9,8 @@ import type { Diagnostic, NavNode, Navigation, PageRecord } from "./types.ts";
|
|
|
8
9
|
* covers every source (config, folder meta, frontmatter) at once.
|
|
9
10
|
*/
|
|
10
11
|
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
/** Whether an icon string is an asset (image/URL/inline SVG), not a set name. */
|
|
15
|
-
const isAssetIcon = (value: string): boolean =>
|
|
16
|
-
value.startsWith("<") || IMAGE_ICON.test(value);
|
|
12
|
+
const ICON_SHAPE_HINT =
|
|
13
|
+
"Use a built-in icon name, an image path/URL, or inline SVG markup.";
|
|
17
14
|
|
|
18
15
|
/** Flatten a sidebar tree to every node, descending into groups. */
|
|
19
16
|
const flattenNodes = (nodes: NavNode[]): NavNode[] =>
|
|
@@ -66,9 +63,14 @@ const unknownIconDiagnostics = (
|
|
|
66
63
|
continue;
|
|
67
64
|
}
|
|
68
65
|
seen.add(icon);
|
|
66
|
+
// Markup that isn't a complete <svg> element (an <img> tag, a truncated
|
|
67
|
+
// svg) is a shape problem, not a set-name typo — say so.
|
|
68
|
+
const message = icon.trimStart().startsWith("<")
|
|
69
|
+
? `Icon markup "${icon}" (${where}) isn't a complete inline <svg> element, so it won't render.`
|
|
70
|
+
: `Unknown icon "${icon}" (${where}) — it isn't in Blume's icon set.`;
|
|
69
71
|
diagnostics.push({
|
|
70
72
|
code: "BLUME_UNKNOWN_ICON",
|
|
71
|
-
message
|
|
73
|
+
message,
|
|
72
74
|
severity: "warning",
|
|
73
75
|
suggestion,
|
|
74
76
|
});
|
|
@@ -78,16 +80,11 @@ const unknownIconDiagnostics = (
|
|
|
78
80
|
|
|
79
81
|
/** Warn about icon names that aren't in Blume's set (skipping image/SVG icons). */
|
|
80
82
|
export const validateNavIcons = (navigation: Navigation): Diagnostic[] =>
|
|
81
|
-
unknownIconDiagnostics(
|
|
82
|
-
collectIcons(navigation),
|
|
83
|
-
"Use a built-in icon name, an image path/URL, or inline SVG markup."
|
|
84
|
-
);
|
|
83
|
+
unknownIconDiagnostics(collectIcons(navigation), ICON_SHAPE_HINT);
|
|
85
84
|
|
|
86
85
|
/**
|
|
87
|
-
* Warn about unknown icons on curated `search.popular` links.
|
|
88
|
-
*
|
|
89
|
-
* navigation — and unlike nav icons they resolve in a *client* island, so only
|
|
90
|
-
* set names work (an image/SVG icon quietly falls back to the file glyph).
|
|
86
|
+
* Warn about unknown icons on curated `search.popular` links. Same accepted
|
|
87
|
+
* input shapes as nav icons — resolved to markup on the server for the island.
|
|
91
88
|
*/
|
|
92
89
|
export const validateSearchPopularIcons = (
|
|
93
90
|
popular: { icon?: string; label: string }[]
|
|
@@ -97,25 +94,7 @@ export const validateSearchPopularIcons = (
|
|
|
97
94
|
? [{ icon: link.icon, where: `popular link "${link.label}"` }]
|
|
98
95
|
: []
|
|
99
96
|
);
|
|
100
|
-
|
|
101
|
-
// here they are exactly the silent failure this validator exists to catch.
|
|
102
|
-
const diagnostics: Diagnostic[] = [];
|
|
103
|
-
const seen = new Set<string>();
|
|
104
|
-
for (const { icon, where } of icons) {
|
|
105
|
-
if (isAssetIcon(icon) && !seen.has(icon)) {
|
|
106
|
-
seen.add(icon);
|
|
107
|
-
diagnostics.push({
|
|
108
|
-
code: "BLUME_UNKNOWN_ICON",
|
|
109
|
-
message: `Icon "${icon}" (${where}) is an image or inline SVG — popular links render in the client search island, where only built-in icon names resolve, so it falls back to the file glyph.`,
|
|
110
|
-
severity: "warning",
|
|
111
|
-
suggestion: "Use a built-in icon name.",
|
|
112
|
-
});
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
return [
|
|
116
|
-
...diagnostics,
|
|
117
|
-
...unknownIconDiagnostics(icons, "Use a built-in icon name."),
|
|
118
|
-
];
|
|
97
|
+
return unknownIconDiagnostics(icons, ICON_SHAPE_HINT);
|
|
119
98
|
};
|
|
120
99
|
|
|
121
100
|
/** Whether an internal path resolves to a page or a section that has pages. */
|
|
@@ -143,12 +143,22 @@ const normalizeLoadedEntries = (
|
|
|
143
143
|
loaded: ({ source: ContentSource } & SourceLoadResult)[],
|
|
144
144
|
config: ResolvedConfig
|
|
145
145
|
): { pages: PageRecord[]; diagnostics: Diagnostic[]; droppedPages: number } => {
|
|
146
|
-
// Only thread `frontmatter.extend` through when a project
|
|
147
|
-
// known-key split in `normalizeEntry` stays off the default
|
|
146
|
+
// Only thread `frontmatter.extend` / `content.types` through when a project
|
|
147
|
+
// opts in, so the known-key split in `normalizeEntry` stays off the default
|
|
148
|
+
// path.
|
|
148
149
|
const frontmatterExtend =
|
|
149
150
|
Object.keys(config.frontmatter.extend).length > 0
|
|
150
151
|
? config.frontmatter.extend
|
|
151
152
|
: undefined;
|
|
153
|
+
const declaredTypes = Object.entries(config.content.types).filter(
|
|
154
|
+
([, type]) => Object.keys(type.frontmatter).length > 0
|
|
155
|
+
);
|
|
156
|
+
const typeFrontmatter =
|
|
157
|
+
declaredTypes.length > 0
|
|
158
|
+
? Object.fromEntries(
|
|
159
|
+
declaredTypes.map(([name, type]) => [name, type.frontmatter])
|
|
160
|
+
)
|
|
161
|
+
: undefined;
|
|
152
162
|
|
|
153
163
|
const pages: PageRecord[] = [];
|
|
154
164
|
const allDiagnostics: Diagnostic[] = [];
|
|
@@ -166,6 +176,7 @@ const normalizeLoadedEntries = (
|
|
|
166
176
|
prefix: source.prefix,
|
|
167
177
|
staged: source.staged,
|
|
168
178
|
},
|
|
179
|
+
typeFrontmatter,
|
|
169
180
|
});
|
|
170
181
|
if (normalized.pages.length === 0 && normalized.diagnostics.length > 0) {
|
|
171
182
|
droppedPages += 1;
|