@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/README.md CHANGED
@@ -45,7 +45,7 @@ Define a collection:
45
45
  // content.config.ts
46
46
  import { defineCollection, defineContentConfig } from '@lupinum/ginko-content/config'
47
47
 
48
- export const pages = defineCollection('pages', {
48
+ export const pages = defineCollection({
49
49
  type: 'page',
50
50
  source: '**/*.md'
51
51
  })
@@ -74,9 +74,7 @@ Render the current route through the collection:
74
74
  ```vue
75
75
  <!-- pages/[...slug].vue -->
76
76
  <script setup lang="ts">
77
- import { pages } from '~/content.config'
78
-
79
- const { page } = await useContentPage(pages)
77
+ const { page } = await useContentPage('pages')
80
78
  </script>
81
79
 
82
80
  <template>
@@ -89,9 +87,10 @@ const { page } = await useContentPage(pages)
89
87
  - file-authored pages and navigation from `content/`
90
88
  - collection definitions as the source of truth for content shape and source
91
89
  files
92
- - Markdown, MDC, YAML, JSON, and CSV ingestion
90
+ - Markdown, Comark component tags, YAML, JSON, and CSV ingestion
93
91
  - locale-aware content routing
94
- - route-aware page loading with `useContentPage(handle)`
92
+ - route-aware page loading with `useContentPage('pages')`
93
+ - semantic previous/next route-page data through `useContentPage('pages', { surround })`
95
94
  - server reads through `one`, `many`, `paginate`, `resolveOne`, `tree`, and
96
95
  `neighbors`
97
96
  - Vue composables for the same read model
@@ -99,6 +98,23 @@ const { page } = await useContentPage(pages)
99
98
  - sitemap integration for public content routes
100
99
  - a server-side provider contract for advanced custom sources
101
100
 
101
+ ## I18n, Sitemap, and Prerender Ownership
102
+
103
+ For localized Nuxt apps, keep one source of truth per route type:
104
+
105
+ - Static Nuxt page paths belong in Nuxt I18n `i18n.pages`.
106
+ - Content page paths belong in Ginko collection routes and content files.
107
+ - Sitemap XML is generated by `@nuxtjs/sitemap`.
108
+ - Ginko registers the content sitemap source and contributes content prerender
109
+ routes.
110
+
111
+ Do not duplicate docs, blog, pricing, privacy, or translated locale paths in
112
+ `sitemap.urls` or app-owned `nitro.prerender.routes` arrays. Use
113
+ `@nuxtjs/sitemap >= 8.0.15` when translated static app slugs such as `/preise`
114
+ and `/en/pricing` need cross-locale sitemap alternates.
115
+
116
+ See the public guide: [Sitemap and prerender](../../docs/content/docs/6.i18n/4.sitemap-prerender.md).
117
+
102
118
  ## Scope
103
119
 
104
120
  The default provider reads files from your Nuxt project. The package does not
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "Ginko Content release compatibility",
3
3
  "releaseStack": {
4
- "@lupinum/ginko-content": "0.1.1",
4
+ "@lupinum/ginko-content": "0.1.2",
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"
@@ -13,7 +13,7 @@
13
13
  "convex": ["1.38.0", "^1.38.0"],
14
14
  "convex-helpers": ["^0.1.117", "0.1.117"],
15
15
  "h3": ["1.15.11"],
16
- "nuxt": ["^4.4.6", "4.4.6", ">=4.0.0"],
16
+ "nuxt": ["^4.4.7", "4.4.7", ">=4.4.7"],
17
17
  "typescript": ["~6.0.3", "^6.0.3", "6.0.3"],
18
18
  "vite": ["^7.3.3", "7.3.3"],
19
19
  "vitest": ["^4.1.6", ">=4.1.6"],
package/dist/cli.mjs CHANGED
@@ -42,32 +42,27 @@ const sourceChecks = [
42
42
  {
43
43
  pattern: /\bqueryCollectionItemSurroundings\s*\(/,
44
44
  message: "Nuxt Content v3 surround helper found.",
45
- suggestion: "Use useContentNeighbors(handle, { by: { route } }) in route page components."
45
+ suggestion: "Use useContentPage('docs', { surround: true }) in route page components."
46
46
  },
47
47
  {
48
48
  pattern: /\bqueryCollectionSearchSections\s*\(/,
49
49
  message: "Nuxt Content v3 search sections helper found.",
50
- suggestion: "Use useContentSearchData(collection) for UI search data."
50
+ suggestion: "Use useContentSearchData('docs') for UI search data."
51
51
  },
52
52
  {
53
53
  pattern: /\bqueryCollectionNavigation\s*\(/,
54
54
  message: "Nuxt Content v3 navigation helper found.",
55
- suggestion: "Use useContentTree(handle) for layout navigation."
55
+ suggestion: "Use useContentNavigation('docs') for layout navigation."
56
56
  },
57
57
  {
58
58
  pattern: /\bqueryCollection\s*\(/,
59
59
  message: "Removed collection query helper found.",
60
- suggestion: "Use one(handle, options), many(handle, options), paginate(handle, options), or the matching useContent* composable."
60
+ suggestion: "Use one('docs', options), many('docs', options), paginate('docs', options), or the matching useContent* composable."
61
61
  },
62
62
  {
63
63
  pattern: /\buseContentList\s*\(/,
64
64
  message: "Removed content list composable found.",
65
- suggestion: "Use useContentMany(handle, options) or many(handle, options)."
66
- },
67
- {
68
- pattern: /\buseContentNavigation\s*\(/,
69
- message: "Removed content navigation composable found.",
70
- suggestion: "Use useContentTree(handle) for layout navigation."
65
+ suggestion: "Use useContentMany('docs', options) or many('docs', options)."
71
66
  },
72
67
  {
73
68
  pattern: /\bcontent\.(database|preview|build)\b/,
@@ -84,15 +79,10 @@ const sourceChecks = [
84
79
  message: "ContentRenderer is receiving a document body.",
85
80
  suggestion: "Pass the full content document to ContentRenderer, not document.body."
86
81
  },
87
- {
88
- pattern: /<NuxtLink\b[^>]*:to\s*=\s*["'][^"']+\.path["']/,
89
- message: "NuxtLink may be using raw query .path.",
90
- suggestion: "Use useContentMany(handle, options) for route-safe list items, or link route-page payloads with their explicit path field."
91
- },
92
82
  {
93
83
  pattern: /<NuxtLink\b[^>]*:to\s*=\s*["'][^"']+\._path["']/,
94
84
  message: "NuxtLink is using raw content _path.",
95
- suggestion: "Use useContentMany(handle, options) for list pages and bind the route-safe item.path field."
85
+ suggestion: "Use useContentMany('docs', options) for list pages and bind the route-safe item.path field."
96
86
  }
97
87
  ];
98
88
  const lockfileNames = ["pnpm-lock.yaml", "package-lock.json", "yarn.lock", "bun.lockb"];
@@ -156,36 +146,91 @@ function findMatchingBrace(text, start) {
156
146
  }
157
147
  return text.length - 1;
158
148
  }
159
- function findCollectionDefinitions(text) {
160
- const definitions = [];
149
+ function findAuthoredCollectionDefinitions(text) {
150
+ const definitions = /* @__PURE__ */ new Map();
161
151
  const callPattern = /\bdefineCollection\s*\(/g;
162
152
  for (const match of text.matchAll(callPattern)) {
163
153
  const callStart = match.index || 0;
164
154
  const argsStart = callStart + match[0].length;
165
155
  const args = text.slice(argsStart);
166
- const namedMatch = args.match(/^\s*(['"])([^'"]+)\1\s*,\s*\{/);
167
- if (namedMatch) {
168
- const bodyStart = argsStart + namedMatch[0].lastIndexOf("{");
169
- const bodyEnd = findMatchingBrace(text, bodyStart);
170
- definitions.push({
171
- name: namedMatch[2],
172
- block: text.slice(bodyStart, bodyEnd + 1)
173
- });
156
+ const objectMatch = args.match(/^\s*\{/);
157
+ if (!objectMatch) {
174
158
  continue;
175
159
  }
176
- const objectMatch = args.match(/^\s*\{/);
177
- const propertyMatch = text.slice(0, callStart).match(/([a-z_$][\w$]*)\s*:\s*$/i);
178
- if (objectMatch && propertyMatch) {
179
- const bodyStart = argsStart + objectMatch[0].lastIndexOf("{");
180
- const bodyEnd = findMatchingBrace(text, bodyStart);
181
- definitions.push({
182
- name: propertyMatch[1],
183
- block: text.slice(bodyStart, bodyEnd + 1)
160
+ const bodyStart = argsStart + objectMatch[0].lastIndexOf("{");
161
+ const bodyEnd = findMatchingBrace(text, bodyStart);
162
+ const block = text.slice(bodyStart, bodyEnd + 1);
163
+ const prefix = text.slice(0, callStart);
164
+ const propertyMatch = prefix.match(/([a-z_$][\w$]*)\s*:\s*$/i);
165
+ if (propertyMatch) {
166
+ definitions.set(propertyMatch[1], {
167
+ key: propertyMatch[1],
168
+ block
184
169
  });
170
+ continue;
171
+ }
172
+ const variableMatch = prefix.match(/(?:^|[\s;])(?:export\s+)?(?:const|let|var)\s+([a-z_$][\w$]*)\s*=\s*$/i);
173
+ if (variableMatch) {
174
+ definitions.set(variableMatch[1], { block });
185
175
  }
186
176
  }
187
177
  return definitions;
188
178
  }
179
+ function findCollectionDefinitions(text) {
180
+ const authoredCollections = findAuthoredCollectionDefinitions(text);
181
+ const definitions = /* @__PURE__ */ new Map();
182
+ for (const block of findObjectPropertyBlocks(text, "collections")) {
183
+ for (const match of block.matchAll(/([a-z_$][\w$]*)\s*:\s*([a-z_$][\w$]*)\b/g)) {
184
+ const [, key, identifier] = match;
185
+ const authored = authoredCollections.get(identifier);
186
+ if (authored) {
187
+ definitions.set(key, {
188
+ name: key,
189
+ block: authored.block
190
+ });
191
+ }
192
+ }
193
+ for (const match of block.matchAll(/(?:^|[,{]\s*)([a-z_$][\w$]*)\s*(?=,|\})/g)) {
194
+ const key = match[1];
195
+ const authored = authoredCollections.get(key);
196
+ if (authored) {
197
+ definitions.set(key, {
198
+ name: key,
199
+ block: authored.block
200
+ });
201
+ }
202
+ }
203
+ }
204
+ for (const authored of authoredCollections.values()) {
205
+ if (authored.key) {
206
+ definitions.set(authored.key, {
207
+ name: authored.key,
208
+ block: authored.block
209
+ });
210
+ }
211
+ }
212
+ return [...definitions.values()];
213
+ }
214
+ function findObjectPropertyBlocks(text, property) {
215
+ const blocks = [];
216
+ const pattern = new RegExp(`\\b${property}\\s*:\\s*\\{`, "g");
217
+ for (const match of text.matchAll(pattern)) {
218
+ const bodyStart = text.indexOf("{", match.index);
219
+ if (bodyStart === -1) {
220
+ continue;
221
+ }
222
+ const bodyEnd = findMatchingBrace(text, bodyStart);
223
+ blocks.push(text.slice(bodyStart, bodyEnd + 1));
224
+ }
225
+ return blocks;
226
+ }
227
+ function extractStringArrayProperty(block, property) {
228
+ const match = block.match(new RegExp(`\\b${property}\\s*:\\s*\\[([\\s\\S]*?)\\]`));
229
+ if (!match) {
230
+ return [];
231
+ }
232
+ return [...match[1].matchAll(/['"]([^'"]+)['"]/g)].map((item) => item[1]);
233
+ }
189
234
  async function collectFiles(dir, rootDir, files = []) {
190
235
  const entries = await readdir(dir, { withFileTypes: true });
191
236
  for (const entry of entries) {
@@ -261,6 +306,32 @@ async function inspectSourceFiles(rootDir) {
261
306
  }
262
307
  return findings;
263
308
  }
309
+ async function inspectSearchCollections(rootDir) {
310
+ const contentConfig = await readTextIfPresent(join(rootDir, "content.config.ts"));
311
+ const nuxtConfig = await readTextIfPresent(join(rootDir, "nuxt.config.ts"));
312
+ if (!contentConfig || !nuxtConfig) {
313
+ return [];
314
+ }
315
+ const dataCollections = new Set(
316
+ findCollectionDefinitions(contentConfig).filter((collection) => /\btype\s*:\s*['"]data['"]/.test(collection.block)).map((collection) => collection.name)
317
+ );
318
+ if (!dataCollections.size) {
319
+ return [];
320
+ }
321
+ const configuredCollections = new Set(
322
+ findObjectPropertyBlocks(nuxtConfig, "search").flatMap((block) => extractStringArrayProperty(block, "collections"))
323
+ );
324
+ const dataSearchCollections = [...configuredCollections].filter((collection) => dataCollections.has(collection));
325
+ if (!dataSearchCollections.length) {
326
+ return [];
327
+ }
328
+ return [{
329
+ severity: "info",
330
+ file: "nuxt.config.ts",
331
+ message: `Data-only collections listed in content.search.collections: ${dataSearchCollections.join(", ")}.`,
332
+ suggestion: "Remove data-only collections from the static public search index, make them route-backed pages, or use provider-backed search with route-safe results."
333
+ }];
334
+ }
264
335
  async function inspectLockfiles(rootDir) {
265
336
  const findings = [];
266
337
  for (const lockfileName of lockfileNames) {
@@ -510,7 +581,7 @@ async function inspectI18nSourceSmells(rootDir, locales) {
510
581
  severity: "error",
511
582
  file: relativeFile,
512
583
  message: "Hardcoded locale route branch found.",
513
- suggestion: "Use @nuxtjs/i18n route helpers, useContentTree(handle), or useContentMany(handle, options) item paths instead of branching on locale codes."
584
+ suggestion: "Use @nuxtjs/i18n route helpers, useContentNavigation('docs'), or useContentMany('docs', options) item paths instead of branching on locale codes."
514
585
  });
515
586
  }
516
587
  if (/<(?:NuxtLink|U[A-Za-z]+)\b[^>]*:to\s*=\s*["'][^"']+\._path["']/.test(text)) {
@@ -518,7 +589,7 @@ async function inspectI18nSourceSmells(rootDir, locales) {
518
589
  severity: "error",
519
590
  file: relativeFile,
520
591
  message: "UI link is bound to raw content _path.",
521
- suggestion: "Use useContentMany(handle, options) for localized list pages and bind item.path."
592
+ suggestion: "Use useContentMany('docs', options) for localized list pages and bind item.path."
522
593
  });
523
594
  }
524
595
  }
@@ -703,6 +774,7 @@ async function runDoctor(options = {}) {
703
774
  const findings = [
704
775
  ...await inspectPackageJson(rootDir),
705
776
  ...await inspectSourceFiles(rootDir),
777
+ ...await inspectSearchCollections(rootDir),
706
778
  ...await inspectLockfiles(rootDir),
707
779
  ...await inspectSitemap(rootDir),
708
780
  ...options.i18n ? await inspectI18n(rootDir) : []
@@ -5,4 +5,5 @@ export declare const getObjectShape: (schema: any) => Record<string, any>;
5
5
  export declare const getReferenceDescriptor: (schema: any) => {
6
6
  collection?: string;
7
7
  } | null;
8
+ export declare const collectTopLevelReferenceFieldsByTarget: (schema: unknown) => Record<string, string[]>;
8
9
  export declare const collectTopLevelReferenceFields: (schema: unknown, targetCollection?: string) => string[];
@@ -53,18 +53,41 @@ const getArrayElement = (schema) => {
53
53
  const def = getSchemaDef(current);
54
54
  return def.element || def.type || null;
55
55
  };
56
- export const collectTopLevelReferenceFields = (schema, targetCollection) => {
56
+ const wildcardReferenceTarget = "*";
57
+ const getTopLevelReferenceDescriptor = (schema) => {
58
+ const direct = getReferenceDescriptor(schema);
59
+ if (direct) {
60
+ return direct;
61
+ }
62
+ const arrayElement = getArrayElement(schema);
63
+ return arrayElement ? getReferenceDescriptor(arrayElement) : null;
64
+ };
65
+ export const collectTopLevelReferenceFieldsByTarget = (schema) => {
57
66
  const current = unwrapSchema(schema);
58
67
  if (getSchemaTypeName(current) !== "ZodObject") {
59
- return [];
68
+ return {};
60
69
  }
61
- return Object.entries(getObjectShape(current)).filter(([, childSchema]) => {
62
- const direct = getReferenceDescriptor(childSchema);
63
- if (direct) {
64
- return !direct.collection || !targetCollection || direct.collection === targetCollection;
70
+ const references = {};
71
+ for (const [field, childSchema] of Object.entries(getObjectShape(current))) {
72
+ const descriptor = getTopLevelReferenceDescriptor(childSchema);
73
+ if (!descriptor) {
74
+ continue;
65
75
  }
66
- const arrayElement = getArrayElement(childSchema);
67
- const arrayRef = arrayElement ? getReferenceDescriptor(arrayElement) : null;
68
- return Boolean(arrayRef && (!arrayRef.collection || !targetCollection || arrayRef.collection === targetCollection));
69
- }).map(([field]) => field);
76
+ const target = descriptor.collection || wildcardReferenceTarget;
77
+ references[target] ||= [];
78
+ references[target].push(field);
79
+ }
80
+ return references;
81
+ };
82
+ export const collectTopLevelReferenceFields = (schema, targetCollection) => {
83
+ const references = collectTopLevelReferenceFieldsByTarget(schema);
84
+ if (!targetCollection) {
85
+ return [...new Set(Object.values(references).flat())];
86
+ }
87
+ return [
88
+ .../* @__PURE__ */ new Set([
89
+ ...references[targetCollection] || [],
90
+ ...references[wildcardReferenceTarget] || []
91
+ ])
92
+ ];
70
93
  };
package/dist/module.d.mts CHANGED
@@ -4,8 +4,8 @@ import { ContentCollectionConfig as ContentCollectionConfig$1, ContentCollection
4
4
  export { defineCollection, defineContentConfig, reference } from '../dist/types/config';
5
5
  import { StorageValue } from 'unstorage';
6
6
  import { ModuleOptions as ModuleOptions$1, ContentContext as ContentContext$1, ContentRevalidateOptions as ContentRevalidateOptions$1 } from '../dist/types/module';
7
- import { ContentSearchPublicRuntimeConfig as ContentSearchPublicRuntimeConfig$1 } from '../dist/types/search';
8
7
  import { ZodType, z } from 'zod';
8
+ import { ContentSearchPublicRuntimeConfig as ContentSearchPublicRuntimeConfig$1 } from '../dist/types/search';
9
9
  import { ListenOptions } from 'listhen';
10
10
 
11
11
  /**
@@ -29,6 +29,7 @@ type ContentCollectionSource = string | string[];
29
29
  * express roots such as `{ en: '/docs', de: '/dokumentation' }`.
30
30
  */
31
31
  type ContentCollectionRouteConfig = string | Record<string, string>;
32
+ type ContentCollectionKind = 'page' | 'data';
32
33
  type ContentCmsFieldType = 'text' | 'textarea' | 'richtext' | 'slug' | 'email' | 'url' | 'number' | 'range' | 'select' | 'multiselect' | 'radio' | 'checkbox' | 'toggle' | 'date' | 'datetime' | 'time' | 'json' | 'object' | 'array' | 'blocks' | 'relation' | 'relations' | 'image' | 'images' | 'file' | 'icon' | 'code' | 'color' | 'divider' | 'section';
33
34
  interface ContentCmsRelationConfig {
34
35
  collectionId: string;
@@ -88,6 +89,11 @@ interface ContentCollectionSourceObject {
88
89
  * Declarative collection definition used by `content.config.ts`.
89
90
  */
90
91
  interface ContentCollectionConfig<TSchema extends ZodType | undefined = ZodType | undefined> {
92
+ /**
93
+ * Collection kind declared in `defineCollection`. Page collections are public
94
+ * content routes by default; data collections are app-owned records.
95
+ */
96
+ type?: ContentCollectionKind;
91
97
  /**
92
98
  * Source glob or source descriptor understood by the filesystem ingestion
93
99
  * layer. CMS-backed projects do not need a runtime source; filesystem
@@ -140,13 +146,13 @@ interface ContentCollectionConfig<TSchema extends ZodType | undefined = ZodType
140
146
  */
141
147
  cms?: ContentCmsCollectionConfig;
142
148
  }
143
- type ContentCollectionKind = 'page' | 'data';
144
149
  type ContentProviderName = 'filesystem' | (string & {});
145
150
  type DefineCollectionOptions<TSchema extends ZodType | undefined = ZodType | undefined> = Omit<ContentCollectionConfig<TSchema>, 'source' | 'exclude'>;
146
151
  interface DefineCollectionObject<TSchema extends ZodType | undefined = ZodType | undefined> extends DefineCollectionOptions<TSchema> {
147
152
  /**
148
153
  * Nuxt Content v3-compatible collection kind. Ginko does not keep separate
149
- * page/data collection runtimes; `data` collections default to `sitemap: false`.
154
+ * page/data query builders, but the kind remains canonical metadata at runtime.
155
+ * `data` collections default to `sitemap: false`.
150
156
  */
151
157
  type: ContentCollectionKind;
152
158
  /**
@@ -219,6 +225,17 @@ interface ContentCollectionHandle<Name extends string = string, TSchema extends
219
225
  */
220
226
  readonly __schema: TSchema;
221
227
  }
228
+ type IsI18nConfig<TConfig> = TConfig extends {
229
+ i18n: true | ContentCollectionI18nConfig;
230
+ } ? TConfig['i18n'] extends false ? false : true : false;
231
+ type CollectionNameFromConfigKey<Key extends string, TCollection> = TCollection extends ContentCollectionHandle<infer Name, ZodType | undefined, boolean> ? [Name] extends [never] ? Key : Name : Key;
232
+ type NamedContentCollection<Key extends string, TCollection> = TCollection extends ContentCollectionHandle<infer Name, infer TSchema, infer TI18n> ? Omit<TCollection, 'name' | '__schema' | '__i18n'> & ContentCollectionHandle<[
233
+ Name
234
+ ] extends [never] ? Key : Name, TSchema, TI18n> : TCollection extends ContentCollectionConfig<infer TSchema> ? Omit<TCollection, 'name' | '__schema' | '__i18n'> & ContentCollectionHandle<CollectionNameFromConfigKey<Key, TCollection>, TSchema, IsI18nConfig<TCollection>> : TCollection;
235
+ type NamedContentCollections<TCollections extends Record<string, ContentCollectionConfig>> = {
236
+ [Key in keyof TCollections]: Key extends string ? NamedContentCollection<Key, TCollections[Key]> : TCollections[Key];
237
+ };
238
+ declare function normalizeContentConfigCollectionNames<TCollections extends Record<string, ContentCollectionConfig>>(collections: TCollections): NamedContentCollections<TCollections>;
222
239
 
223
240
  type LayoutKey = string;
224
241
  interface ContentSeoImage {
@@ -1107,12 +1124,20 @@ interface ContentQueryRequest {
1107
1124
  */
1108
1125
  type ContentQueryFetcher<T> = (query: ContentQueryRequest) => Promise<ContentQueryResponse<T>>;
1109
1126
  type QueryMatchOperator = (item: unknown, condition: unknown) => boolean;
1110
- interface ContentCollectionMap {
1127
+ declare global {
1128
+ interface GinkoContentCollectionMap {
1129
+ }
1130
+ interface GinkoContentCollectionI18nMap {
1131
+ }
1132
+ }
1133
+ interface ContentCollectionMap extends GinkoContentCollectionMap {
1111
1134
  }
1112
- interface ContentCollectionI18nMap {
1135
+ interface ContentCollectionI18nMap extends GinkoContentCollectionI18nMap {
1113
1136
  }
1114
- type ContentCollectionName = keyof ContentCollectionMap & string;
1137
+ type ContentCollectionName = keyof GinkoContentCollectionMap & string;
1115
1138
  type ContentCollectionItem<K extends ContentCollectionName> = ContentCollectionMap[K];
1139
+ type ContentCollectionStringName = [ContentCollectionName] extends [never] ? string : ContentCollectionName;
1140
+ type ContentCollectionTarget = ContentCollectionHandle | ContentCollectionStringName;
1116
1141
  /**
1117
1142
  * ============================================================================
1118
1143
  * Unified Query API — public surface (ADR-0016)
@@ -1203,8 +1228,8 @@ type HandleSchema<H> = H extends {
1203
1228
  __schema: infer S;
1204
1229
  } ? S extends {
1205
1230
  _output: infer O;
1206
- } ? O & StrictParsedContentMeta : StrictParsedContentMeta : StrictParsedContentMeta;
1207
- type SelectFields<H> = H extends string ? ReadonlyArray<string> : ReadonlyArray<Extract<keyof HandleSchema<H>, string>>;
1231
+ } ? O & StrictParsedContentMeta : StrictParsedContentMeta : H extends ContentCollectionName ? ContentCollectionMap[H] & StrictParsedContentMeta : StrictParsedContentMeta;
1232
+ type SelectFields<H> = string extends H ? ReadonlyArray<string> : ReadonlyArray<Extract<keyof HandleSchema<H>, string>>;
1208
1233
  /**
1209
1234
  * Runtime document shape inferred from a collection handle.
1210
1235
  */
@@ -1212,12 +1237,12 @@ type DocumentFromHandle<H> = H extends {
1212
1237
  __schema: {
1213
1238
  _output: infer O;
1214
1239
  };
1215
- } ? O & ParsedContent : ParsedContent;
1240
+ } ? O & StrictParsedContent : H extends ContentCollectionName ? ContentCollectionMap[H] : ParsedContent;
1216
1241
  /**
1217
1242
  * Explicit reference population map. Keys are fields on the source document;
1218
1243
  * values are target collection handles.
1219
1244
  */
1220
- type PopulateSpec = Record<string, ContentCollectionHandle | string>;
1245
+ type PopulateSpec = Record<string, ContentCollectionTarget>;
1221
1246
  type PopulatedDocument<T, P> = P extends undefined ? T : P extends PopulateSpec ? string extends keyof P ? T : Omit<T, keyof P> & {
1222
1247
  [K in keyof P & keyof T]: T[K] extends ReadonlyArray<unknown> ? Array<LocalizedDoc<DocumentFromHandle<P[K]>>> : LocalizedDoc<DocumentFromHandle<P[K]>> | null;
1223
1248
  } : T;
@@ -1232,7 +1257,7 @@ type PopulateOption<P extends PopulateSpec | undefined = undefined> = {
1232
1257
  */
1233
1258
  type HandleIsI18n<H> = H extends {
1234
1259
  __i18n: infer I;
1235
- } ? I extends true ? true : false : false;
1260
+ } ? I extends true ? true : false : H extends keyof GinkoContentCollectionI18nMap ? true : false;
1236
1261
  /**
1237
1262
  * Locale option made required when the handle is i18n.
1238
1263
  */
@@ -1349,7 +1374,7 @@ interface PaginationResult<T = ParsedContentMeta> {
1349
1374
  nextPage: number | null;
1350
1375
  prevPage: number | null;
1351
1376
  }
1352
- type BacklinkSource = ContentCollectionHandle | string;
1377
+ type BacklinkSource = ContentCollectionTarget;
1353
1378
  type SourceName<S> = S extends string ? S : S extends {
1354
1379
  name: infer N;
1355
1380
  } ? Extract<N, string> : string;
@@ -1376,7 +1401,7 @@ type BacklinksOptions<Target = unknown, Source extends BacklinkSource | Readonly
1376
1401
  limit?: number;
1377
1402
  skip?: number;
1378
1403
  fallback?: LocaleFallback;
1379
- select?: Source extends string ? ReadonlyArray<string> : ReadonlyArray<Extract<keyof DocumentFromSource<Source>, string>>;
1404
+ select?: string extends Source ? ReadonlyArray<string> : ReadonlyArray<Extract<keyof DocumentFromSource<Source>, string>>;
1380
1405
  } & BacklinksLocaleOption<Target, Source> & PopulateOption<P>;
1381
1406
  type BacklinksResult<Source extends BacklinkSource | ReadonlyArray<BacklinkSource> = BacklinkSource | ReadonlyArray<BacklinkSource>, P extends PopulateSpec | undefined = undefined> = Array<LocalizedDoc<PopulatedDocument<DocumentFromSource<Source>, P>>>;
1382
1407
  /**
@@ -1448,7 +1473,7 @@ interface ContentMiniSearchOptions {
1448
1473
  /**
1449
1474
  * Fields copied into search result payloads.
1450
1475
  *
1451
- * @default ['path', 'title', 'excerpt', 'anchor', 'locale']
1476
+ * @default ['path', 'title', 'excerpt', 'anchor', 'locale', 'collection']
1452
1477
  */
1453
1478
  storeFields: string[];
1454
1479
  /**
@@ -1478,6 +1503,7 @@ interface ContentSearchPublicRuntimeConfig {
1478
1503
  }
1479
1504
  interface ContentSearchIndexRecord {
1480
1505
  id: string;
1506
+ collection: string;
1481
1507
  path: string;
1482
1508
  title: string;
1483
1509
  excerpt: string;
@@ -1488,6 +1514,7 @@ interface ContentSearchIndexRecord {
1488
1514
  [field: string]: unknown;
1489
1515
  }
1490
1516
  interface ContentSearchResult {
1517
+ collection: string;
1491
1518
  path: string;
1492
1519
  title: string;
1493
1520
  excerpt: string;
@@ -1659,6 +1686,36 @@ interface ContentSitemapAssertOptions {
1659
1686
  * @default []
1660
1687
  */
1661
1688
  requiredCollections?: string[];
1689
+ /**
1690
+ * Public URL paths that must appear in generated sitemap loc values.
1691
+ *
1692
+ * Values are compared by URL pathname, so absolute sitemap URLs are
1693
+ * normalized before assertion. Use this for release QA of important route
1694
+ * identities; keep `requiredCollections` for the broader content invariant.
1695
+ *
1696
+ * @default []
1697
+ */
1698
+ requiredPaths?: string[];
1699
+ /**
1700
+ * Public URL path prefixes that must not appear in generated sitemap loc
1701
+ * values.
1702
+ *
1703
+ * Use this to guard against internal/generated routes such as `/_payload`,
1704
+ * `/_nuxt`, API routes, or auth-only sections.
1705
+ *
1706
+ * @default []
1707
+ */
1708
+ forbiddenPathPrefixes?: string[];
1709
+ /**
1710
+ * Fail when generated sitemap URLs use placeholder or local hosts such as
1711
+ * `example.com`, `localhost`, `127.0.0.1`, or `.localhost`.
1712
+ *
1713
+ * Enable this for production-like release checks. It is disabled by default
1714
+ * so local examples and template builds can still generate static output.
1715
+ *
1716
+ * @default false
1717
+ */
1718
+ requireProductionSiteUrl?: boolean;
1662
1719
  /**
1663
1720
  * Optional per-sitemap overrides keyed by sitemap name, for example `en-US`.
1664
1721
  *
@@ -1694,7 +1751,8 @@ interface ContentSearchOptions {
1694
1751
  /**
1695
1752
  * Collections included in the built-in index.
1696
1753
  *
1697
- * When omitted, all configured collections are indexed.
1754
+ * When omitted, Ginko indexes route-backed public collections only. Data-only
1755
+ * collections are excluded unless listed explicitly.
1698
1756
  */
1699
1757
  collections?: string[];
1700
1758
  /**
@@ -1757,8 +1815,9 @@ interface ModuleOptions {
1757
1815
  * Built-in full-text search configuration.
1758
1816
  *
1759
1817
  * When enabled, Ginko exposes JSON/Pagefind search endpoints under
1760
- * the content api base route. Import `useContentSearchResults()` from
1761
- * `@lupinum/ginko-content/client` to consume them from app components.
1818
+ * the content api base route. `useContentSearchData()` and
1819
+ * `useContentSearchResults()` are auto-imported; import the headless
1820
+ * `useContentSearch()` helper from `@lupinum/ginko-content/client`.
1762
1821
  */
1763
1822
  search: false | ContentSearchOptions;
1764
1823
  /**
@@ -2041,5 +2100,5 @@ declare module 'nitropack' {
2041
2100
  }
2042
2101
  }
2043
2102
 
2044
- export { _default as default };
2045
- export type { BacklinkFields, BacklinkSource, BacklinksOptions, BacklinksResult, CollectionQueryBuilder, CollectionQueryOperator, CollectionQueryValue, CollectionSchema, ContentCacheArtifact, ContentCmsCollectionConfig, ContentCmsFieldConfig, ContentCmsFieldType, ContentCmsRelationConfig, ContentCollectionConfig, ContentCollectionHandle, ContentCollectionI18nConfig, ContentCollectionI18nMap, ContentCollectionItem, ContentCollectionItemSurroundingsOptions, ContentCollectionKind, ContentCollectionMap, ContentCollectionName, ContentCollectionNavigationOptions, ContentCollectionPageOptions, ContentCollectionRouteConfig, ContentCollectionRouteMetaOptions, ContentCollectionSearchSectionsOptions, ContentCollectionSource, ContentCollectionSourceObject, ContentConfig, ContentContext, ContentI18nOptions, ContentLocaleEntry, ContentLocaleRoute, ContentManifest, ContentMiniSearchOptions, ContentNavigationItem, ContentPageResult, ContentPreviewOptions, ContentProviderName, ContentProviderSearchRequest, ContentQueryBuilder, ContentQueryBuilderParams, ContentQueryBuilderWhere, ContentQueryFetcher, ContentQueryRequest, ContentQuerySortFields, ContentQuerySortOptions, ContentQuerySortParams, ContentReferenceSchema, ContentResolvedMeta, ContentRevalidateOptions, ContentRouteMeta, ContentSearchEngine, ContentSearchIndexRecord, ContentSearchOptions, ContentSearchPublicRuntimeConfig, ContentSearchResult, ContentSearchSection, ContentSelector, ContentSeoImage, ContentSeoMeta, ContentSitemapAlternative, ContentSitemapAssertOptions, ContentSitemapAssertSitemapOptions, ContentSitemapEntry, ContentSitemapImage, ContentSitemapOptions, ContentTransformer, ContentTreeItem, ContentVariant, DefineCollectionObject, DefineCollectionOptions, DocumentFromHandle, LocaleFallback, LocalePathEntry, LocalizedDoc, ManifestVariant, ManyOptions, MarkdownNode, MarkdownOptions, MarkdownParsedContent, MarkdownPluginDescriptor, MarkdownPluginOptions, MarkdownRoot, ModuleHooks, ModuleOptions, MountOptions, NavItem, NeighborsOptions, NeighborsResult, OneOptions, PaginationOptions, PaginationResult, ParseContentOptions, ParsedContent, ParsedContentInternalMeta, ParsedContentMeta, PopulateFromOptions, PopulateSpec, PopulatedDocument, QueryGroupBuilder, QueryGroupFunction, QueryMatchOperator, QueryOperators, QueryOrderDirection, QueryOrderOptions, QueryWhere, ResolutionEnvelope, ResolveContentReferenceOptions, ResolveOneOptions, ResolveOneResult, ResolvedContentI18nOptions, ResolvedMarkdownPlugin, ResolvedVariant, SortDirection, SortSpec, StrictParsedContent, StrictParsedContentMeta, Toc, TocLink, TransformContentOptions, TransformContentSource, TreeOptions, VariantsOptions };
2103
+ export { _default as default, normalizeContentConfigCollectionNames };
2104
+ export type { BacklinkFields, BacklinkSource, BacklinksOptions, BacklinksResult, CollectionQueryBuilder, CollectionQueryOperator, CollectionQueryValue, CollectionSchema, ContentCacheArtifact, ContentCmsCollectionConfig, ContentCmsFieldConfig, ContentCmsFieldType, ContentCmsRelationConfig, ContentCollectionConfig, ContentCollectionHandle, ContentCollectionI18nConfig, ContentCollectionI18nMap, ContentCollectionItem, ContentCollectionItemSurroundingsOptions, ContentCollectionKind, ContentCollectionMap, ContentCollectionName, ContentCollectionNavigationOptions, ContentCollectionPageOptions, ContentCollectionRouteConfig, ContentCollectionRouteMetaOptions, ContentCollectionSearchSectionsOptions, ContentCollectionSource, ContentCollectionSourceObject, ContentCollectionStringName, ContentCollectionTarget, ContentConfig, ContentContext, ContentI18nOptions, ContentLocaleEntry, ContentLocaleRoute, ContentManifest, ContentMiniSearchOptions, ContentNavigationItem, ContentPageResult, ContentPreviewOptions, ContentProviderName, ContentProviderSearchRequest, ContentQueryBuilder, ContentQueryBuilderParams, ContentQueryBuilderWhere, ContentQueryFetcher, ContentQueryRequest, ContentQuerySortFields, ContentQuerySortOptions, ContentQuerySortParams, ContentReferenceSchema, ContentResolvedMeta, ContentRevalidateOptions, ContentRouteMeta, ContentSearchEngine, ContentSearchIndexRecord, ContentSearchOptions, ContentSearchPublicRuntimeConfig, ContentSearchResult, ContentSearchSection, ContentSelector, ContentSeoImage, ContentSeoMeta, ContentSitemapAlternative, ContentSitemapAssertOptions, ContentSitemapAssertSitemapOptions, ContentSitemapEntry, ContentSitemapImage, ContentSitemapOptions, ContentTransformer, ContentTreeItem, ContentVariant, DefineCollectionObject, DefineCollectionOptions, DocumentFromHandle, LocaleFallback, LocalePathEntry, LocalizedDoc, ManifestVariant, ManyOptions, MarkdownNode, MarkdownOptions, MarkdownParsedContent, MarkdownPluginDescriptor, MarkdownPluginOptions, MarkdownRoot, ModuleHooks, ModuleOptions, MountOptions, NavItem, NeighborsOptions, NeighborsResult, OneOptions, PaginationOptions, PaginationResult, ParseContentOptions, ParsedContent, ParsedContentInternalMeta, ParsedContentMeta, PopulateFromOptions, PopulateSpec, PopulatedDocument, QueryGroupBuilder, QueryGroupFunction, QueryMatchOperator, QueryOperators, QueryOrderDirection, QueryOrderOptions, QueryWhere, ResolutionEnvelope, ResolveContentReferenceOptions, ResolveOneOptions, ResolveOneResult, ResolvedContentI18nOptions, ResolvedMarkdownPlugin, ResolvedVariant, SortDirection, SortSpec, StrictParsedContent, StrictParsedContentMeta, Toc, TocLink, TransformContentOptions, TransformContentSource, TreeOptions, VariantsOptions };
package/dist/module.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lupinum/ginko-content",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "configKey": "content",
5
5
  "compatibility": {
6
6
  "nuxt": ">=4.0.0"