blume 1.3.0 → 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.
Files changed (110) hide show
  1. package/CHANGELOG.md +59 -0
  2. package/dist/cli/index.js +3290 -254
  3. package/dist/cli/index.js.map +79 -67
  4. package/dist/types/core/base-path.d.ts +5 -0
  5. package/dist/types/core/config-input.d.ts +82 -6
  6. package/dist/types/core/i18n-ui.d.ts +2 -0
  7. package/dist/types/core/schema.d.ts +19 -2
  8. package/dist/types/core/sources/types.d.ts +5 -0
  9. package/dist/types/core/types.d.ts +4 -3
  10. package/docs/02-deployment.mdx +1 -1
  11. package/docs/configuration/ai.mdx +16 -2
  12. package/docs/configuration/index.mdx +26 -0
  13. package/docs/configuration/search.mdx +1 -3
  14. package/docs/content/i18n.mdx +13 -1
  15. package/docs/content/navigation.mdx +11 -0
  16. package/docs/reference/cli.mdx +4 -0
  17. package/docs/reference/frontmatter.mdx +33 -0
  18. package/docs/reference/meta.ts +1 -1
  19. package/docs/reference/translate.mdx +80 -0
  20. package/package.json +1 -1
  21. package/src/ai/agent-readability.ts +7 -4
  22. package/src/ai/ask-context.ts +3 -6
  23. package/src/ai/link-headers.ts +4 -3
  24. package/src/ai/llms.ts +4 -2
  25. package/src/ai/markdown.ts +34 -1
  26. package/src/ai/mcp/data.ts +10 -4
  27. package/src/ai/mcp/server.ts +74 -3
  28. package/src/ai/mcp/tools.ts +2 -2
  29. package/src/astro/generate.ts +2 -2
  30. package/src/astro/integration.ts +6 -2
  31. package/src/astro/markdown-negotiation.ts +5 -0
  32. package/src/astro/templates.ts +74 -21
  33. package/src/audit/url.ts +5 -10
  34. package/src/cli/commands/build.ts +145 -34
  35. package/src/cli/commands/translate.ts +300 -0
  36. package/src/cli/index.ts +2 -0
  37. package/src/components/Icon.astro +2 -7
  38. package/src/components/content/Step.astro +3 -8
  39. package/src/components/content/Tab.astro +20 -1
  40. package/src/components/layout/LanguageSwitcher.astro +2 -1
  41. package/src/components/layout/Logo.astro +4 -4
  42. package/src/components/layout/PageActions.astro +12 -7
  43. package/src/components/layout/Search.astro +15 -20
  44. package/src/components/layout/search/orama.ts +3 -1
  45. package/src/core/base-path.ts +9 -0
  46. package/src/core/config-input.ts +84 -6
  47. package/src/core/graph.ts +46 -2
  48. package/src/core/i18n-ui.ts +2 -0
  49. package/src/core/i18n.ts +31 -0
  50. package/src/core/nav-diagnostics.ts +13 -34
  51. package/src/core/project-graph.ts +13 -2
  52. package/src/core/schema.ts +174 -74
  53. package/src/core/sources/normalize.ts +25 -12
  54. package/src/core/sources/types.ts +5 -0
  55. package/src/core/types.ts +4 -3
  56. package/src/core/ui-packs/ar.ts +42 -1
  57. package/src/core/ui-packs/bg.ts +42 -1
  58. package/src/core/ui-packs/bn.ts +42 -1
  59. package/src/core/ui-packs/ca.ts +44 -1
  60. package/src/core/ui-packs/cs.ts +42 -1
  61. package/src/core/ui-packs/da.ts +42 -1
  62. package/src/core/ui-packs/de.ts +42 -1
  63. package/src/core/ui-packs/el.ts +44 -1
  64. package/src/core/ui-packs/es.ts +44 -1
  65. package/src/core/ui-packs/fa.ts +42 -1
  66. package/src/core/ui-packs/fi.ts +42 -1
  67. package/src/core/ui-packs/fr.ts +44 -1
  68. package/src/core/ui-packs/he.ts +42 -1
  69. package/src/core/ui-packs/hi.ts +42 -1
  70. package/src/core/ui-packs/hr.ts +42 -1
  71. package/src/core/ui-packs/hu.ts +42 -1
  72. package/src/core/ui-packs/id.ts +42 -1
  73. package/src/core/ui-packs/it.ts +44 -1
  74. package/src/core/ui-packs/ja.ts +44 -1
  75. package/src/core/ui-packs/ko.ts +44 -1
  76. package/src/core/ui-packs/nl.ts +42 -1
  77. package/src/core/ui-packs/no.ts +42 -1
  78. package/src/core/ui-packs/pl.ts +42 -1
  79. package/src/core/ui-packs/pt-br.ts +44 -1
  80. package/src/core/ui-packs/pt.ts +44 -1
  81. package/src/core/ui-packs/ro.ts +42 -1
  82. package/src/core/ui-packs/ru.ts +42 -1
  83. package/src/core/ui-packs/sk.ts +42 -1
  84. package/src/core/ui-packs/sr.ts +42 -1
  85. package/src/core/ui-packs/sv.ts +42 -1
  86. package/src/core/ui-packs/th.ts +44 -1
  87. package/src/core/ui-packs/tr.ts +42 -1
  88. package/src/core/ui-packs/uk.ts +42 -1
  89. package/src/core/ui-packs/vi.ts +44 -1
  90. package/src/core/ui-packs/zh-tw.ts +44 -1
  91. package/src/core/ui-packs/zh.ts +44 -1
  92. package/src/deploy/adapter-output.ts +44 -5
  93. package/src/deploy/cloudflare-negotiation.ts +527 -0
  94. package/src/deploy/redirects.ts +13 -0
  95. package/src/deploy/vercel-negotiation.ts +30 -13
  96. package/src/eval/agents.ts +1 -1
  97. package/src/search/documents.ts +11 -0
  98. package/src/search/facets.ts +33 -0
  99. package/src/search/orama-index.ts +48 -6
  100. package/src/search/popular-icon.ts +33 -0
  101. package/src/theme/fonts.ts +3 -1
  102. package/src/theme/icon-kind.ts +20 -0
  103. package/src/translate/agents.ts +51 -0
  104. package/src/translate/ledger.ts +148 -0
  105. package/src/translate/meta.ts +149 -0
  106. package/src/translate/prompts.ts +95 -0
  107. package/src/translate/report.ts +360 -0
  108. package/src/translate/run.ts +376 -0
  109. package/src/translate/validate.ts +171 -0
  110. package/src/translate/work-list.ts +0 -0
@@ -141,6 +141,37 @@ export const pageMetaSchema = pageMetaBaseSchema;
141
141
  export type PageMeta = z.infer<typeof pageMetaBaseSchema>;
142
142
  export type PageMetaInput = z.input<typeof pageMetaBaseSchema>;
143
143
 
144
+ /**
145
+ * A map of custom frontmatter keys to user-supplied validation schemas,
146
+ * consumed through the Standard Schema `~standard` contract — never Zod's own
147
+ * API — so the consumer's zod (any version), Valibot, or ArkType all work
148
+ * (see `standard-schema.ts`). Shared by the site-wide `frontmatter.extend`
149
+ * and the per-type `content.types.<type>.frontmatter` maps. Built-in
150
+ * frontmatter fields can't be redeclared — they're load-bearing (routing,
151
+ * sidebar, SEO), and shadowing one would silently change its semantics.
152
+ */
153
+ const customKeySchemaRecord = (where: string) =>
154
+ z
155
+ .record(
156
+ z.string(),
157
+ z.custom<StandardSchema>(isStandardSchema, {
158
+ message:
159
+ "Expected a Standard Schema (e.g. a Zod schema — any Zod version works).",
160
+ })
161
+ )
162
+ .default({})
163
+ .superRefine((value, ctx) => {
164
+ for (const key of Object.keys(value)) {
165
+ if (Object.hasOwn(pageMetaBaseSchema.shape, key)) {
166
+ ctx.addIssue({
167
+ code: z.ZodIssueCode.custom,
168
+ message: `"${key}" is a built-in frontmatter field and cannot be redeclared via ${where}.`,
169
+ path: [key],
170
+ });
171
+ }
172
+ }
173
+ });
174
+
144
175
  // ---------------------------------------------------------------------------
145
176
  // Folder meta (meta.ts)
146
177
  // ---------------------------------------------------------------------------
@@ -345,6 +376,30 @@ const contentSourceSchema = z.discriminatedUnion("type", [
345
376
  /** A resolved content-source config entry (post-defaults). */
346
377
  export type ContentSourceConfig = z.infer<typeof contentSourceSchema>;
347
378
 
379
+ /**
380
+ * Per-type content definition. An object (rather than a bare frontmatter map)
381
+ * so type-scoped concerns added later — search facets, templates — have a
382
+ * home without a breaking config change.
383
+ */
384
+ const contentTypeConfigSchema = z.strictObject({
385
+ /**
386
+ * Custom frontmatter keys whose values become filterable facets for pages
387
+ * of this type — surfaced in search documents and filterable through the
388
+ * MCP tools' `filters` input. Each name must be a custom key declared for
389
+ * the type (in its `frontmatter` map or the site-wide `frontmatter.extend`),
390
+ * checked at the config level where both maps are visible. Only string
391
+ * (or number/boolean, stringified) values ever facet.
392
+ */
393
+ facets: z.array(z.string()).default([]),
394
+ /**
395
+ * Custom frontmatter keys for pages of this type, layered on top of the
396
+ * site-wide `frontmatter.extend`. Every declared key is validated on every
397
+ * page of the type — absent ones included — so a required schema enforces
398
+ * the key type-wide while leaving other types untouched.
399
+ */
400
+ frontmatter: customKeySchemaRecord("content.types"),
401
+ });
402
+
348
403
  const contentConfigSchema = z.strictObject({
349
404
  defaultType: z.string().default("doc"),
350
405
  exclude: z.array(z.string()).default(["**/_*", "**/.*"]),
@@ -357,8 +412,32 @@ const contentConfigSchema = z.strictObject({
357
412
  * existing projects are unchanged.
358
413
  */
359
414
  sources: z.array(contentSourceSchema).optional(),
415
+ /**
416
+ * Per-type content definitions, keyed by the frontmatter `type` they apply
417
+ * to (including `defaultType`, for pages that set none).
418
+ */
419
+ types: z.record(z.string(), contentTypeConfigSchema).default({}),
360
420
  });
361
421
 
422
+ /**
423
+ * A header label that may localize: a plain string, or a map of locale code to
424
+ * label (`{ en: "Docs", ja: "ドキュメント" }`). Resolved when each locale's
425
+ * navigation is built — the active locale's entry wins, then the default
426
+ * locale's, then the map's first entry — so a single-locale site can keep
427
+ * plain strings and an i18n site can translate its header without forking the
428
+ * config.
429
+ */
430
+ const localizableLabelSchema = z.union([
431
+ z.string(),
432
+ z
433
+ .record(z.string(), z.string())
434
+ .refine((value) => Object.keys(value).length > 0, {
435
+ message: "Provide at least one locale's label.",
436
+ }),
437
+ ]);
438
+
439
+ export type LocalizableLabel = z.infer<typeof localizableLabelSchema>;
440
+
362
441
  const navTabSchema = z.strictObject({
363
442
  // Rejected empty rather than accepted: an empty `href` would render a link to
364
443
  // nowhere, and it can't mean "resolve it for me" either — that's what
@@ -370,13 +449,13 @@ const navTabSchema = z.strictObject({
370
449
  z.strictObject({
371
450
  description: z.string().optional(),
372
451
  icon: iconName.optional(),
373
- label: z.string(),
452
+ label: localizableLabelSchema,
374
453
  path: z.string(),
375
454
  tag: z.string().optional(),
376
455
  })
377
456
  )
378
457
  .optional(),
379
- label: z.string(),
458
+ label: localizableLabelSchema,
380
459
  path: z.string(),
381
460
  });
382
461
 
@@ -857,6 +936,12 @@ const localeSchema = z.strictObject({
857
936
  /** Text direction; drives `<html dir>` and a future RTL pass. */
858
937
  dir: z.enum(["ltr", "rtl"]).default("ltr"),
859
938
  label: z.string(),
939
+ /**
940
+ * Freeform style guidance for `blume translate`, e.g. "Brazilian
941
+ * Portuguese, informal você". Pins register and dialect from the first
942
+ * translation and wins over an existing translation's style on reruns.
943
+ */
944
+ style: z.string().optional(),
860
945
  });
861
946
 
862
947
  /**
@@ -1390,26 +1475,7 @@ const asyncapiConfigSchema = z.strictObject({
1390
1475
  * sidebar, SEO), and shadowing one would silently change its semantics.
1391
1476
  */
1392
1477
  const frontmatterConfigSchema = z.strictObject({
1393
- extend: z
1394
- .record(
1395
- z.string(),
1396
- z.custom<StandardSchema>(isStandardSchema, {
1397
- message:
1398
- "Expected a Standard Schema (e.g. a Zod schema — any Zod version works).",
1399
- })
1400
- )
1401
- .default({})
1402
- .superRefine((value, ctx) => {
1403
- for (const key of Object.keys(value)) {
1404
- if (Object.hasOwn(pageMetaBaseSchema.shape, key)) {
1405
- ctx.addIssue({
1406
- code: z.ZodIssueCode.custom,
1407
- message: `"${key}" is a built-in frontmatter field and cannot be redeclared via frontmatter.extend.`,
1408
- path: [key],
1409
- });
1410
- }
1411
- }
1412
- }),
1478
+ extend: customKeySchemaRecord("frontmatter.extend"),
1413
1479
  });
1414
1480
 
1415
1481
  /** Full user-facing config schema. All fields optional with defaults. */
@@ -1444,58 +1510,92 @@ const tocConfigSchema = z
1444
1510
  "toc.minHeadingLevel must be less than or equal to toc.maxHeadingLevel.",
1445
1511
  });
1446
1512
 
1447
- export const blumeConfigSchema = z.strictObject({
1448
- ai: aiConfigSchema.prefault({}),
1449
- analytics: analyticsConfigSchema.optional(),
1450
- asyncapi: asyncapiConfigSchema.prefault({}),
1451
- banner: bannerConfigSchema.optional(),
1452
- /**
1453
- * Site-wide mount point prepended to every generated route (e.g. `/docs`),
1454
- * while staying invisible to the sidebar/nav tree. Distinct from a per-source
1455
- * `prefix` (which creates a group) and from `deployment.base` (Astro's
1456
- * host-subdirectory base); the two compose. Normalized to `""` or `/seg`.
1457
- */
1458
- basePath: z
1459
- .string()
1460
- .optional()
1461
- .transform((value) => normalizeBasePath(value)),
1462
- content: contentConfigSchema.prefault({}),
1463
- /**
1464
- * Date presentation for the "last updated" stamp and the changelog timeline.
1465
- * Pass-through `Intl.DateTimeFormat` options; defaults to `{ dateStyle: "long" }`.
1466
- */
1467
- dateFormat: dateFormatConfigSchema.default({ dateStyle: "long" }),
1468
- deployment: deploymentConfigSchema.prefault({}),
1469
- description: z.string().optional(),
1470
- /**
1471
- * Where `<Component path>` resolves live previews and their source from.
1472
- * A string is shorthand for `{ source }` the directory (or glob, for
1473
- * colocated registry layouts) under the project root that holds example
1474
- * files. The object form adds `css`: a stylesheet injected into every
1475
- * preview frame (design tokens, shadcn variables, `@theme` mappings).
1476
- */
1477
- examples: examplesConfigSchema.prefault("examples"),
1478
- export: exportConfigSchema.prefault(false),
1479
- feedback: z.boolean().default(true),
1480
- /** Opt-in custom frontmatter keys, validated by user-supplied schemas. */
1481
- frontmatter: frontmatterConfigSchema.prefault({}),
1482
- github: githubConfigSchema.optional(),
1483
- i18n: i18nConfigSchema.optional(),
1484
- image: imageConfigSchema.prefault({}),
1485
- integrations: z.array(z.custom<AstroIntegration>()).default([]),
1486
- lastModified: lastModifiedConfigSchema.default(false),
1487
- logo: logoConfigSchema.optional(),
1488
- markdown: markdownConfigSchema.prefault({}),
1489
- navigation: navigationConfigSchema.prefault({}),
1490
- openapi: openapiConfigSchema.prefault({}),
1491
- react: reactConfigSchema.prefault({}),
1492
- redirects: z.array(redirectSchema).default([]),
1493
- search: searchConfigSchema.prefault({}),
1494
- seo: seoConfigSchema.prefault({}),
1495
- theme: themeConfigSchema.prefault({}),
1496
- title: z.string().default("Documentation"),
1497
- toc: tocConfigSchema,
1498
- });
1513
+ export const blumeConfigSchema = z
1514
+ .strictObject({
1515
+ ai: aiConfigSchema.prefault({}),
1516
+ analytics: analyticsConfigSchema.optional(),
1517
+ asyncapi: asyncapiConfigSchema.prefault({}),
1518
+ banner: bannerConfigSchema.optional(),
1519
+ /**
1520
+ * Site-wide mount point prepended to every generated route (e.g. `/docs`),
1521
+ * while staying invisible to the sidebar/nav tree. Distinct from a per-source
1522
+ * `prefix` (which creates a group) and from `deployment.base` (Astro's
1523
+ * host-subdirectory base); the two compose. Normalized to `""` or `/seg`.
1524
+ */
1525
+ basePath: z
1526
+ .string()
1527
+ .optional()
1528
+ .transform((value) => normalizeBasePath(value)),
1529
+ content: contentConfigSchema.prefault({}),
1530
+ /**
1531
+ * Date presentation for the "last updated" stamp and the changelog timeline.
1532
+ * Pass-through `Intl.DateTimeFormat` options; defaults to `{ dateStyle: "long" }`.
1533
+ */
1534
+ dateFormat: dateFormatConfigSchema.default({ dateStyle: "long" }),
1535
+ deployment: deploymentConfigSchema.prefault({}),
1536
+ description: z.string().optional(),
1537
+ /**
1538
+ * Where `<Component path>` resolves live previews and their source from.
1539
+ * A string is shorthand for `{ source }` — the directory (or glob, for
1540
+ * colocated registry layouts) under the project root that holds example
1541
+ * files. The object form adds `css`: a stylesheet injected into every
1542
+ * preview frame (design tokens, shadcn variables, `@theme` mappings).
1543
+ */
1544
+ examples: examplesConfigSchema.prefault("examples"),
1545
+ export: exportConfigSchema.prefault(false),
1546
+ feedback: z.boolean().default(true),
1547
+ /** Opt-in custom frontmatter keys, validated by user-supplied schemas. */
1548
+ frontmatter: frontmatterConfigSchema.prefault({}),
1549
+ github: githubConfigSchema.optional(),
1550
+ i18n: i18nConfigSchema.optional(),
1551
+ image: imageConfigSchema.prefault({}),
1552
+ integrations: z.array(z.custom<AstroIntegration>()).default([]),
1553
+ lastModified: lastModifiedConfigSchema.default(false),
1554
+ logo: logoConfigSchema.optional(),
1555
+ markdown: markdownConfigSchema.prefault({}),
1556
+ navigation: navigationConfigSchema.prefault({}),
1557
+ openapi: openapiConfigSchema.prefault({}),
1558
+ react: reactConfigSchema.prefault({}),
1559
+ redirects: z.array(redirectSchema).default([]),
1560
+ search: searchConfigSchema.prefault({}),
1561
+ seo: seoConfigSchema.prefault({}),
1562
+ theme: themeConfigSchema.prefault({}),
1563
+ title: z.string().default("Documentation"),
1564
+ toc: tocConfigSchema,
1565
+ })
1566
+ .superRefine((config, ctx) => {
1567
+ // A custom key is declared site-wide (`frontmatter.extend`) or per-type
1568
+ // (`content.types`), never both — two schemas for one key would make
1569
+ // precedence on pages of that type ambiguous.
1570
+ for (const [typeName, typeConfig] of Object.entries(config.content.types)) {
1571
+ for (const key of Object.keys(typeConfig.frontmatter)) {
1572
+ if (Object.hasOwn(config.frontmatter.extend, key)) {
1573
+ ctx.addIssue({
1574
+ code: z.ZodIssueCode.custom,
1575
+ message: `"${key}" is already declared in frontmatter.extend and cannot be redeclared for type "${typeName}".`,
1576
+ path: ["content", "types", typeName, "frontmatter", key],
1577
+ });
1578
+ }
1579
+ }
1580
+ // A facet with no declared key could never carry a value — pages can't
1581
+ // even set the key — so it's a config mistake, caught here where both
1582
+ // the per-type map and the site-wide extend are visible.
1583
+ for (const [position, facet] of typeConfig.facets.entries()) {
1584
+ if (
1585
+ !(
1586
+ Object.hasOwn(typeConfig.frontmatter, facet) ||
1587
+ Object.hasOwn(config.frontmatter.extend, facet)
1588
+ )
1589
+ ) {
1590
+ ctx.addIssue({
1591
+ code: z.ZodIssueCode.custom,
1592
+ message: `Facet "${facet}" for type "${typeName}" is not a declared custom frontmatter key — add it to the type's frontmatter map or frontmatter.extend.`,
1593
+ path: ["content", "types", typeName, "facets", position],
1594
+ });
1595
+ }
1596
+ }
1597
+ }
1598
+ });
1499
1599
 
1500
1600
  /** Resolved config: every field present after defaults are applied. */
1501
1601
  export type ResolvedConfig = z.infer<typeof blumeConfigSchema>;
@@ -516,13 +516,13 @@ const segmentKey = (
516
516
  };
517
517
 
518
518
  /**
519
- * Validate the opt-in custom frontmatter keys (`frontmatter.extend`) through
520
- * the Standard Schema contract — the consumer's own Zod (any version),
521
- * Valibot, or ArkType, never Blume's bundled zod (see `standard-schema.ts`).
522
- * Every declared key is checked, absent ones included, so a required schema
523
- * enforces its key on every page. Async schemas are rejected with a
524
- * diagnostic: this funnel is synchronous, and frontmatter validation has no
525
- * business awaiting I/O.
519
+ * Validate the opt-in custom frontmatter keys (`frontmatter.extend` and
520
+ * `content.types.<type>.frontmatter`) through the Standard Schema contract —
521
+ * the consumer's own Zod (any version), Valibot, or ArkType, never Blume's
522
+ * bundled zod (see `standard-schema.ts`). Every declared key is checked,
523
+ * absent ones included, so a required schema enforces its key on every page
524
+ * it applies to. Async schemas are rejected with a diagnostic: this funnel is
525
+ * synchronous, and frontmatter validation has no business awaiting I/O.
526
526
  */
527
527
  const validateCustomKeys = (
528
528
  data: Record<string, unknown>,
@@ -534,7 +534,7 @@ const validateCustomKeys = (
534
534
  const outcome = schema["~standard"].validate(data[key]);
535
535
  if (outcome instanceof Promise) {
536
536
  issues.push({
537
- message: "Async schemas are not supported in frontmatter.extend.",
537
+ message: "Async schemas are not supported for custom frontmatter keys.",
538
538
  path: [key],
539
539
  });
540
540
  continue;
@@ -562,9 +562,11 @@ const validateCustomKeys = (
562
562
 
563
563
  /**
564
564
  * Parse an entry's frontmatter: built-in keys through the strict page schema,
565
- * custom keys (`frontmatter.extend`) through their user-supplied schemas. The
566
- * custom keys are carved out before the strict parse, so the page schema stays
567
- * strict for everything else and unknown-key typo catching is unchanged.
565
+ * custom keys (`frontmatter.extend` plus the page type's
566
+ * `content.types.<type>.frontmatter`) through their user-supplied schemas.
567
+ * The custom keys are carved out before the strict parse, so the page schema
568
+ * stays strict for everything else and unknown-key typo catching is unchanged
569
+ * — a key declared only for some other type stays unknown here.
568
570
  * Returns diagnostics instead of meta when either side rejects.
569
571
  */
570
572
  const parseEntryMeta = (
@@ -573,7 +575,18 @@ const parseEntryMeta = (
573
575
  ):
574
576
  | { meta: PageMeta; custom?: Record<string, unknown>; diagnostics?: never }
575
577
  | { meta?: never; diagnostics: Diagnostic[] } => {
576
- const extend = ctx.frontmatterExtend;
578
+ // Resolved the same way `contentType` is after parsing (`meta.type` falling
579
+ // back to `defaultType`); a non-string `type` fails the strict parse below,
580
+ // so which per-type map was merged for that entry never matters.
581
+ const entryType =
582
+ typeof entry.data.type === "string" ? entry.data.type : ctx.defaultType;
583
+ const typeExtend = ctx.typeFrontmatter?.[entryType];
584
+ // Config validation rejects a key declared both site-wide and per-type, so
585
+ // this merge never has to pick a winner.
586
+ const extend =
587
+ ctx.frontmatterExtend || typeExtend
588
+ ? { ...ctx.frontmatterExtend, ...typeExtend }
589
+ : undefined;
577
590
  const known = extend
578
591
  ? Object.fromEntries(
579
592
  Object.entries(entry.data).filter(
@@ -112,4 +112,9 @@ export interface NormalizeContext {
112
112
  /** Opt-in custom frontmatter keys (`frontmatter.extend`), schema per key. */
113
113
  frontmatterExtend?: FrontmatterExtend;
114
114
  i18n?: ResolvedI18nConfig;
115
+ /**
116
+ * Per-type custom frontmatter keys (`content.types.<type>.frontmatter`),
117
+ * applied to a page only when its resolved `type` matches.
118
+ */
119
+ typeFrontmatter?: Record<string, FrontmatterExtend>;
115
120
  }
package/src/core/types.ts CHANGED
@@ -146,9 +146,10 @@ export interface PageRecord {
146
146
  contentType: string;
147
147
  meta: PageMeta;
148
148
  /**
149
- * Custom frontmatter values declared via `frontmatter.extend`, validated by
150
- * the user-supplied schemas (schema output, so transforms apply). Present
151
- * only when the project opts in and the page carries at least one value.
149
+ * Custom frontmatter values declared via `frontmatter.extend` or the page
150
+ * type's `content.types.<type>.frontmatter`, validated by the user-supplied
151
+ * schemas (schema output, so transforms apply). Present only when the
152
+ * project opts in and the page carries at least one value.
152
153
  */
153
154
  custom?: Record<string, unknown>;
154
155
  headings: Heading[];
@@ -9,25 +9,40 @@ export const ar: UIStringsOverride = {
9
9
  connectMcp: "الاتصال بـ MCP",
10
10
  copied: "تم النسخ!",
11
11
  copyClaudeCode: "نسخ أمر Claude Code",
12
+ copyCode: "نسخ الكود",
13
+ copyCodex: "نسخ أمر Codex",
12
14
  copyMarkdown: "نسخ بصيغة Markdown",
13
15
  copyServerUrl: "نسخ عنوان URL للخادم",
14
16
  edit: "التعديل على GitHub",
17
+ export: "تصدير",
18
+ exportEpub: "التصدير إلى EPUB",
19
+ exportPdf: "التصدير إلى PDF",
20
+ generating: "جارٍ الإنشاء…",
21
+ openIn: "فتح في {name}",
15
22
  openInChat: "فتح في المحادثة",
16
23
  scrollToTop: "العودة إلى الأعلى",
17
24
  },
18
25
  ask: {
26
+ ai: "الذكاء الاصطناعي",
27
+ clear: "مسح المحادثة",
28
+ close: "إغلاق",
29
+ copy: "نسخ المحادثة",
19
30
  empty: "اطرح سؤالاً حول الوثائق.",
20
31
  error: "عذراً، حدث خطأ ما.",
21
32
  label: "اطرح سؤالاً",
22
33
  placeholder: "اطرح سؤالاً…",
23
34
  send: "إرسال",
35
+ tip: "نصيحة: يمكنك فتح المحادثة وإغلاقها باستخدام",
24
36
  title: "اسأل الذكاء الاصطناعي",
37
+ you: "أنت",
25
38
  },
26
39
  banner: { dismiss: "إغلاق الإعلان" },
27
40
  changelog: {
28
41
  description: "تحديثات المنتج وملاحظات الإصدارات.",
42
+ showReleases: "عرض إصدارات {version}",
29
43
  title: "سجل التغييرات",
30
44
  },
45
+ content: { diagramError: "تعذّر عرض هذا المخطط." },
31
46
  feedback: {
32
47
  no: "لا",
33
48
  question: "هل كانت هذه الصفحة مفيدة؟",
@@ -35,7 +50,24 @@ export const ar: UIStringsOverride = {
35
50
  yes: "نعم",
36
51
  },
37
52
  languageSwitcher: { label: "اللغة", untranslated: "غير مترجم" },
38
- nav: { breadcrumb: "مسار التنقل" },
53
+ nav: {
54
+ back: "رجوع",
55
+ breadcrumb: "مسار التنقل",
56
+ closeNavigation: "إغلاق التنقل",
57
+ deprecated: "مهمل",
58
+ featured: "مميز",
59
+ githubRepository: "مستودع GitHub",
60
+ navigation: "التنقل",
61
+ primary: "رئيسي",
62
+ sections: "الأقسام",
63
+ toggleNavigation: "تبديل التنقل",
64
+ toggleTheme: "تبديل سمة الألوان",
65
+ },
66
+ notFound: {
67
+ description: "لم نتمكن من العثور على الصفحة التي تبحث عنها.",
68
+ home: "العودة إلى الصفحة الرئيسية",
69
+ title: "الصفحة غير موجودة",
70
+ },
39
71
  page: {
40
72
  lastUpdated: "آخر تحديث في",
41
73
  next: "التالي",
@@ -45,11 +77,20 @@ export const ar: UIStringsOverride = {
45
77
  },
46
78
  search: {
47
79
  all: "الكل",
80
+ allLanguages: "جميع اللغات",
81
+ askAi: "اسأل الذكاء الاصطناعي",
82
+ askAiHint: "احصل على إجابة فورية من الذكاء الاصطناعي",
48
83
  button: "بحث",
49
84
  devOnly: "البحث متاح في إصدار الإنتاج.",
85
+ error: "حدث خطأ ما. يرجى المحاولة مرة أخرى.",
50
86
  label: "البحث في الوثائق",
87
+ navigate: "تنقل",
51
88
  noResults: "لم يتم العثور على نتائج.",
89
+ open: "فتح",
52
90
  placeholder: "البحث في الوثائق…",
91
+ popular: "شائع",
92
+ preview: "معاينة",
93
+ results: "النتائج",
53
94
  },
54
95
  toc: { title: "في هذه الصفحة" },
55
96
  };
@@ -9,25 +9,40 @@ export const bg: UIStringsOverride = {
9
9
  connectMcp: "Свържи се с MCP",
10
10
  copied: "Копирано!",
11
11
  copyClaudeCode: "Копирай командата на Claude Code",
12
+ copyCode: "Копирай кода",
13
+ copyCodex: "Копирай командата на Codex",
12
14
  copyMarkdown: "Копирай като Markdown",
13
15
  copyServerUrl: "Копирай URL на сървъра",
14
16
  edit: "Редактирай в GitHub",
17
+ export: "Експортирай",
18
+ exportEpub: "Експортирай в EPUB",
19
+ exportPdf: "Експортирай в PDF",
20
+ generating: "Генериране…",
21
+ openIn: "Отвори в {name}",
15
22
  openInChat: "Отвори в чата",
16
23
  scrollToTop: "Нагоре",
17
24
  },
18
25
  ask: {
26
+ ai: "ИИ",
27
+ clear: "Изчисти разговора",
28
+ close: "Затвори",
29
+ copy: "Копирай разговора",
19
30
  empty: "Задайте въпрос за документацията.",
20
31
  error: "Съжаляваме, нещо се обърка.",
21
32
  label: "Задайте въпрос",
22
33
  placeholder: "Задайте въпрос…",
23
34
  send: "Изпрати",
35
+ tip: "Съвет: Можете да отваряте и затваряте чата с",
24
36
  title: "Попитай ИИ",
37
+ you: "Вие",
25
38
  },
26
39
  banner: { dismiss: "Затваряне на съобщението" },
27
40
  changelog: {
28
41
  description: "Актуализации на продукта и бележки към изданията.",
42
+ showReleases: "Покажи изданията на {version}",
29
43
  title: "Дневник на промените",
30
44
  },
45
+ content: { diagramError: "Тази диаграма не можа да бъде показана." },
31
46
  feedback: {
32
47
  no: "Не",
33
48
  question: "Беше ли полезна тази страница?",
@@ -35,7 +50,24 @@ export const bg: UIStringsOverride = {
35
50
  yes: "Да",
36
51
  },
37
52
  languageSwitcher: { label: "Език", untranslated: "Непреведено" },
38
- nav: { breadcrumb: "Навигационна пътека" },
53
+ nav: {
54
+ back: "Назад",
55
+ breadcrumb: "Навигационна пътека",
56
+ closeNavigation: "Затвори навигацията",
57
+ deprecated: "остаряло",
58
+ featured: "Препоръчано",
59
+ githubRepository: "Хранилище в GitHub",
60
+ navigation: "Навигация",
61
+ primary: "Основно",
62
+ sections: "Раздели",
63
+ toggleNavigation: "Превключи навигацията",
64
+ toggleTheme: "Превключи цветовата тема",
65
+ },
66
+ notFound: {
67
+ description: "Не успяхме да намерим страницата, която търсите.",
68
+ home: "Обратно към началната страница",
69
+ title: "Страницата не е намерена",
70
+ },
39
71
  page: {
40
72
  lastUpdated: "Последна актуализация",
41
73
  next: "Напред",
@@ -45,11 +77,20 @@ export const bg: UIStringsOverride = {
45
77
  },
46
78
  search: {
47
79
  all: "Всички",
80
+ allLanguages: "Всички езици",
81
+ askAi: "Попитай ИИ",
82
+ askAiHint: "Получете незабавен отговор от ИИ",
48
83
  button: "Търсене",
49
84
  devOnly: "Търсенето е достъпно в производствената компилация.",
85
+ error: "Нещо се обърка. Моля, опитайте отново.",
50
86
  label: "Търсене в документацията",
87
+ navigate: "навигация",
51
88
  noResults: "Няма намерени резултати.",
89
+ open: "отвори",
52
90
  placeholder: "Търсене в документацията…",
91
+ popular: "Популярни",
92
+ preview: "преглед",
93
+ results: "Резултати",
53
94
  },
54
95
  toc: { title: "На тази страница" },
55
96
  };
@@ -9,25 +9,40 @@ export const bn: UIStringsOverride = {
9
9
  connectMcp: "MCP-তে সংযোগ করুন",
10
10
  copied: "অনুলিপি করা হয়েছে!",
11
11
  copyClaudeCode: "Claude Code কমান্ড অনুলিপি করুন",
12
+ copyCode: "কোড অনুলিপি করুন",
13
+ copyCodex: "Codex কমান্ড অনুলিপি করুন",
12
14
  copyMarkdown: "Markdown হিসেবে অনুলিপি করুন",
13
15
  copyServerUrl: "সার্ভার URL অনুলিপি করুন",
14
16
  edit: "GitHub-এ সম্পাদনা করুন",
17
+ export: "এক্সপোর্ট করুন",
18
+ exportEpub: "EPUB-এ এক্সপোর্ট করুন",
19
+ exportPdf: "PDF-এ এক্সপোর্ট করুন",
20
+ generating: "তৈরি হচ্ছে…",
21
+ openIn: "{name}-এ খুলুন",
15
22
  openInChat: "চ্যাটে খুলুন",
16
23
  scrollToTop: "উপরে যান",
17
24
  },
18
25
  ask: {
26
+ ai: "AI",
27
+ clear: "কথোপকথন মুছুন",
28
+ close: "বন্ধ করুন",
29
+ copy: "কথোপকথন অনুলিপি করুন",
19
30
  empty: "ডকুমেন্টেশন সম্পর্কে একটি প্রশ্ন করুন।",
20
31
  error: "দুঃখিত, কিছু একটা ভুল হয়েছে।",
21
32
  label: "একটি প্রশ্ন করুন",
22
33
  placeholder: "একটি প্রশ্ন করুন…",
23
34
  send: "পাঠান",
35
+ tip: "টিপ: চ্যাট খুলতে ও বন্ধ করতে চাপুন",
24
36
  title: "AI-কে জিজ্ঞাসা করুন",
37
+ you: "আপনি",
25
38
  },
26
39
  banner: { dismiss: "ঘোষণা বন্ধ করুন" },
27
40
  changelog: {
28
41
  description: "পণ্য আপডেট এবং রিলিজ নোট।",
42
+ showReleases: "{version} রিলিজ দেখান",
29
43
  title: "পরিবর্তন লগ",
30
44
  },
45
+ content: { diagramError: "এই ডায়াগ্রামটি রেন্ডার করা যায়নি।" },
31
46
  feedback: {
32
47
  no: "না",
33
48
  question: "এই পৃষ্ঠাটি কি সহায়ক ছিল?",
@@ -35,7 +50,24 @@ export const bn: UIStringsOverride = {
35
50
  yes: "হ্যাঁ",
36
51
  },
37
52
  languageSwitcher: { label: "ভাষা", untranslated: "অনূদিত নয়" },
38
- nav: { breadcrumb: "ব্রেডক্রাম্ব" },
53
+ nav: {
54
+ back: "ফিরে যান",
55
+ breadcrumb: "ব্রেডক্রাম্ব",
56
+ closeNavigation: "নেভিগেশন বন্ধ করুন",
57
+ deprecated: "অপ্রচলিত",
58
+ featured: "ফিচার্ড",
59
+ githubRepository: "GitHub রিপোজিটরি",
60
+ navigation: "নেভিগেশন",
61
+ primary: "প্রধান",
62
+ sections: "বিভাগসমূহ",
63
+ toggleNavigation: "নেভিগেশন টগল করুন",
64
+ toggleTheme: "রঙের থিম টগল করুন",
65
+ },
66
+ notFound: {
67
+ description: "আপনি যে পৃষ্ঠাটি খুঁজছেন তা আমরা খুঁজে পাইনি।",
68
+ home: "হোমে ফিরে যান",
69
+ title: "পৃষ্ঠা পাওয়া যায়নি",
70
+ },
39
71
  page: {
40
72
  lastUpdated: "সর্বশেষ আপডেট",
41
73
  next: "পরবর্তী",
@@ -45,11 +77,20 @@ export const bn: UIStringsOverride = {
45
77
  },
46
78
  search: {
47
79
  all: "সব",
80
+ allLanguages: "সব ভাষা",
81
+ askAi: "AI-কে জিজ্ঞাসা করুন",
82
+ askAiHint: "AI থেকে তাৎক্ষণিক উত্তর পান",
48
83
  button: "অনুসন্ধান",
49
84
  devOnly: "অনুসন্ধান প্রোডাকশন বিল্ডে উপলব্ধ।",
85
+ error: "কিছু একটা ভুল হয়েছে। অনুগ্রহ করে আবার চেষ্টা করুন।",
50
86
  label: "ডকুমেন্টেশন অনুসন্ধান করুন",
87
+ navigate: "নেভিগেট",
51
88
  noResults: "কোনো ফলাফল পাওয়া যায়নি।",
89
+ open: "খুলুন",
52
90
  placeholder: "ডকুমেন্টেশন অনুসন্ধান করুন…",
91
+ popular: "জনপ্রিয়",
92
+ preview: "প্রিভিউ",
93
+ results: "ফলাফল",
53
94
  },
54
95
  toc: { title: "এই পৃষ্ঠায়" },
55
96
  };