@lupinum/ginko-content 0.1.6 → 0.2.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 (230) hide show
  1. package/compatibility.json +1 -1
  2. package/dist/cli.mjs +2 -2
  3. package/dist/cms-contract/build.js +15 -20
  4. package/dist/cms-contract/types.d.ts +12 -7
  5. package/dist/core/content/document +1 -0
  6. package/dist/core/content/document.d.ts +16 -0
  7. package/dist/core/content/document.js +2 -0
  8. package/dist/core/content/graph.d.ts +3 -1
  9. package/dist/core/content/graph.js +21 -20
  10. package/dist/core/content/locale.d.ts +6 -1
  11. package/dist/core/content/locale.js +17 -6
  12. package/dist/core/content/path.d.ts +5 -5
  13. package/dist/core/content/path.js +8 -8
  14. package/dist/core/content/snapshot +1 -0
  15. package/dist/core/content/snapshot.d.ts +25 -0
  16. package/dist/core/content/snapshot.js +99 -0
  17. package/dist/core/provider-errors.d.ts +1 -1
  18. package/dist/core/provider-errors.js +1 -0
  19. package/dist/core/query/builder.js +3 -3
  20. package/dist/core/query/execute.js +75 -36
  21. package/dist/core/query/filter.js +2 -2
  22. package/dist/core/query/lower.js +79 -3
  23. package/dist/core/query/params.js +7 -7
  24. package/dist/core/query/plan.d.ts +18 -0
  25. package/dist/core/query/plan.js +7 -0
  26. package/dist/core/references/resolve.js +6 -9
  27. package/dist/features/agent/agent-markdown +1 -0
  28. package/dist/features/agent/agent-markdown.d.ts +116 -0
  29. package/dist/features/agent/agent-markdown.js +138 -0
  30. package/dist/features/agent/walker +1 -0
  31. package/dist/features/agent/walker.d.ts +10 -0
  32. package/dist/features/agent/walker.js +174 -0
  33. package/dist/features/collections/resolve.d.ts +2 -2
  34. package/dist/features/collections/resolve.js +19 -16
  35. package/dist/features/localization/links.js +1 -1
  36. package/dist/features/localization/results.d.ts +3 -3
  37. package/dist/features/localization/results.js +46 -41
  38. package/dist/features/localization/translated-slugs.js +5 -5
  39. package/dist/features/navigation/build.d.ts +2 -2
  40. package/dist/features/navigation/build.js +23 -26
  41. package/dist/features/navigation/canonical.d.ts +10 -11
  42. package/dist/features/navigation/canonical.js +26 -21
  43. package/dist/features/navigation/resolve.js +4 -4
  44. package/dist/features/navigation/tree.js +3 -3
  45. package/dist/{runtime → features}/query/backlinks.js +3 -5
  46. package/dist/{runtime → features}/query/documents.js +1 -1
  47. package/dist/{runtime → features}/query/localized-docs.d.ts +1 -1
  48. package/dist/{runtime → features}/query/localized-docs.js +3 -6
  49. package/dist/{runtime → features}/query/navigation.d.ts +1 -1
  50. package/dist/{runtime → features}/query/navigation.js +8 -8
  51. package/dist/{runtime → features}/query/populate.js +6 -14
  52. package/dist/{runtime → features}/query/unified.d.ts +4 -4
  53. package/dist/{runtime → features}/query/unified.js +4 -2
  54. package/dist/{runtime → features}/query/variants.js +4 -4
  55. package/dist/features/search/navigation.js +2 -2
  56. package/dist/features/search/sections.d.ts +1 -1
  57. package/dist/features/search/sections.js +1 -1
  58. package/dist/features/sitemap/query.d.ts +5 -5
  59. package/dist/features/sitemap/query.js +6 -6
  60. package/dist/integrations/nitro/ingest.js +9 -20
  61. package/dist/integrations/nitro/storage.d.ts +1 -0
  62. package/dist/integrations/nitro/storage.js +22 -8
  63. package/dist/module.d.mts +135 -812
  64. package/dist/module.json +1 -1
  65. package/dist/module.mjs +70 -27
  66. package/dist/parsers/csv.js +3 -3
  67. package/dist/parsers/index.js +3 -3
  68. package/dist/parsers/json.js +8 -6
  69. package/dist/parsers/markdown-plugins.js +9 -5
  70. package/dist/parsers/markdown.js +7 -6
  71. package/dist/parsers/path-meta.js +21 -19
  72. package/dist/parsers/reserved +1 -0
  73. package/dist/parsers/reserved.d.ts +15 -0
  74. package/dist/parsers/reserved.js +35 -0
  75. package/dist/parsers/yaml.js +6 -4
  76. package/dist/public/agent +1 -0
  77. package/dist/public/agent.d.ts +17 -0
  78. package/dist/public/agent.js +34 -0
  79. package/dist/public/client.d.ts +2 -3
  80. package/dist/public/client.js +1 -2
  81. package/dist/public/provider-query +1 -0
  82. package/dist/public/provider-query.d.ts +58 -0
  83. package/dist/public/provider-query.js +5 -0
  84. package/dist/public/provider.d.ts +8 -3
  85. package/dist/public/provider.js +2 -0
  86. package/dist/public/server.d.ts +7 -12
  87. package/dist/public/server.js +5 -36
  88. package/dist/runtime/app/components/ContentRenderer.d.vue.ts +3 -3
  89. package/dist/runtime/app/components/ContentRenderer.vue +2 -2
  90. package/dist/runtime/app/components/ContentRenderer.vue.d.ts +3 -3
  91. package/dist/runtime/app/components/ContentRendererInline.d.vue.ts +1 -1
  92. package/dist/runtime/app/components/ContentRendererInline.vue.d.ts +1 -1
  93. package/dist/runtime/app/components/Prose/ProseImg.d.vue.ts +4 -4
  94. package/dist/runtime/app/components/Prose/ProseImg.vue.d.ts +4 -4
  95. package/dist/runtime/app/components/Prose/ProsePre.d.vue.ts +1 -1
  96. package/dist/runtime/app/components/Prose/ProsePre.vue.d.ts +1 -1
  97. package/dist/runtime/app/components/internal/ContentRendererMarkdown.d.vue.ts +1 -1
  98. package/dist/runtime/app/components/internal/ContentRendererMarkdown.vue +4 -4
  99. package/dist/runtime/app/components/internal/ContentRendererMarkdown.vue.d.ts +1 -1
  100. package/dist/runtime/app/components/internal/MarkdownRenderer.d.ts +3 -3
  101. package/dist/runtime/app/composables/preview.d.ts +1 -1
  102. package/dist/runtime/app/composables/query-api.d.ts +5 -5
  103. package/dist/runtime/app/composables/query-api.js +5 -3
  104. package/dist/runtime/app/composables/route.d.ts +1 -1
  105. package/dist/runtime/app/composables/route.js +4 -4
  106. package/dist/runtime/app/composables/search.js +1 -1
  107. package/dist/runtime/app/composables/use-content-document.js +1 -1
  108. package/dist/runtime/app/composables/use-content-list.js +1 -1
  109. package/dist/runtime/app/composables/use-content-navigation.js +2 -2
  110. package/dist/runtime/app/composables/use-content-page.js +4 -4
  111. package/dist/runtime/app/pages/content.vue +2 -1
  112. package/dist/runtime/markdown/plugins.js +6 -8
  113. package/dist/runtime/server/agent-markdown.d.ts +5 -65
  114. package/dist/runtime/server/agent-markdown.js +40 -315
  115. package/dist/runtime/server/agent-site.js +1 -1
  116. package/dist/runtime/server/api/agent-raw.js +1 -1
  117. package/dist/runtime/server/api/cache.d.ts +1 -1
  118. package/dist/runtime/server/api/cache.js +43 -4
  119. package/dist/runtime/server/api/locales.js +3 -3
  120. package/dist/runtime/server/api/navigation.d.ts +1 -1
  121. package/dist/runtime/server/api/navigation.js +3 -1
  122. package/dist/runtime/server/api/query.js +2 -2
  123. package/dist/runtime/server/api/search-index.d.ts +1 -1
  124. package/dist/runtime/server/api/sitemap.d.ts +1 -1
  125. package/dist/runtime/server/cache-adapters.d.ts +18 -0
  126. package/dist/runtime/server/cache-adapters.js +12 -0
  127. package/dist/runtime/server/collection-helpers.d.ts +2 -2
  128. package/dist/runtime/server/collection-helpers.js +15 -11
  129. package/dist/runtime/server/index.d.ts +6 -4
  130. package/dist/runtime/server/index.js +10 -3
  131. package/dist/runtime/server/middleware/agent-link-headers.js +1 -1
  132. package/dist/runtime/server/middleware/agent-markdown.js +1 -1
  133. package/dist/runtime/server/navigation-query.d.ts +2 -2
  134. package/dist/runtime/server/navigation-query.js +52 -7
  135. package/dist/runtime/server/navigation.d.ts +1 -1
  136. package/dist/runtime/server/provider-document +1 -0
  137. package/dist/runtime/server/provider-document.d.ts +59 -0
  138. package/dist/runtime/server/provider-document.js +48 -0
  139. package/dist/runtime/server/provider-query-wire +1 -0
  140. package/dist/runtime/server/provider-query-wire.d.ts +32 -0
  141. package/dist/runtime/server/provider-query-wire.js +27 -0
  142. package/dist/runtime/server/provider-query.d.ts +18 -9
  143. package/dist/runtime/server/provider-query.js +62 -28
  144. package/dist/runtime/server/provider-result.d.ts +4 -4
  145. package/dist/runtime/server/providers/filesystem.js +3 -11
  146. package/dist/runtime/server/providers/index.d.ts +18 -1
  147. package/dist/runtime/server/providers/index.js +72 -20
  148. package/dist/runtime/server/query-api.d.ts +5 -5
  149. package/dist/runtime/server/query-api.js +9 -6
  150. package/dist/runtime/server/query-executor.d.ts +3 -3
  151. package/dist/runtime/server/query-executor.js +43 -59
  152. package/dist/runtime/server/search.d.ts +1 -1
  153. package/dist/runtime/server/search.js +4 -4
  154. package/dist/runtime/server/sitemap.js +5 -5
  155. package/dist/runtime/server/storage-access.d.ts +4 -1
  156. package/dist/runtime/server/storage-access.js +27 -2
  157. package/dist/runtime/transformers/component-resolver.d.ts +1 -1
  158. package/dist/storage/content.d.ts +2 -6
  159. package/dist/storage/content.js +19 -8
  160. package/dist/storage/contents.d.ts +9 -1
  161. package/dist/storage/contents.js +26 -19
  162. package/dist/storage/driver.d.ts +2 -2
  163. package/dist/storage/driver.js +2 -1
  164. package/dist/storage/graph.js +4 -0
  165. package/dist/storage/manifest.js +3 -1
  166. package/dist/storage/references.js +9 -5
  167. package/dist/storage/snapshot-runtime +1 -0
  168. package/dist/storage/snapshot-runtime.d.ts +11 -0
  169. package/dist/storage/snapshot-runtime.js +46 -0
  170. package/dist/storage/validation.d.ts +1 -1
  171. package/dist/storage/validation.js +47 -102
  172. package/dist/testing/provider-contract.d.ts +14 -4
  173. package/dist/testing/provider-contract.js +121 -68
  174. package/dist/testing/provider-fixture.d.ts +1 -1
  175. package/dist/testing/provider-fixture.js +124 -120
  176. package/dist/types/config.d.ts +8 -4
  177. package/dist/types/content.d.ts +104 -70
  178. package/dist/types/fields.js +1 -1
  179. package/dist/types/module.d.ts +1 -1
  180. package/dist/types/query-parts/public.d.ts +14 -5
  181. package/dist/types/query-parts/results.d.ts +6 -2
  182. package/dist/types/query-parts/transport.d.ts +19 -0
  183. package/dist/types/virtual.d.ts +6 -0
  184. package/dist/types.d.mts +2 -2
  185. package/dist/web-types.json +1 -1
  186. package/package.json +39 -9
  187. package/dist/runtime/app/plugins/markdown-components +0 -1
  188. package/dist/runtime/app/plugins/markdown-components.d.ts +0 -2
  189. package/dist/runtime/app/plugins/markdown-components.js +0 -19
  190. /package/dist/{runtime → features/agent}/agent-paths +0 -0
  191. /package/dist/{runtime → features/agent}/agent-paths.d.ts +0 -0
  192. /package/dist/{runtime → features/agent}/agent-paths.js +0 -0
  193. /package/dist/{runtime → features}/query/backlinks +0 -0
  194. /package/dist/{runtime → features}/query/backlinks.d.ts +0 -0
  195. /package/dist/{runtime → features}/query/context +0 -0
  196. /package/dist/{runtime → features}/query/context.d.ts +0 -0
  197. /package/dist/{runtime → features}/query/context.js +0 -0
  198. /package/dist/{runtime → features}/query/documents +0 -0
  199. /package/dist/{runtime → features}/query/documents.d.ts +0 -0
  200. /package/dist/{runtime → features}/query/errors +0 -0
  201. /package/dist/{runtime → features}/query/errors.d.ts +0 -0
  202. /package/dist/{runtime → features}/query/errors.js +0 -0
  203. /package/dist/{runtime → features}/query/handles +0 -0
  204. /package/dist/{runtime → features}/query/handles.d.ts +0 -0
  205. /package/dist/{runtime → features}/query/handles.js +0 -0
  206. /package/dist/{runtime → features}/query/locale-options +0 -0
  207. /package/dist/{runtime → features}/query/locale-options.d.ts +0 -0
  208. /package/dist/{runtime → features}/query/locale-options.js +0 -0
  209. /package/dist/{runtime → features}/query/localized-docs +0 -0
  210. /package/dist/{runtime → features}/query/navigation +0 -0
  211. /package/dist/{runtime → features}/query/pagination +0 -0
  212. /package/dist/{runtime → features}/query/pagination.d.ts +0 -0
  213. /package/dist/{runtime → features}/query/pagination.js +0 -0
  214. /package/dist/{runtime → features}/query/populate +0 -0
  215. /package/dist/{runtime → features}/query/populate.d.ts +0 -0
  216. /package/dist/{runtime → features}/query/public-limits +0 -0
  217. /package/dist/{runtime → features}/query/public-limits.d.ts +0 -0
  218. /package/dist/{runtime → features}/query/public-limits.js +0 -0
  219. /package/dist/{runtime → features}/query/resolve +0 -0
  220. /package/dist/{runtime → features}/query/resolve.d.ts +0 -0
  221. /package/dist/{runtime → features}/query/resolve.js +0 -0
  222. /package/dist/{runtime → features}/query/responses +0 -0
  223. /package/dist/{runtime → features}/query/responses.d.ts +0 -0
  224. /package/dist/{runtime → features}/query/responses.js +0 -0
  225. /package/dist/{runtime → features}/query/routes +0 -0
  226. /package/dist/{runtime → features}/query/routes.d.ts +0 -0
  227. /package/dist/{runtime → features}/query/routes.js +0 -0
  228. /package/dist/{runtime → features}/query/unified +0 -0
  229. /package/dist/{runtime → features}/query/variants +0 -0
  230. /package/dist/{runtime → features}/query/variants.d.ts +0 -0
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "Ginko Content release compatibility",
3
3
  "releaseStack": {
4
- "@lupinum/ginko-content": "0.1.6",
4
+ "@lupinum/ginko-content": "0.2.0",
5
5
  "@lupinum/ginko-cms": "0.1.2",
6
6
  "@lupinum/ginko-cms-convex": "0.1.1",
7
7
  "@lupinum/ginko-cms-contract": "0.1.1"
package/dist/cli.mjs CHANGED
@@ -505,7 +505,7 @@ async function inspectRepeatedLocalePrefixes(rootDir, locales) {
505
505
  severity: "error",
506
506
  file: relativeFile,
507
507
  message: `Repeated locale prefix "${repeatedPath}" found in generated output.`,
508
- suggestion: "Use locale-neutral raw _path values in content links and let Nuxt I18n add the route prefix once."
508
+ suggestion: "Bind route-ready item.path values from useContentMany(), navigation, or search results instead of manually prefixing locale paths."
509
509
  });
510
510
  break;
511
511
  }
@@ -573,7 +573,7 @@ async function inspectI18nSearchIndex(rootDir, locales) {
573
573
  return false;
574
574
  }
575
575
  const item = record;
576
- return item.locale === locale || item._locale === locale || typeof item.path === "string" && item.path.startsWith(`/${locale}/`);
576
+ return item.locale === locale || typeof item.path === "string" && item.path.startsWith(`/${locale}/`);
577
577
  });
578
578
  if (!hasLocaleRecords) {
579
579
  findings.push({
@@ -33,6 +33,8 @@ export class CmsContractSchemaUnsupportedError extends Error {
33
33
  this.unsupported = unsupported;
34
34
  this.name = "CmsContractSchemaUnsupportedError";
35
35
  }
36
+ collectionSlug;
37
+ unsupported;
36
38
  }
37
39
  export function buildCmsContract(config, options) {
38
40
  const include = options.include ? new Set(options.include) : null;
@@ -59,7 +61,7 @@ function buildCollectionContract(slug, collection, options) {
59
61
  const contract = {
60
62
  slug,
61
63
  label: cms.label ?? deriveLabel(slug),
62
- type: cms.type ?? deriveCollectionType(slug, collection),
64
+ type: resolveCollectionType(slug, cms),
63
65
  icon: cms.icon ?? null,
64
66
  locales,
65
67
  defaultLocale,
@@ -122,9 +124,11 @@ function isSingleRouteSource(collection) {
122
124
  const routeSources = sources.filter((source) => typeof source === "string");
123
125
  return routeSources.length === 1 && !/[{*?[\]]/.test(routeSources[0] ?? "");
124
126
  }
125
- function deriveCollectionType(slug, collection) {
126
- if (collection.cms?.type) return collection.cms.type;
127
- if (slug === "docs" || collection.cms?.route?.rootSlug) return "tree";
127
+ function resolveCollectionType(slug, cms) {
128
+ if (cms.type) return cms.type;
129
+ console.warn(
130
+ `[ginko-content] Collection "${slug}" has no explicit \`cms.type\`; defaulting to 'flat'. Declare \`cms: { type: 'flat' | 'tree' }\` in the collection config to silence this warning.`
131
+ );
128
132
  return "flat";
129
133
  }
130
134
  function deriveLabel(slug) {
@@ -142,15 +146,15 @@ function buildFields(collection, cms, isLocalized) {
142
146
  const existing = fields.get(key);
143
147
  fields.set(key, mergeField(existing, key, override, isLocalized));
144
148
  }
145
- return [...fields.values()].sort((a, b) => (a.order ?? 0) - (b.order ?? 0)).map((field2, index) => ({ ...field2, order: index }));
149
+ return [...fields.values()];
146
150
  }
147
151
  function implicitPageFields(collection, isLocalized) {
148
152
  const fields = [
149
- field({ key: "title", type: "text", required: true, localized: isLocalized }),
150
- field({ key: "description", type: "textarea", localized: isLocalized })
153
+ field({ key: "title", type: "text", role: "title", required: true, localized: isLocalized }),
154
+ field({ key: "description", type: "textarea", role: "description", localized: isLocalized })
151
155
  ];
152
156
  if (isMarkdownCollection(collection)) {
153
- fields.push(field({ key: "bodyMdc", type: "richtext", localized: isLocalized }));
157
+ fields.push(field({ key: "bodyMdc", type: "richtext", role: "body", localized: isLocalized }));
154
158
  }
155
159
  return fields;
156
160
  }
@@ -240,10 +244,7 @@ function getArrayElement(schema) {
240
244
  return def?.element ?? def?.type ?? null;
241
245
  }
242
246
  function fieldsFromObjectSchema(schema, isLocalized) {
243
- return Object.entries(getObjectShape(unwrapSchema(schema))).sort(([left], [right]) => left.localeCompare(right)).map(([key, child], index) => ({
244
- ...fieldFromSchema(key, child, isLocalized),
245
- order: index
246
- }));
247
+ return Object.entries(getObjectShape(unwrapSchema(schema))).sort(([left], [right]) => left.localeCompare(right)).map(([key, child]) => fieldFromSchema(key, child, isLocalized));
247
248
  }
248
249
  function fieldFromMetadata(key, metadata, schema, isLocalized, required) {
249
250
  const localized = metadata.localized ?? isLocalized;
@@ -313,14 +314,11 @@ function mergeField(existing, key, override, isLocalized) {
313
314
  ...override.description !== void 0 ? { description: override.description } : {},
314
315
  ...override.required !== void 0 ? { required: override.required } : {},
315
316
  ...override.localized !== void 0 ? { localized: override.localized } : {},
316
- ...override.hidden !== void 0 ? { hidden: override.hidden } : {},
317
317
  ...override.searchable !== void 0 ? { searchable: override.searchable } : {},
318
318
  ...override.sortable !== void 0 ? { sortable: override.sortable } : {},
319
- ...override.order !== void 0 ? { order: override.order } : {},
320
- ...override.width !== void 0 ? { width: override.width } : {},
321
319
  ...override.defaultValue !== void 0 ? { defaultValue: override.defaultValue } : {},
322
320
  ...override.validation !== void 0 ? { validation: override.validation } : {},
323
- ...override.condition !== void 0 ? { condition: override.condition } : {},
321
+ ...override.editor !== void 0 ? { editor: override.editor } : {},
324
322
  ...override.options !== void 0 ? { options: override.options } : {},
325
323
  ...override.relation !== void 0 ? { relation: override.relation } : {},
326
324
  ...override.min !== void 0 ? { min: override.min } : {},
@@ -334,22 +332,19 @@ function field(input) {
334
332
  return {
335
333
  key: input.key,
336
334
  type: input.type,
335
+ role: input.role ?? null,
337
336
  label: input.label ?? null,
338
337
  description: input.description ?? null,
339
338
  required: input.required ?? false,
340
339
  localized: input.localized ?? false,
341
- hidden: input.hidden ?? false,
342
340
  searchable: input.searchable ?? false,
343
341
  sortable: input.sortable ?? false,
344
- order: input.order ?? 0,
345
- width: input.width ?? "full",
346
342
  defaultValue: input.defaultValue,
347
343
  options: input.options ?? null,
348
344
  relation: input.relation ?? null,
349
345
  media: input.media ?? null,
350
346
  fields: input.fields ?? null,
351
347
  validation: input.validation ?? null,
352
- condition: input.condition ?? null,
353
348
  min: input.min ?? null,
354
349
  max: input.max ?? null,
355
350
  step: input.step ?? null,
@@ -63,15 +63,13 @@ export interface CmsCollectionRouting {
63
63
  export interface CmsFieldContract {
64
64
  key: string;
65
65
  type: ContentCmsFieldType;
66
+ role?: 'title' | 'description' | 'body' | null;
66
67
  label?: string | Record<string, string> | null;
67
68
  description?: string | null;
68
69
  required: boolean;
69
70
  localized: boolean;
70
- hidden: boolean;
71
71
  searchable: boolean;
72
72
  sortable: boolean;
73
- order: number;
74
- width: 'full' | 'half';
75
73
  defaultValue?: unknown;
76
74
  options?: string[] | null;
77
75
  relation?: ContentCmsRelationConfig | null;
@@ -81,12 +79,19 @@ export interface CmsFieldContract {
81
79
  } | null;
82
80
  fields?: CmsFieldContract[] | null;
83
81
  validation?: Record<string, unknown> | null;
84
- condition?: Record<string, unknown> | null;
85
82
  min?: number | null;
86
83
  max?: number | null;
87
84
  step?: number | null;
88
85
  slugFrom?: string | null;
89
86
  language?: string | null;
87
+ /**
88
+ * Opaque editor-layout passthrough forwarded byte-for-byte from
89
+ * `ContentCmsFieldConfig.editor`. ginko-content neither types nor interprets
90
+ * its contents — pure layout policy (width, display order, hidden state,
91
+ * conditional visibility, ...) lives here and its schema is owned by the
92
+ * consuming CMS. Absent when the collection config supplied no `editor`.
93
+ */
94
+ editor?: Record<string, unknown>;
90
95
  }
91
96
  /**
92
97
  * Reference to a schema validation artifact. The actual artifact (a serialized,
@@ -94,9 +99,9 @@ export interface CmsFieldContract {
94
99
  * shipped alongside the contract. The CMS reads the checksum to verify the
95
100
  * artifact it holds matches the contract it imported.
96
101
  *
97
- * Keeping the artifact behind a ref (rather than embedding it inline) lets the
98
- * CMS drop the artifact when it doesn't need publish validation (e.g. for a
99
- * code-only data collection).
102
+ * The artifact bytes are embedded so a contract consumer can validate content
103
+ * without fetching a second artifact, while `artifactId` and `checksum` keep the
104
+ * embedded payload identifiable and verifiable.
100
105
  */
101
106
  export interface CmsSchemaArtifactRef {
102
107
  /** Stable id used to look up the artifact in the artifact registry. */
@@ -0,0 +1 @@
1
+ export * from './document.js'
@@ -0,0 +1,16 @@
1
+ import type { MissingDocument, ParsedContent } from '../../types/content';
2
+ /**
3
+ * True when a loader result is a {@link MissingDocument} sentinel rather than a
4
+ * real parsed document. Keyed on the `missing: true` discriminant that every
5
+ * stub site sets — NOT on `body === null`: a real document may legitimately
6
+ * have a null body (data-style documents from custom transformers, e.g. the
7
+ * transformer example's `.names` files) and must not be treated as missing.
8
+ */
9
+ export declare const isMissingDocument: (document: ParsedContent | MissingDocument) => document is MissingDocument;
10
+ /**
11
+ * Shared type guard narrowing a loader result to a servable
12
+ * {@link ParsedContent}. Replaces the per-call-site `body !== null` checks so
13
+ * every consumer (snapshot writer, content list, single-document lookup) agrees
14
+ * on what counts as a real document.
15
+ */
16
+ export declare const isRealDocument: (document: ParsedContent | MissingDocument) => document is ParsedContent;
@@ -0,0 +1,2 @@
1
+ export const isMissingDocument = (document) => document.missing === true;
2
+ export const isRealDocument = (document) => !isMissingDocument(document);
@@ -76,7 +76,7 @@ export declare const resolveLocaleChain: (requestedLocale: string | undefined, d
76
76
  * `storage/content.ts:createServerQueryFetch` and
77
77
  * `memoizeRuntimeValue(event, 'graph', ...)`.
78
78
  *
79
- * INVARIANT: documents without `_path` are still indexed by id (so refs can
79
+ * INVARIANT: documents without `path` are still indexed by id (so refs can
80
80
  * find them) but are excluded from path/route/canonical lookups. Partials
81
81
  * and navigation documents are likewise indexed by id only.
82
82
  */
@@ -97,6 +97,7 @@ export declare const buildContentGraph: (documents: ParsedContent[], options?: {
97
97
  */
98
98
  export declare const resolveGraphVariant: (graph: ContentGraph, canonicalKey: string, requestedLocale?: string, options?: {
99
99
  defaultLocale?: string;
100
+ locales?: string[];
100
101
  fallback?: string[];
101
102
  exact?: boolean;
102
103
  localeFallback?: Record<string, string[]>;
@@ -120,6 +121,7 @@ export declare const resolveGraphVariant: (graph: ContentGraph, canonicalKey: st
120
121
  */
121
122
  export declare const resolveGraphRouteVariant: (graph: ContentGraph, routePath: string, requestedLocale?: string, options?: {
122
123
  defaultLocale?: string;
124
+ locales?: string[];
123
125
  fallback?: string[];
124
126
  exact?: boolean;
125
127
  localeFallback?: Record<string, string[]>;
@@ -1,4 +1,5 @@
1
1
  import { normalizeReferenceValue, buildReferenceTargets } from "../references/resolve.js";
2
+ import { sortLocalesCanonically } from "./locale.js";
2
3
  const normalizePath = (path) => {
3
4
  if (!path || path === "/") {
4
5
  return "/";
@@ -27,44 +28,44 @@ export const buildContentGraph = (documents, options = {}) => {
27
28
  const byNavigationPath = {};
28
29
  const defaultLocale = options.defaultLocale || "";
29
30
  for (const document of documents) {
30
- const documentId = document._id || `${document._collection || "content"}:${document._path || "document"}`;
31
+ const documentId = document.id || `${document.collection || "content"}:${document.path || "document"}`;
31
32
  byId[documentId] = document;
32
- if (!document._path) {
33
+ if (!document.path) {
33
34
  continue;
34
35
  }
35
- const path = normalizePath(document._path);
36
+ const path = normalizePath(document.path);
36
37
  manifest.paths[path] ||= [];
37
- if (document._locale === defaultLocale) {
38
+ if (document.locale === defaultLocale) {
38
39
  manifest.paths[path].unshift(documentId);
39
40
  } else {
40
41
  manifest.paths[path].push(documentId);
41
42
  }
42
- if (document._collection) {
43
- manifest.collections[document._collection] ||= [];
44
- manifest.collections[document._collection].push(documentId);
43
+ if (document.collection) {
44
+ manifest.collections[document.collection] ||= [];
45
+ manifest.collections[document.collection].push(documentId);
45
46
  }
46
- if (document._navigation) {
47
- const locale2 = document._locale || defaultLocale;
47
+ if (document.navigationFile) {
48
+ const locale2 = document.locale || defaultLocale;
48
49
  byNavigationPath[path] ||= {};
49
50
  byNavigationPath[path][locale2] = document;
50
51
  }
51
- const isVariantDocument = !document._partial && !document._navigation && document._canonicalKey;
52
+ const isVariantDocument = !document.partial && !document.navigationFile && document.canonicalKey;
52
53
  if (!isVariantDocument) {
53
54
  continue;
54
55
  }
55
- const locale = document._locale || defaultLocale;
56
+ const locale = document.locale || defaultLocale;
56
57
  const variant = {
57
- canonicalKey: document._canonicalKey,
58
+ canonicalKey: document.canonicalKey,
58
59
  contentId: documentId,
59
60
  locale,
60
61
  path,
61
62
  document
62
63
  };
63
- manifest.byCanonical[document._canonicalKey] ||= {};
64
- manifest.byCanonical[document._canonicalKey][locale] = variant;
65
- manifest.byRoute[`${locale}:${path}`] = document._canonicalKey;
66
- if (document._type === "markdown" && typeof document.ref === "string" && document.ref.length) {
67
- manifest.byRef[document.ref] = document._canonicalKey;
64
+ manifest.byCanonical[document.canonicalKey] ||= {};
65
+ manifest.byCanonical[document.canonicalKey][locale] = variant;
66
+ manifest.byRoute[`${locale}:${path}`] = document.canonicalKey;
67
+ if (document.type === "markdown" && typeof document.ref === "string" && document.ref.length) {
68
+ manifest.byRef[document.ref] = document.canonicalKey;
68
69
  }
69
70
  }
70
71
  for (const [canonicalKey, variants] of Object.entries(manifest.byCanonical)) {
@@ -87,7 +88,7 @@ export const buildContentGraph = (documents, options = {}) => {
87
88
  byRoute: manifest.byRoute,
88
89
  byRef: manifest.byRef,
89
90
  byNavigationPath,
90
- referenceTargets: buildReferenceTargets(documents.filter((document) => !document._partial && !document._navigation), options.locales || []),
91
+ referenceTargets: buildReferenceTargets(documents.filter((document) => !document.partial && !document.navigationFile), options.locales || []),
91
92
  manifest
92
93
  };
93
94
  };
@@ -101,7 +102,7 @@ export const resolveGraphVariant = (graph, canonicalKey, requestedLocale, option
101
102
  if (!Object.keys(scopedVariants).length) {
102
103
  return null;
103
104
  }
104
- const availableLocales = Object.keys(scopedVariants);
105
+ const availableLocales = sortLocalesCanonically(Object.keys(scopedVariants), options);
105
106
  const localeChain = options.exact ? requestedLocale ? [requestedLocale] : [] : options.fallback?.length ? Array.from(new Set([requestedLocale, ...options.fallback].filter(Boolean))) : resolveLocaleChain(requestedLocale, options.defaultLocale, options.localeFallback || {});
106
107
  const resolvedLocale = localeChain.find((locale) => scopedVariants[locale]) || (options.exact ? void 0 : availableLocales[0]);
107
108
  if (resolvedLocale === void 0) {
@@ -122,7 +123,7 @@ export const resolveGraphVariant = (graph, canonicalKey, requestedLocale, option
122
123
  export const resolveGraphRouteVariant = (graph, routePath, requestedLocale, options = {}) => {
123
124
  const normalizedPath = normalizePath(routePath);
124
125
  const localeChain = options.exact ? requestedLocale ? [requestedLocale] : [] : options.fallback?.length ? Array.from(new Set([requestedLocale, ...options.fallback].filter(Boolean))) : resolveLocaleChain(requestedLocale, options.defaultLocale, options.localeFallback || {});
125
- const localesToSearch = localeChain.length ? localeChain : Array.from(new Set(Object.values(graph.byCanonical).flatMap((variants) => Object.keys(variants))));
126
+ const localesToSearch = localeChain.length ? localeChain : sortLocalesCanonically(Object.values(graph.byCanonical).flatMap((variants) => Object.keys(variants)), options);
126
127
  for (const locale of localesToSearch) {
127
128
  const canonicalKey = graph.byRoute[`${locale}:${normalizedPath}`];
128
129
  if (!canonicalKey) {
@@ -9,7 +9,7 @@
9
9
  * 2. **Inline locale variant id** — YAML/JSON sources can carry locale
10
10
  * overrides inline (`i18n: { de: { title: '…' } }`). We split each
11
11
  * such document into one variant per locale at ingest; the
12
- * synthetic variants share a `_id` shape of
12
+ * synthetic variants share a `id` shape of
13
13
  * `${sourceId}#__locale=${locale}`. `splitInlineLocaleVariantId`
14
14
  * reverses that for lookups.
15
15
  *
@@ -33,6 +33,11 @@ export declare const INLINE_LOCALE_ID_SEPARATOR = "#__locale=";
33
33
  * buildLocaleFallbackChain('en', 'en') // []
34
34
  */
35
35
  export declare const buildLocaleFallbackChain: (locale: string, defaultLocale?: string, fallback?: Record<string, string[]>) => string[];
36
+ /** Canonical locale order: default locale first, then configured locale order. */
37
+ export declare const sortLocalesCanonically: (locales: string[], config?: {
38
+ defaultLocale?: string;
39
+ locales?: string[];
40
+ }) => string[];
36
41
  /**
37
42
  * Split an inline-variant id back into `{ sourceId, locale }`. Returns the
38
43
  * input unchanged (with `locale: undefined`) when no separator is found.
@@ -33,6 +33,17 @@ export const buildLocaleFallbackChain = (locale, defaultLocale, fallback) => {
33
33
  ...defaultLocale && defaultLocale !== locale ? [defaultLocale] : []
34
34
  ]));
35
35
  };
36
+ export const sortLocalesCanonically = (locales, config = {}) => {
37
+ const order = [
38
+ ...config.defaultLocale ? [config.defaultLocale] : [],
39
+ ...config.locales || []
40
+ ];
41
+ const rank = new Map(order.map((locale, index) => [locale, index]));
42
+ const inputOrder = new Map(locales.map((locale, index) => [locale, index]));
43
+ return Array.from(new Set(locales)).sort(
44
+ (left, right) => (rank.get(left) ?? Number.MAX_SAFE_INTEGER) - (rank.get(right) ?? Number.MAX_SAFE_INTEGER) || (inputOrder.get(left) ?? Number.MAX_SAFE_INTEGER) - (inputOrder.get(right) ?? Number.MAX_SAFE_INTEGER)
45
+ );
46
+ };
36
47
  export const splitInlineLocaleVariantId = (id) => {
37
48
  const separatorIndex = id.indexOf(INLINE_LOCALE_ID_SEPARATOR);
38
49
  if (separatorIndex === -1) {
@@ -44,7 +55,7 @@ export const splitInlineLocaleVariantId = (id) => {
44
55
  };
45
56
  };
46
57
  export const expandDataLocaleVariants = (document, i18nConfig) => {
47
- if (!i18nConfig || document._type !== "yaml" && document._type !== "json") {
58
+ if (!i18nConfig || document.type !== "yaml" && document.type !== "json") {
48
59
  return [document];
49
60
  }
50
61
  const rawI18n = isPlainObject(document.i18n) ? document.i18n : void 0;
@@ -52,15 +63,15 @@ export const expandDataLocaleVariants = (document, i18nConfig) => {
52
63
  return [document];
53
64
  }
54
65
  const { i18n: _removed, ...baseDocument } = document;
55
- const sourceLocale = document._locale || i18nConfig.defaultLocale;
56
- const variants = [{ ...baseDocument, _locale: sourceLocale }];
66
+ const sourceLocale = document.locale || i18nConfig.defaultLocale;
67
+ const variants = [{ ...baseDocument, locale: sourceLocale }];
57
68
  for (const locale of i18nConfig.locales) {
58
69
  if (locale === sourceLocale) {
59
70
  continue;
60
71
  }
61
72
  const override = rawI18n[locale];
62
73
  if (typeof override !== "undefined" && !isPlainObject(override)) {
63
- console.warn(`[content] Inline i18n override for locale "${locale}" in "${document._id}" must be an object. Skipping invalid override.`);
74
+ console.warn(`[content] Inline i18n override for locale "${locale}" in "${document.id}" must be an object. Skipping invalid override.`);
64
75
  continue;
65
76
  }
66
77
  if (!isPlainObject(override)) {
@@ -69,8 +80,8 @@ export const expandDataLocaleVariants = (document, i18nConfig) => {
69
80
  const merged = mergeLocaleOverride(baseDocument, override);
70
81
  variants.push({
71
82
  ...merged,
72
- _id: `${document._id}${INLINE_LOCALE_ID_SEPARATOR}${locale}`,
73
- _locale: locale
83
+ id: `${document.id}${INLINE_LOCALE_ID_SEPARATOR}${locale}`,
84
+ locale
74
85
  });
75
86
  }
76
87
  return variants;
@@ -1,9 +1,9 @@
1
1
  export declare const describeId: (id: string) => {
2
- _source: string;
3
- _path: string;
4
- _extension: string;
5
- _file: string;
6
- _basename: string;
2
+ source: string;
3
+ path: string;
4
+ extension: string;
5
+ file: string;
6
+ basename: string;
7
7
  };
8
8
  export declare const isDraftPath: (path: string) => boolean;
9
9
  export declare const isPartialPath: (path: string) => boolean;
@@ -4,18 +4,18 @@ import { slugifyUrlSegment } from "./slug.js";
4
4
  const SEMVER_REGEX = /^(\d+)(\.\d+)*(\.x)?$/;
5
5
  const NUMERIC_PREFIX_RE = /^(\d+)\.(.+)$/;
6
6
  export const describeId = (id) => {
7
- const [_source, ...parts] = id.split(":");
8
- const [, basename, _extension] = parts[parts.length - 1]?.match(/(.*)\.([^.]+)$/) || [];
7
+ const [source, ...parts] = id.split(":");
8
+ const [, basename, extension] = parts[parts.length - 1]?.match(/(.*)\.([^.]+)$/) || [];
9
9
  if (basename) {
10
10
  parts[parts.length - 1] = basename;
11
11
  }
12
- const _path = (parts || []).join("/");
12
+ const path = (parts || []).join("/");
13
13
  return {
14
- _source,
15
- _path,
16
- _extension,
17
- _file: _extension ? `${_path}.${_extension}` : _path,
18
- _basename: basename || ""
14
+ source,
15
+ path,
16
+ extension,
17
+ file: extension ? `${path}.${extension}` : path,
18
+ basename: basename || ""
19
19
  };
20
20
  };
21
21
  export const isDraftPath = (path) => /\.draft(\/|\.|$)/.test(path);
@@ -0,0 +1 @@
1
+ export * from './snapshot.js'
@@ -0,0 +1,25 @@
1
+ import type { ParsedContent } from '../../types/content';
2
+ export declare const CONTENT_SNAPSHOT_VERSION: 1;
3
+ export interface ContentSnapshot {
4
+ version: typeof CONTENT_SNAPSHOT_VERSION;
5
+ /** Must equal the runtime config's cache integrity; mismatch means a stale artifact. */
6
+ integrity: string;
7
+ generatedAt: number;
8
+ /** Fully-qualified, locale-suffixed ids. One per stored variant. */
9
+ documentIds: string[];
10
+ /** Source ids represented by the stored documents. Completeness is asserted on these. */
11
+ documentSourceIds: string[];
12
+ documents: ParsedContent[];
13
+ }
14
+ export interface BuildContentSnapshotArgs {
15
+ integrity: string;
16
+ documents: ParsedContent[];
17
+ sourceIds: string[];
18
+ now: number;
19
+ }
20
+ /** Build error carrying every offending path; never fail on just the first one. */
21
+ export declare class ContentSnapshotError extends Error {
22
+ }
23
+ export declare const buildContentSnapshot: (args: BuildContentSnapshotArgs) => ContentSnapshot;
24
+ export declare const isContentSnapshot: (value: unknown) => value is ContentSnapshot;
25
+ export declare const assertSnapshotComplete: (snapshot: ContentSnapshot, sourceIds: string[]) => void;
@@ -0,0 +1,99 @@
1
+ import { splitInlineLocaleVariantId } from "./locale.js";
2
+ export const CONTENT_SNAPSHOT_VERSION = 1;
3
+ export class ContentSnapshotError extends Error {
4
+ }
5
+ const documentIdOf = (document) => document.id;
6
+ const documentSourceIdOf = (document) => splitInlineLocaleVariantId(documentIdOf(document)).sourceId;
7
+ const isPlainObject = (value) => {
8
+ const prototype = Object.getPrototypeOf(value);
9
+ return prototype === Object.prototype || prototype === null;
10
+ };
11
+ const describeSymbolKey = (symbol) => `[${String(symbol)}] (symbol-keyed property)`;
12
+ const collectNonJsonValues = (value, path, ancestors, offenders) => {
13
+ if (value === null || value === void 0 || typeof value === "string" || typeof value === "boolean") {
14
+ return;
15
+ }
16
+ if (typeof value === "number") {
17
+ if (!Number.isFinite(value)) offenders.push(path);
18
+ return;
19
+ }
20
+ if (typeof value !== "object") {
21
+ offenders.push(path);
22
+ return;
23
+ }
24
+ if (value instanceof Date) {
25
+ if (Number.isNaN(value.getTime())) offenders.push(path);
26
+ return;
27
+ }
28
+ if (ancestors.has(value)) {
29
+ offenders.push(path);
30
+ return;
31
+ }
32
+ ancestors.add(value);
33
+ try {
34
+ for (const symbol of Object.getOwnPropertySymbols(value)) {
35
+ if (Object.prototype.propertyIsEnumerable.call(value, symbol)) {
36
+ offenders.push(`${path}${describeSymbolKey(symbol)}`);
37
+ }
38
+ }
39
+ if (Array.isArray(value)) {
40
+ for (let index = 0; index < value.length; index += 1) {
41
+ if (!(index in value)) {
42
+ offenders.push(`${path}[${index}]`);
43
+ continue;
44
+ }
45
+ collectNonJsonValues(value[index], `${path}[${index}]`, ancestors, offenders);
46
+ }
47
+ return;
48
+ }
49
+ if (!isPlainObject(value)) {
50
+ offenders.push(path);
51
+ return;
52
+ }
53
+ for (const [key, child] of Object.entries(value)) {
54
+ collectNonJsonValues(child, `${path}.${key}`, ancestors, offenders);
55
+ }
56
+ } finally {
57
+ ancestors.delete(value);
58
+ }
59
+ };
60
+ export const buildContentSnapshot = (args) => {
61
+ const lossy = [];
62
+ const documents = [];
63
+ for (const document of args.documents) {
64
+ const offenders = [];
65
+ collectNonJsonValues(document, "$", /* @__PURE__ */ new WeakSet(), offenders);
66
+ if (offenders.length) {
67
+ lossy.push(...offenders.map((path) => `${documentIdOf(document)}:${path}`));
68
+ continue;
69
+ }
70
+ documents.push(JSON.parse(JSON.stringify(document)));
71
+ }
72
+ if (lossy.length > 0) {
73
+ throw new ContentSnapshotError(
74
+ `[content] snapshot: ${lossy.length} non-JSON value(s) found (invalid Date, Map, circular reference, symbol key, ...): ${lossy.slice(0, 10).join(", ")}`
75
+ );
76
+ }
77
+ return {
78
+ version: CONTENT_SNAPSHOT_VERSION,
79
+ integrity: args.integrity,
80
+ generatedAt: args.now,
81
+ documentIds: documents.map(documentIdOf),
82
+ documentSourceIds: [...new Set(documents.map(documentSourceIdOf))].sort(),
83
+ documents
84
+ };
85
+ };
86
+ export const isContentSnapshot = (value) => {
87
+ if (typeof value !== "object" || value === null) return false;
88
+ const snapshot = value;
89
+ return snapshot.version === CONTENT_SNAPSHOT_VERSION && typeof snapshot.integrity === "string" && typeof snapshot.generatedAt === "number" && Array.isArray(snapshot.documentIds) && Array.isArray(snapshot.documentSourceIds) && Array.isArray(snapshot.documents);
90
+ };
91
+ export const assertSnapshotComplete = (snapshot, sourceIds) => {
92
+ const have = new Set(snapshot.documentSourceIds);
93
+ const missing = [...new Set(sourceIds)].filter((id) => !have.has(id));
94
+ if (missing.length > 0) {
95
+ throw new ContentSnapshotError(
96
+ `[content] snapshot incomplete: ${missing.length} source document(s) missing (first ${Math.min(missing.length, 20)}): ${missing.slice(0, 20).join(", ")}. This build would silently 404 these pages in production.`
97
+ );
98
+ }
99
+ };
@@ -1,4 +1,4 @@
1
- export type ContentProviderErrorCode = 'unknown_provider' | 'unknown_collection' | 'unsupported_provider_operation' | 'unsupported_query_operator' | 'unsupported_query_shape' | 'data_collection_route_access' | 'data_collection_search_access' | 'data_collection_sitemap_access' | 'missing_locale_route' | 'provider_config_missing' | 'provider_module_missing' | 'provider_module_invalid' | 'provider_result_invalid' | 'unsupported_provider_search_index' | 'unsupported_provider_search' | 'unsupported_provider_site_data' | 'unsupported_provider_prerender';
1
+ export type ContentProviderErrorCode = 'unknown_provider' | 'unknown_collection' | 'unsupported_provider_operation' | 'unsupported_query_operator' | 'unsupported_query_shape' | 'provider_query_not_json_pure' | 'data_collection_route_access' | 'data_collection_search_access' | 'data_collection_sitemap_access' | 'missing_locale_route' | 'provider_config_missing' | 'provider_module_missing' | 'provider_module_invalid' | 'provider_result_invalid' | 'unsupported_provider_search_index' | 'unsupported_provider_search' | 'unsupported_provider_site_data' | 'unsupported_provider_prerender';
2
2
  export declare const statusForProviderError: Record<ContentProviderErrorCode, number>;
3
3
  export interface ContentProviderError extends Error {
4
4
  statusCode: number;
@@ -4,6 +4,7 @@ export const statusForProviderError = {
4
4
  unsupported_provider_operation: 400,
5
5
  unsupported_query_operator: 400,
6
6
  unsupported_query_shape: 400,
7
+ provider_query_not_json_pure: 500,
7
8
  data_collection_route_access: 400,
8
9
  data_collection_search_access: 400,
9
10
  data_collection_sitemap_access: 400,
@@ -152,11 +152,11 @@ export function createQuery(fetcher, opts = {}) {
152
152
  const snapshot = { params: () => ({ ...this.params(), count: true }) };
153
153
  return fetcher(snapshot).then(resolveQueryResult);
154
154
  },
155
- locale: (_locale, options) => {
156
- const filteredWhere = queryParams.where ? ensureArray(queryParams.where).filter((item) => typeof item._locale === "undefined") : void 0;
155
+ locale: (locale, options) => {
156
+ const filteredWhere = queryParams.where ? ensureArray(queryParams.where).filter((item) => typeof item.locale === "undefined") : void 0;
157
157
  return next({
158
158
  resolveLocale: {
159
- locale: _locale,
159
+ locale,
160
160
  fallback: Array.isArray(options?.fallback) ? options?.fallback : Boolean(options?.fallback),
161
161
  exact: !options?.fallback
162
162
  },