@lupinum/ginko-content 0.1.1 → 0.1.2

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 (35) hide show
  1. package/README.md +22 -6
  2. package/compatibility.json +2 -2
  3. package/dist/cli.mjs +108 -36
  4. package/dist/core/references/schema.d.ts +1 -0
  5. package/dist/core/references/schema.js +33 -10
  6. package/dist/module.d.mts +78 -19
  7. package/dist/module.json +1 -1
  8. package/dist/module.mjs +196 -33
  9. package/dist/public/client.d.ts +4 -2
  10. package/dist/public/client.js +2 -0
  11. package/dist/runtime/app/components/ContentRenderer.vue +9 -5
  12. package/dist/runtime/app/composables/head.d.ts +27 -3
  13. package/dist/runtime/app/composables/head.js +82 -78
  14. package/dist/runtime/app/composables/query-api.d.ts +9 -10
  15. package/dist/runtime/app/composables/search.d.ts +4 -4
  16. package/dist/runtime/app/composables/search.js +11 -5
  17. package/dist/runtime/app/composables/use-content.d.ts +35 -15
  18. package/dist/runtime/app/composables/use-content.js +83 -13
  19. package/dist/runtime/query/unified.d.ts +1 -0
  20. package/dist/runtime/query/unified.js +93 -6
  21. package/dist/runtime/server/api/search.d.ts +2 -1
  22. package/dist/runtime/server/api/search.js +6 -2
  23. package/dist/runtime/server/pagefind.js +1 -0
  24. package/dist/runtime/server/search.d.ts +11 -0
  25. package/dist/runtime/server/search.js +24 -7
  26. package/dist/runtime/shared/search.js +3 -2
  27. package/dist/testing/provider-fixture.js +1 -0
  28. package/dist/types/config.d.ts +27 -7
  29. package/dist/types/config.js +26 -2
  30. package/dist/types/module.d.ts +35 -3
  31. package/dist/types/query.d.ts +19 -11
  32. package/dist/types/search.d.ts +3 -1
  33. package/dist/types.d.mts +2 -2
  34. package/dist/web-types.json +1 -1
  35. package/package.json +4 -4
package/dist/module.mjs CHANGED
@@ -6,6 +6,8 @@ import { defu } from 'defu';
6
6
  import { hash } from 'ohash';
7
7
  import { resolve, join, relative, isAbsolute } from 'pathe';
8
8
  import { withTrailingSlash } from 'ufo';
9
+ import { normalizeContentConfigCollectionNames } from '../dist/types/config';
10
+ export { defineCollection, defineContentConfig, reference } from '../dist/types/config';
9
11
  import 'unstorage/drivers/fs';
10
12
  import 'unstorage/drivers/http';
11
13
  import 'unstorage/drivers/github';
@@ -27,10 +29,11 @@ import { buildContentGraph } from '../dist/core/content/graph';
27
29
  import { normalizeCollectionSources, normalizeCollectionExcludes } from '../dist/core/content/sources';
28
30
  import { resolveCollectionI18n, prefixPathWithLocale } from '../dist/features/localization/path';
29
31
  import { resolveCollection } from '../dist/core/content/collection';
30
- export { defineCollection, defineContentConfig, reference } from '../dist/types/config';
32
+ import { normalizeRouteMounts, normalizeContentPath } from '../dist/core/content/path';
33
+ import { collectTopLevelReferenceFieldsByTarget } from '../dist/core/references/schema';
31
34
 
32
35
  const name = "@lupinum/ginko-content";
33
- const version = "0.1.1";
36
+ const version = "0.1.2";
34
37
 
35
38
  const logger = consola.withTag("@lupinum/ginko-content");
36
39
  const CACHE_VERSION = 3;
@@ -241,16 +244,8 @@ const registerContentDevRuntime = (nuxt, options, contentContext) => {
241
244
 
242
245
  const registerRuntimeImports = (resolveRuntimeModule) => {
243
246
  addImports([
244
- // Unified query API (ADR-0016) — app wrappers bind the Nuxt content context explicitly.
245
- { name: "one", as: "one", from: resolveRuntimeModule("./app/composables/query-api.js") },
246
- { name: "many", as: "many", from: resolveRuntimeModule("./app/composables/query-api.js") },
247
- { name: "paginate", as: "paginate", from: resolveRuntimeModule("./app/composables/query-api.js") },
248
- { name: "backlinks", as: "backlinks", from: resolveRuntimeModule("./app/composables/query-api.js") },
249
- { name: "resolveOne", as: "resolveOne", from: resolveRuntimeModule("./app/composables/query-api.js") },
250
- { name: "variants", as: "variants", from: resolveRuntimeModule("./app/composables/query-api.js") },
251
- { name: "tree", as: "tree", from: resolveRuntimeModule("./app/composables/query-api.js") },
252
- { name: "neighbors", as: "neighbors", from: resolveRuntimeModule("./app/composables/query-api.js") },
253
247
  { name: "getCollectionPath", as: "getCollectionPath", from: resolveRuntimeModule("./query/routes.js") },
248
+ { name: "useContentHead", as: "useContentHead", from: resolveRuntimeModule("./app/composables/head.js") },
254
249
  // Vue composables — reactive wrappers over the same options shape.
255
250
  { name: "useContentPage", as: "useContentPage", from: resolveRuntimeModule("./app/composables/use-content.js") },
256
251
  { name: "useContentOne", as: "useContentOne", from: resolveRuntimeModule("./app/composables/use-content.js") },
@@ -260,9 +255,12 @@ const registerRuntimeImports = (resolveRuntimeModule) => {
260
255
  { name: "useContentResolveOne", as: "useContentResolveOne", from: resolveRuntimeModule("./app/composables/use-content.js") },
261
256
  { name: "useContentVariants", as: "useContentVariants", from: resolveRuntimeModule("./app/composables/use-content.js") },
262
257
  { name: "useContentTree", as: "useContentTree", from: resolveRuntimeModule("./app/composables/use-content.js") },
258
+ { name: "useContentNavigation", as: "useContentNavigation", from: resolveRuntimeModule("./app/composables/use-content.js") },
263
259
  { name: "useContentNeighbors", as: "useContentNeighbors", from: resolveRuntimeModule("./app/composables/use-content.js") },
264
260
  { name: "useContentLocaleSwitch", as: "useContentLocaleSwitch", from: resolveRuntimeModule("./app/composables/use-content.js") },
265
- // Search & site data — kept (out of scope for ADR-0016).
261
+ { name: "useContentSwitchLocalePath", as: "useContentSwitchLocalePath", from: resolveRuntimeModule("./app/composables/route.js") },
262
+ // Search & site data — keep the headless useContentSearch export explicit
263
+ // because Nuxt UI owns the same auto-import name.
266
264
  { name: "useContentSearchData", as: "useContentSearchData", from: resolveRuntimeModule("./app/composables/search.js") },
267
265
  { name: "useContentSearchResults", as: "useContentSearchResults", from: resolveRuntimeModule("./app/composables/search.js") },
268
266
  { name: "querySiteData", as: "querySiteData", from: resolveRuntimeModule("./app/composables/site-data.js") }
@@ -357,25 +355,68 @@ const registerUserContentComponents = async (nuxt, resolve) => {
357
355
  }
358
356
  }
359
357
  };
360
- const registerGeneratedTypes = (contentConfigPath, resolveRuntimeModuleRoot) => {
358
+ const registerGeneratedTypes = (contentConfigPath, resolveRuntimeModuleRoot, collectionNames = [], i18nCollectionNames = []) => {
359
+ const collectionNameUnion = collectionNames.length ? collectionNames.map((name) => JSON.stringify(name)).join(" | ") : "never";
360
+ const i18nCollectionNameUnion = i18nCollectionNames.length ? i18nCollectionNames.map((name) => JSON.stringify(name)).join(" | ") : "never";
361
+ const collectionMapProperties = collectionNames.map((name) => {
362
+ const key = JSON.stringify(name);
363
+ return ` ${key}: StrictParsedContent & __GeneratedCollectionSchema<__ContentCollectionExport<${key}>>`;
364
+ });
361
365
  return addTypeTemplate({
362
366
  filename: "types/content.d.ts",
363
367
  getContents: () => [
364
368
  contentConfigPath ? `import type contentConfig from ${JSON.stringify(contentConfigPath)}` : "type __ContentConfig = { collections?: {} }",
369
+ contentConfigPath ? `import type * as contentConfigModule from ${JSON.stringify(contentConfigPath)}` : "declare const contentConfigModule: {}",
365
370
  "import type { StrictParsedContent } from '@lupinum/ginko-content'",
366
371
  "import type { CollectionSchema } from '@lupinum/ginko-content/config'",
367
372
  contentConfigPath ? "type __ContentConfig = typeof contentConfig" : "",
368
373
  "type __ContentCollections = NonNullable<__ContentConfig['collections']>",
374
+ `type __RuntimeContentCollectionNames = ${collectionNameUnion}`,
375
+ "type __NamedContentCollectionNames = {",
376
+ " [K in keyof typeof contentConfigModule & string]: typeof contentConfigModule[K] extends { name: K } ? K : never",
377
+ "}[keyof typeof contentConfigModule & string]",
378
+ "type __InferredContentCollectionNames = string extends keyof __ContentCollections",
379
+ " ? __NamedContentCollectionNames",
380
+ " : keyof __ContentCollections & string",
381
+ "type __ContentCollectionNames = [__RuntimeContentCollectionNames] extends [never]",
382
+ " ? __InferredContentCollectionNames",
383
+ " : __RuntimeContentCollectionNames",
384
+ "type __ContentCollectionExport<K extends string> = K extends keyof typeof contentConfigModule",
385
+ " ? typeof contentConfigModule[K]",
386
+ " : __ContentCollections[K]",
387
+ "type __GeneratedCollectionSchema<TCollection> = TCollection extends { __schema: infer TSchema }",
388
+ " ? TSchema extends { _output: infer TOutput } ? TOutput : {}",
389
+ " : CollectionSchema<TCollection>",
369
390
  "type __GeneratedContentCollectionMap = {",
370
- " [K in keyof __ContentCollections & string]: StrictParsedContent & CollectionSchema<__ContentCollections[K]>",
391
+ " [K in __ContentCollectionNames]: StrictParsedContent & __GeneratedCollectionSchema<__ContentCollectionExport<K>>",
371
392
  "}",
372
- "type __GeneratedI18nCollectionNames = {",
373
- " [K in keyof __ContentCollections & string]: NonNullable<__ContentCollections[K]['i18n']> extends never ? never : K",
374
- "}[keyof __ContentCollections & string]",
393
+ `type __RuntimeI18nCollectionNames = ${i18nCollectionNameUnion}`,
394
+ "type __InferredI18nCollectionNames = {",
395
+ " [K in __ContentCollectionNames]: __ContentCollectionExport<K> extends { __i18n: true }",
396
+ " ? K",
397
+ " : __ContentCollectionExport<K> extends { i18n: true } ? K : never",
398
+ "}[__ContentCollectionNames]",
399
+ "type __GeneratedI18nCollectionNames = [__RuntimeI18nCollectionNames] extends [never]",
400
+ " ? __InferredI18nCollectionNames",
401
+ " : __RuntimeI18nCollectionNames",
375
402
  "declare module '@lupinum/ginko-content' {",
376
403
  " interface ContentCollectionMap extends __GeneratedContentCollectionMap {}",
377
404
  " interface ContentCollectionI18nMap extends Pick<__GeneratedContentCollectionMap, __GeneratedI18nCollectionNames> {}",
378
405
  "}",
406
+ "declare global {",
407
+ " interface GinkoContentCollectionMap {",
408
+ ...collectionMapProperties,
409
+ " }",
410
+ " interface GinkoContentCollectionI18nMap extends Pick<__GeneratedContentCollectionMap, __GeneratedI18nCollectionNames> {}",
411
+ "}",
412
+ "declare module '@lupinum/ginko-content/dist/types/query' {",
413
+ " interface ContentCollectionMap extends __GeneratedContentCollectionMap {}",
414
+ " interface ContentCollectionI18nMap extends Pick<__GeneratedContentCollectionMap, __GeneratedI18nCollectionNames> {}",
415
+ "}",
416
+ "declare module '@lupinum/ginko-content/dist/types/query.js' {",
417
+ " interface ContentCollectionMap extends __GeneratedContentCollectionMap {}",
418
+ " interface ContentCollectionI18nMap extends Pick<__GeneratedContentCollectionMap, __GeneratedI18nCollectionNames> {}",
419
+ "}",
379
420
  "declare module '#content/server' {",
380
421
  ` const one: typeof import('${resolveRuntimeModuleRoot("./server")}').one`,
381
422
  ` const many: typeof import('${resolveRuntimeModuleRoot("./server")}').many`,
@@ -499,6 +540,23 @@ const toLocalPath = (value) => {
499
540
  return value;
500
541
  }
501
542
  };
543
+ const placeholderHosts = /* @__PURE__ */ new Set([
544
+ "example.com",
545
+ "example.net",
546
+ "example.org",
547
+ "localhost",
548
+ "127.0.0.1",
549
+ "0.0.0.0",
550
+ "::1"
551
+ ]);
552
+ const isPlaceholderHost = (host) => placeholderHosts.has(host) || host.endsWith(".example.com") || host.endsWith(".example.net") || host.endsWith(".example.org") || host.endsWith(".localhost");
553
+ const extractPlaceholderSiteUrls = (locValues) => Array.from(new Set(locValues.filter((value) => {
554
+ try {
555
+ return isPlaceholderHost(new URL(value).hostname);
556
+ } catch {
557
+ return false;
558
+ }
559
+ })));
502
560
  const discoverSitemapsFromDisk = async (outputPublicDir) => {
503
561
  const indexPath = join$1(outputPublicDir, SITEMAP_INDEX);
504
562
  try {
@@ -554,6 +612,9 @@ const normalizeContentSitemapAssertOptions = (options) => ({
554
612
  minUrlsPerSitemap: options?.minUrlsPerSitemap ?? 1,
555
613
  requireImages: options?.requireImages ?? false,
556
614
  requiredCollections: options?.requiredCollections ?? [],
615
+ requiredPaths: options?.requiredPaths ?? [],
616
+ forbiddenPathPrefixes: options?.forbiddenPathPrefixes ?? [],
617
+ requireProductionSiteUrl: options?.requireProductionSiteUrl ?? false,
557
618
  sitemaps: options?.sitemaps ?? {}
558
619
  });
559
620
  const shouldRunSitemapAssertionOnCompiled = (options, nitro) => {
@@ -595,6 +656,29 @@ async function assertGeneratedSitemaps({
595
656
  failures.push(`- ${target.name}: expected image entries but found none`);
596
657
  }
597
658
  }
659
+ const sitemapLocValues = discoveredSitemaps.flatMap((target) => extractLocValues(target.xml));
660
+ const sitemapPaths = Array.from(new Set(sitemapLocValues.map(toLocalPath)));
661
+ const pathSet = new Set(sitemapPaths);
662
+ const missingPaths = options.requiredPaths.filter((path) => !pathSet.has(path));
663
+ if (missingPaths.length) {
664
+ failures.push(`- Missing required sitemap paths: ${missingPaths.join(", ")}`);
665
+ }
666
+ const forbiddenPaths = sitemapPaths.filter(
667
+ (path) => options.forbiddenPathPrefixes.some((prefix) => path === prefix || path.startsWith(prefix.endsWith("/") ? prefix : `${prefix}/`))
668
+ );
669
+ if (forbiddenPaths.length) {
670
+ failures.push(`- Forbidden sitemap paths found: ${forbiddenPaths.join(", ")}`);
671
+ }
672
+ if (options.requireProductionSiteUrl) {
673
+ const placeholderSiteUrls = extractPlaceholderSiteUrls(sitemapLocValues);
674
+ if (placeholderSiteUrls.length) {
675
+ failures.push([
676
+ `- Placeholder sitemap URLs found: ${placeholderSiteUrls.join(", ")}`,
677
+ "Expected production URLs in generated sitemap loc values.",
678
+ "Set site.url or runtimeConfig.public.siteUrl to the deployed origin for production release checks."
679
+ ].join(" "));
680
+ }
681
+ }
598
682
  const missingCollections = options.requiredCollections.filter((collection) => (collectionRouteCounts[collection] || 0) === 0);
599
683
  if (missingCollections.length) {
600
684
  failures.push(`- Missing content sitemap routes for collections: ${missingCollections.join(", ")}`);
@@ -663,12 +747,15 @@ function configureNuxtSitemapSource(nuxt, apiBaseURL, sitemapPath = "/sitemap")
663
747
  }),
664
748
  source
665
749
  ];
666
- sitemap.excludeAppSources = true;
667
750
  }
668
751
  function resolveNuxtSitemapPrerenderRoutes(nuxt) {
669
752
  if (!hasNuxtSitemapModule(nuxt.options.modules)) {
670
753
  return [];
671
754
  }
755
+ const nuxtSitemap = nuxt.options.sitemap;
756
+ if (nuxtSitemap?.sitemaps === false) {
757
+ return ["/sitemap.xml"];
758
+ }
672
759
  const nuxtI18n = nuxt.options.i18n || {};
673
760
  if (!hasNuxtI18nModule(nuxt.options.modules) || !Array.isArray(nuxtI18n.locales) || nuxtI18n.locales.length === 0) {
674
761
  return ["/sitemap.xml"];
@@ -733,7 +820,7 @@ function normalizeSearchOptions(options) {
733
820
  }
734
821
  const defaultMiniSearchOptions = {
735
822
  fields: ["title", "content", "headings"],
736
- storeFields: ["path", "title", "excerpt", "anchor", "locale"],
823
+ storeFields: ["path", "title", "excerpt", "anchor", "locale", "collection"],
737
824
  boost: {
738
825
  title: 4,
739
826
  headings: 2,
@@ -1069,12 +1156,68 @@ const registerContentNitroIntegrationHooks = (nitroConfig, options, contentConte
1069
1156
  routes.add(route);
1070
1157
  }
1071
1158
  }
1072
- for (const route of options.sitemapPrerenderRoutes || []) {
1159
+ const sitemapPrerenderRoutes = typeof options.sitemapPrerenderRoutes === "function" ? options.sitemapPrerenderRoutes() : options.sitemapPrerenderRoutes || [];
1160
+ for (const route of sitemapPrerenderRoutes) {
1073
1161
  routes.add(route);
1074
1162
  }
1075
1163
  });
1076
1164
  };
1077
1165
 
1166
+ const getContentPageMeta = (page) => {
1167
+ const content = page.meta?.content;
1168
+ return content && typeof content === "object" ? content : void 0;
1169
+ };
1170
+ const isRouteConfig = (value) => typeof value === "string" || value !== null && typeof value === "object" && Object.values(value).every((entry) => typeof entry === "string");
1171
+ const describePage = (page) => page.file || page.path || "<unknown page>";
1172
+ const expectedLocalesFor = (collection, locales, defaultLocale) => {
1173
+ const collectionI18n = collection.i18n && typeof collection.i18n === "object" ? collection.i18n : void 0;
1174
+ return collectionI18n?.locales?.length ? collectionI18n.locales : locales.length ? locales : defaultLocale ? [defaultLocale] : [];
1175
+ };
1176
+ function validateContentPageRouteMetadata(pages, collections, options = {}) {
1177
+ const visit = (page) => {
1178
+ const contentMeta = getContentPageMeta(page);
1179
+ if (contentMeta) {
1180
+ validatePage(page, contentMeta, collections, options);
1181
+ }
1182
+ for (const child of page.children || []) {
1183
+ visit(child);
1184
+ }
1185
+ };
1186
+ for (const page of pages) {
1187
+ visit(page);
1188
+ }
1189
+ }
1190
+ function validatePage(page, contentMeta, collections, options) {
1191
+ const collectionName = contentMeta.collection;
1192
+ if (typeof collectionName !== "string" || !collectionName) {
1193
+ throw new Error(`@lupinum/ginko-content page "${describePage(page)}" declares content route metadata without a string collection name.`);
1194
+ }
1195
+ const collection = collections[collectionName];
1196
+ if (!collection) {
1197
+ throw new Error(`@lupinum/ginko-content page "${describePage(page)}" declares content collection "${collectionName}", but no matching collection exists in content.config.ts.`);
1198
+ }
1199
+ if (!isRouteConfig(contentMeta.route)) {
1200
+ throw new Error(`@lupinum/ginko-content page "${describePage(page)}" declares content collection "${collectionName}" but no route metadata. Add definePageMeta({ content: { collection: '${collectionName}', route: ... } }).`);
1201
+ }
1202
+ const locales = expectedLocalesFor(collection, options.locales || [], options.defaultLocale);
1203
+ const defaultLocale = collection.i18n && typeof collection.i18n === "object" ? collection.i18n.defaultLocale : options.defaultLocale;
1204
+ const expected = normalizeRouteMounts(collection.route, locales, defaultLocale);
1205
+ const actual = normalizeRouteMounts(contentMeta.route, locales, defaultLocale);
1206
+ if (!expected) {
1207
+ throw new Error(`@lupinum/ginko-content page "${describePage(page)}" declares route metadata for collection "${collectionName}", but that collection has no route config in content.config.ts.`);
1208
+ }
1209
+ for (const locale of Object.keys(expected)) {
1210
+ const expectedRoute = normalizeContentPath(expected[locale]);
1211
+ const actualRoute = actual?.[locale] ? normalizeContentPath(actual[locale]) : void 0;
1212
+ if (!actualRoute) {
1213
+ throw new Error(`@lupinum/ginko-content route metadata mismatch in "${describePage(page)}": collection "${collectionName}" is missing locale "${locale}" route "${expectedRoute}".`);
1214
+ }
1215
+ if (actualRoute !== expectedRoute) {
1216
+ throw new Error(`@lupinum/ginko-content route metadata mismatch in "${describePage(page)}": collection "${collectionName}" locale "${locale}" expected route "${expectedRoute}" but page metadata declares "${actualRoute}".`);
1217
+ }
1218
+ }
1219
+ }
1220
+
1078
1221
  const module$1 = defineNuxtModule({
1079
1222
  meta: {
1080
1223
  name,
@@ -1180,6 +1323,7 @@ const module$1 = defineNuxtModule({
1180
1323
  const resolvedI18n = resolveModuleI18nOptions(options, nuxt);
1181
1324
  const resolvedSitemap = normalizeSitemapOptions(options);
1182
1325
  const resolvedSearch = normalizeSearchOptions(options);
1326
+ validateCollectionNames(appContentConfig.collections);
1183
1327
  options.collections = Object.fromEntries(Object.entries(appContentConfig.collections).map(([name2, collection]) => [
1184
1328
  name2,
1185
1329
  {
@@ -1212,6 +1356,12 @@ const module$1 = defineNuxtModule({
1212
1356
  if (resolvedSitemap !== false) {
1213
1357
  configureNuxtSitemapSource(nuxt, options.api.baseURL, resolvedSitemap.path);
1214
1358
  }
1359
+ nuxt.hook("pages:extend", (pages) => {
1360
+ validateContentPageRouteMetadata(pages, options.collections, {
1361
+ locales: resolvedI18n.locales,
1362
+ defaultLocale: resolvedI18n.defaultLocale
1363
+ });
1364
+ });
1215
1365
  if (resolvedSitemap && resolvedSitemap.assert.enabled) {
1216
1366
  nuxt.hook("sitemap:prerender:done", async ({ sitemaps }) => {
1217
1367
  const assertOptions = contentContext.sitemap?.assert;
@@ -1233,7 +1383,12 @@ const module$1 = defineNuxtModule({
1233
1383
  registerRuntimeImports(resolveRuntimeModule);
1234
1384
  registerRuntimeComponents(resolve);
1235
1385
  registerContentComponentsTemplate(addTemplate);
1236
- registerGeneratedTypes(contentConfigPath, resolveRuntimeModule);
1386
+ registerGeneratedTypes(
1387
+ contentConfigPath,
1388
+ resolveRuntimeModule,
1389
+ Object.keys(appContentConfig.collections),
1390
+ Object.entries(appContentConfig.collections).filter(([, collection]) => Boolean(collection.i18n)).map(([name2]) => name2)
1391
+ );
1237
1392
  await registerUserContentComponents(nuxt, resolve);
1238
1393
  const getSearchRuntime = () => contentContext.search === false ? false : createSearchRuntimeConfig(contentContext.search, options.api.baseURL);
1239
1394
  nuxt.hook("nitro:config", (nitroConfig) => {
@@ -1291,7 +1446,7 @@ const module$1 = defineNuxtModule({
1291
1446
  }
1292
1447
  registerContentNitroIntegrationHooks(nitroConfig, {
1293
1448
  rootDir: nuxt.options.rootDir,
1294
- sitemapPrerenderRoutes: contentContext.sitemap === false ? [] : resolveNuxtSitemapPrerenderRoutes(nuxt)
1449
+ sitemapPrerenderRoutes: () => contentContext.sitemap === false ? [] : resolveNuxtSitemapPrerenderRoutes(nuxt)
1295
1450
  }, {
1296
1451
  collections: contentContext.collections,
1297
1452
  locales: contentContext.locales,
@@ -1345,17 +1500,22 @@ const module$1 = defineNuxtModule({
1345
1500
  contentContext.defaultLocale = contentContext.defaultLocale || contentContext.locales[0];
1346
1501
  contentContext.markdown = processMarkdownOptions(contentContext.markdown);
1347
1502
  await validateBuiltinMarkdownPlugins(contentContext.markdown.plugins, resolvePath);
1348
- const runtimeCollections = Object.fromEntries(Object.entries(options.collections || {}).map(([name2, collection]) => [
1349
- name2,
1350
- {
1351
- ...collection.source ? { source: collection.source } : {},
1352
- ...collection.exclude ? { exclude: collection.exclude } : {},
1353
- strict: collection.strict ?? true,
1354
- ...collection.route ? { route: collection.route } : {},
1355
- ...typeof collection.sitemap === "boolean" ? { sitemap: collection.sitemap } : {},
1356
- ...collection.i18n && collection.i18n !== true ? { i18n: collection.i18n } : {}
1357
- }
1358
- ]));
1503
+ const runtimeCollections = Object.fromEntries(Object.entries(options.collections || {}).map(([name2, collection]) => {
1504
+ const references = collectTopLevelReferenceFieldsByTarget(collection.schema);
1505
+ return [
1506
+ name2,
1507
+ {
1508
+ ...collection.source ? { source: collection.source } : {},
1509
+ ...collection.exclude ? { exclude: collection.exclude } : {},
1510
+ ...collection.type ? { type: collection.type } : {},
1511
+ strict: collection.strict ?? true,
1512
+ ...collection.route ? { route: collection.route } : {},
1513
+ ...typeof collection.sitemap === "boolean" ? { sitemap: collection.sitemap } : {},
1514
+ ...collection.i18n && collection.i18n !== true ? { i18n: collection.i18n } : {},
1515
+ ...Object.keys(references).length ? { references } : {}
1516
+ }
1517
+ ];
1518
+ }));
1359
1519
  const cacheIntegrity = hash({
1360
1520
  locales: contentContext.locales,
1361
1521
  defaultLocale: contentContext.defaultLocale,
@@ -1385,6 +1545,9 @@ function assertConfiguredProviderAvailable(contentContext) {
1385
1545
  }
1386
1546
  throw new Error(`content.config.ts sets provider "${provider}", but no provider module registered it. Register a module for "${provider}" or add it to content providers.`);
1387
1547
  }
1548
+ function validateCollectionNames(collections) {
1549
+ normalizeContentConfigCollectionNames(collections);
1550
+ }
1388
1551
  function validateRemovedMarkdownOptions(options) {
1389
1552
  if (options.highlight !== void 0) {
1390
1553
  throw new Error("`content.highlight` was removed. Enable syntax highlighting with `content.markdown.plugins`, for example `[['highlight', { ...options }]]`.");
@@ -7,9 +7,11 @@
7
7
  export { one, many, paginate, backlinks, resolveOne, variants, tree, neighbors } from '../runtime/app/composables/query-api.js';
8
8
  export { getCollectionPath } from '../runtime/query/routes.js';
9
9
  export type { CollectionPathOptions } from '../runtime/query/routes.js';
10
- export { useContentPage, useContentOne, useContentMany, useContentPagination, useContentBacklinks, useContentResolveOne, useContentVariants, useContentTree, useContentNeighbors, useContentLocaleSwitch } from '../runtime/app/composables/use-content.js';
10
+ export { useContentHead } from '../runtime/app/composables/head.js';
11
+ export { useContentPage, useContentOne, useContentMany, useContentPagination, useContentBacklinks, useContentResolveOne, useContentVariants, useContentTree, useContentNavigation, useContentNeighbors, useContentLocaleSwitch } from '../runtime/app/composables/use-content.js';
11
12
  export type { UseContentPageOptions } from '../runtime/app/composables/use-content.js';
12
- export type { QueryWhere, QueryOperators, ContentSelector, ContentResolvedMeta, BacklinkFields, BacklinkSource, BacklinksOptions, BacklinksResult, LocaleFallback, DocumentFromHandle, OneOptions, ManyOptions, PaginationOptions, PaginationResult, PopulateSpec, PopulatedDocument, ResolveOneOptions, ResolveOneResult, LocalizedDoc, LocalePathEntry, NeighborsOptions, NeighborsResult, ResolutionEnvelope, SortSpec, TreeOptions, VariantsOptions, ContentVariant, ContentNavigationItem, ContentTreeItem, ContentRouteMeta, ContentSearchSection } from '../types/query.js';
13
+ export type { QueryWhere, QueryOperators, ContentSelector, ContentResolvedMeta, BacklinkFields, BacklinkSource, BacklinksOptions, BacklinksResult, LocaleFallback, DocumentFromHandle, OneOptions, ManyOptions, PaginationOptions, PaginationResult, PopulateSpec, PopulatedDocument, ResolveOneOptions, ResolveOneResult, LocalizedDoc, LocalePathEntry, NeighborsOptions, NeighborsResult, ResolutionEnvelope, SortSpec, TreeOptions, VariantsOptions, ContentVariant, ContentNavigationItem, ContentCollectionName, ContentCollectionStringName, ContentCollectionTarget, ContentTreeItem, ContentRouteMeta, ContentSearchSection } from '../types/query.js';
14
+ export type { ContentNavigationNode } from '../runtime/app/composables/use-content.js';
13
15
  export { useContentSearch, useContentSearchData, useContentSearchResults } from '../runtime/app/composables/search.js';
14
16
  export type { UseContentSearchDataOptions, UseContentSearchDataResult, UseContentSearchOptions, UseContentSearchResult, UseContentSearchResultsOptions, UseContentSearchResultsResult } from '../runtime/app/composables/search.js';
15
17
  export { querySiteData } from '../runtime/app/composables/site-data.js';
@@ -9,6 +9,7 @@ export {
9
9
  neighbors
10
10
  } from "../runtime/app/composables/query-api.js";
11
11
  export { getCollectionPath } from "../runtime/query/routes.js";
12
+ export { useContentHead } from "../runtime/app/composables/head.js";
12
13
  export {
13
14
  useContentPage,
14
15
  useContentOne,
@@ -18,6 +19,7 @@ export {
18
19
  useContentResolveOne,
19
20
  useContentVariants,
20
21
  useContentTree,
22
+ useContentNavigation,
21
23
  useContentNeighbors,
22
24
  useContentLocaleSwitch
23
25
  } from "../runtime/app/composables/use-content.js";
@@ -11,6 +11,13 @@ function normalizeBody(value, excerpt) {
11
11
  }
12
12
  return null;
13
13
  }
14
+ function warnUnsupportedValue(value, excerpt) {
15
+ const path = value?._path ? ` for "${value._path}"` : "";
16
+ const target = excerpt ? "excerpt" : "body";
17
+ console.warn(
18
+ `[ginko-content] <ContentRenderer> could not render ${target}${path}. Pass the full content document with a markdown body, or provide an \`empty\` slot for empty/unsupported content.`
19
+ );
20
+ }
14
21
  export default defineComponent({
15
22
  name: "ContentRenderer",
16
23
  props: {
@@ -74,11 +81,8 @@ export default defineComponent({
74
81
  ...this.$attrs
75
82
  });
76
83
  }
77
- return h(
78
- "pre",
79
- null,
80
- JSON.stringify({ message: "You should use slots with <ContentRenderer>", value, excerpt, tag, prose, unwrap }, null, 2)
81
- );
84
+ warnUnsupportedValue(value, excerpt);
85
+ return null;
82
86
  }
83
87
  });
84
88
  </script>
@@ -1,11 +1,35 @@
1
1
  import type { RouteLocationNormalized, RouteLocationNormalizedLoaded } from 'vue-router';
2
2
  import type { Ref } from 'vue';
3
- import type { ParsedContent } from '../../../types/content';
3
+ type ContentHeadObject = {
4
+ title?: string;
5
+ description?: string;
6
+ image?: string | Record<string, any>;
7
+ meta?: Array<Record<string, any>>;
8
+ link?: Array<Record<string, any>>;
9
+ [key: string]: any;
10
+ };
11
+ type ContentHeadDocument = {
12
+ title?: string;
13
+ description?: string;
14
+ image?: string | Record<string, any>;
15
+ head?: ContentHeadObject;
16
+ seo?: ContentHeadObject;
17
+ };
18
+ type ContentHeadRuntimeConfig = {
19
+ app: {
20
+ baseURL: string;
21
+ };
22
+ public: {
23
+ siteUrl?: unknown;
24
+ };
25
+ };
4
26
  /**
5
27
  * Apply sensible page head defaults from a parsed content document.
6
28
  *
7
- * The composable merges explicit `content.head` values with common fallbacks
29
+ * The composable merges explicit `content.head` or `content.seo` values with common fallbacks
8
30
  * from the document (`title`, `description`, `image`) and adds canonical /
9
31
  * Open Graph URL metadata when a public site URL is configured.
10
32
  */
11
- export declare const useContentHead: (_content: ParsedContent | Ref<ParsedContent>, to?: RouteLocationNormalized | RouteLocationNormalizedLoaded) => void;
33
+ export declare const useContentHead: (_content: ContentHeadDocument | null | undefined | Ref<ContentHeadDocument | null | undefined>, to?: RouteLocationNormalized | RouteLocationNormalizedLoaded) => void;
34
+ export declare const resolveContentHead: (data: ContentHeadDocument | null | undefined, to: Pick<RouteLocationNormalized | RouteLocationNormalizedLoaded, "path" | "fullPath">, config: ContentHeadRuntimeConfig) => ContentHeadObject | null;
35
+ export {};