@lupinum/ginko-content 0.1.0 → 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 (42) hide show
  1. package/README.md +93 -47
  2. package/compatibility.json +5 -5
  3. package/dist/cli.mjs +184 -29
  4. package/dist/core/references/schema.d.ts +1 -0
  5. package/dist/core/references/schema.js +33 -10
  6. package/dist/integrations/nitro/context.d.ts +4 -8
  7. package/dist/module.d.mts +90 -30
  8. package/dist/module.json +1 -1
  9. package/dist/module.mjs +241 -41
  10. package/dist/public/client.d.ts +4 -2
  11. package/dist/public/client.js +2 -0
  12. package/dist/runtime/app/components/ContentRenderer.vue +9 -5
  13. package/dist/runtime/app/components/internal/ContentRendererMarkdown.d.vue.ts +1 -1
  14. package/dist/runtime/app/components/internal/ContentRendererMarkdown.vue.d.ts +1 -1
  15. package/dist/runtime/app/composables/head.d.ts +27 -3
  16. package/dist/runtime/app/composables/head.js +82 -78
  17. package/dist/runtime/app/composables/query-api.d.ts +9 -10
  18. package/dist/runtime/app/composables/search.d.ts +4 -4
  19. package/dist/runtime/app/composables/search.js +11 -5
  20. package/dist/runtime/app/composables/use-content.d.ts +35 -15
  21. package/dist/runtime/app/composables/use-content.js +83 -13
  22. package/dist/runtime/query/unified.d.ts +1 -0
  23. package/dist/runtime/query/unified.js +93 -6
  24. package/dist/runtime/server/api/search.d.ts +2 -1
  25. package/dist/runtime/server/api/search.js +6 -2
  26. package/dist/runtime/server/pagefind.js +1 -0
  27. package/dist/runtime/server/plugins/sitemap.js +4 -4
  28. package/dist/runtime/server/search.d.ts +11 -0
  29. package/dist/runtime/server/search.js +24 -7
  30. package/dist/runtime/shared/search.js +3 -2
  31. package/dist/runtime/utils/sitemap-source +1 -0
  32. package/dist/runtime/utils/sitemap-source.d.ts +2 -0
  33. package/dist/runtime/utils/sitemap-source.js +6 -0
  34. package/dist/testing/provider-fixture.js +1 -0
  35. package/dist/types/config.d.ts +42 -20
  36. package/dist/types/config.js +26 -2
  37. package/dist/types/module.d.ts +39 -6
  38. package/dist/types/query.d.ts +20 -11
  39. package/dist/types/search.d.ts +3 -1
  40. package/dist/types.d.mts +2 -2
  41. package/dist/web-types.json +1 -1
  42. package/package.json +4 -4
@@ -1,8 +1,8 @@
1
1
  import { compileQueryParams } from "../../core/query/filter";
2
2
  import { decorateLocalePathsWithFallbacks, localizePageResult } from "../../features/localization/results";
3
- import { normalizeRouteMounts } from "../../features/localization/path";
3
+ import { normalizeContentPath, normalizeRouteMounts } from "../../features/localization/path";
4
4
  import { normalizeReferenceValue } from "../../core/references/resolve";
5
- import { collectTopLevelReferenceFields } from "../../core/references/schema";
5
+ import { collectTopLevelReferenceFields, collectTopLevelReferenceFieldsByTarget } from "../../core/references/schema";
6
6
  import { MAX_PUBLIC_QUERY_LIMIT, MAX_PUBLIC_QUERY_SKIP } from "./public-limits.js";
7
7
  const NAVIGATION_INTERNAL_FIELDS = [
8
8
  "_id",
@@ -95,6 +95,23 @@ const decorate = (doc, collection, runtime, requestedLocale) => {
95
95
  result.localePaths = decorateLocalePathsWithFallbacks(result.localePaths, locales, fallbackLocale, defaultLocale, routeMounts);
96
96
  return result;
97
97
  };
98
+ const collectionRouteRoots = (collection, runtime) => {
99
+ const collectionConfig = runtime?.collections?.[collection];
100
+ const collectionI18n = collectionConfig?.i18n;
101
+ const collectionLocales = collectionI18n && typeof collectionI18n === "object" ? collectionI18n.locales : void 0;
102
+ const collectionDefault = collectionI18n && typeof collectionI18n === "object" ? collectionI18n.defaultLocale : void 0;
103
+ const locales = collectionLocales?.length ? collectionLocales : runtime?.locales?.length ? runtime.locales : [];
104
+ const defaultLocale = collectionDefault || runtime?.defaultLocale;
105
+ const routeMounts = normalizeRouteMounts(collectionConfig?.route, locales, defaultLocale);
106
+ return new Set(Object.values(routeMounts || {}).map((value) => normalizeContentPath(value)));
107
+ };
108
+ const isCollectionRouteRoot = (path, collection, runtime) => collectionRouteRoots(collection, runtime).has(normalizeContentPath(path));
109
+ const isAncestorRoutePath = (path, childPath) => {
110
+ const normalized = normalizeContentPath(path);
111
+ const child = normalizeContentPath(childPath);
112
+ return normalized === "/" ? child !== "/" : child.startsWith(`${normalized}/`);
113
+ };
114
+ const isNavigationRootPath = (path, flat) => Boolean(flat[0]?.path && isAncestorRoutePath(path, flat[0].path));
98
115
  const collectionDefaultLocale = (collection, runtime) => {
99
116
  const collectionI18n = runtime?.collections?.[collection]?.i18n;
100
117
  const collectionDefault = collectionI18n && typeof collectionI18n === "object" ? collectionI18n.defaultLocale : void 0;
@@ -133,7 +150,7 @@ const explainResolution = (collection, requestedBy, normalizedBy, requestedLocal
133
150
  const ensureCollectionName = (handle) => {
134
151
  if (typeof handle === "string") return handle;
135
152
  if (!handle || typeof handle !== "object" || typeof handle.name !== "string") {
136
- throw new TypeError("query API: expected a collection handle from defineCollection(name, config) or a string collection name");
153
+ throw new TypeError("query API: expected a string collection name or a collection handle from defineContentConfig({ collections }). Use useContentPage('docs') or config.collections.docs.");
137
154
  }
138
155
  return handle.name;
139
156
  };
@@ -150,6 +167,55 @@ const populateReferenceValue = async (context, target, value, locale, fallback)
150
167
  ...fallback !== void 0 ? { fallback } : {}
151
168
  });
152
169
  };
170
+ const wildcardReferenceTarget = "*";
171
+ const collectReferenceFieldsByTarget = (source, sourceCollection, runtime) => {
172
+ if (typeof source !== "string") {
173
+ return collectTopLevelReferenceFieldsByTarget(source.schema);
174
+ }
175
+ return runtime?.collections?.[sourceCollection]?.references || {};
176
+ };
177
+ const invertReferenceFields = (references) => {
178
+ const fields = /* @__PURE__ */ new Map();
179
+ for (const [target, targetFields] of Object.entries(references)) {
180
+ for (const field of targetFields) {
181
+ const targets = fields.get(field) || [];
182
+ targets.push(target);
183
+ fields.set(field, targets);
184
+ }
185
+ }
186
+ return fields;
187
+ };
188
+ const createPopulateTargetMismatchError = (sourceCollection, field, expectedTargets, actualTarget) => new Error([
189
+ `Cannot populate "${sourceCollection}.${field}" from "${actualTarget}".`,
190
+ `Reference metadata declares "${sourceCollection}.${field}" points to ${expectedTargets.map((target) => `"${target}"`).join(" or ")}.`,
191
+ `Change populate.${field} to the declared target collection, or update ${sourceCollection}.schema relation metadata.`
192
+ ].join(" "));
193
+ const validatePopulateSpec = (source, sourceCollection, runtime, populate) => {
194
+ if (!populate || !isRecord(populate)) {
195
+ return;
196
+ }
197
+ const references = collectReferenceFieldsByTarget(source, sourceCollection, runtime);
198
+ const fieldTargets = invertReferenceFields(references);
199
+ if (!fieldTargets.size) {
200
+ return;
201
+ }
202
+ for (const [field, target] of Object.entries(populate)) {
203
+ const declaredTargets = fieldTargets.get(field);
204
+ if (!declaredTargets?.length) {
205
+ continue;
206
+ }
207
+ const actualTarget = ensureCollectionName(target);
208
+ if (declaredTargets.includes(actualTarget) || declaredTargets.includes(wildcardReferenceTarget)) {
209
+ continue;
210
+ }
211
+ throw createPopulateTargetMismatchError(
212
+ sourceCollection,
213
+ field,
214
+ declaredTargets.filter((target2) => target2 !== wildcardReferenceTarget),
215
+ actualTarget
216
+ );
217
+ }
218
+ };
153
219
  const populateDocument = async (context, doc, populate, locale, fallback) => {
154
220
  if (!populate || !isRecord(populate)) {
155
221
  return doc;
@@ -179,6 +245,7 @@ const selectWithPopulate = (select, populate) => {
179
245
  export async function resolveOne(context, handle, options) {
180
246
  const collection = ensureCollectionName(handle);
181
247
  const runtime = context.runtime;
248
+ validatePopulateSpec(handle, collection, runtime, options.populate);
182
249
  const by = options.by;
183
250
  const fallback = resolveFallback(options.fallback, collection, runtime);
184
251
  const params = compileQueryParams({
@@ -216,6 +283,7 @@ export async function one(context, handle, options) {
216
283
  export async function many(context, handle, options = {}) {
217
284
  const collection = ensureCollectionName(handle);
218
285
  const runtime = context.runtime;
286
+ validatePopulateSpec(handle, collection, runtime, options.populate);
219
287
  const fallback = resolveFallback(options.fallback, collection, runtime);
220
288
  const params = compileQueryParams({
221
289
  collection,
@@ -247,6 +315,7 @@ const normalizePositiveInteger = (value, fallback) => {
247
315
  export async function paginate(context, handle, options) {
248
316
  const collection = ensureCollectionName(handle);
249
317
  const runtime = context.runtime;
318
+ validatePopulateSpec(handle, collection, runtime, options.populate);
250
319
  const requestedPage = normalizePositiveInteger(options.page, 1);
251
320
  const limit = Math.min(normalizePositiveInteger(options.limit, 10), MAX_PUBLIC_QUERY_LIMIT);
252
321
  const skip = Math.min((requestedPage - 1) * limit, MAX_PUBLIC_QUERY_SKIP);
@@ -323,9 +392,13 @@ const resolveExplicitBacklinkFields = (fields, sourceName) => {
323
392
  const sourceFields = fields?.[sourceName];
324
393
  return Array.isArray(sourceFields) ? sourceFields.filter((field) => typeof field === "string" && field.length > 0) : [];
325
394
  };
326
- const inferBacklinkFields = (source, targetCollection) => {
395
+ const inferBacklinkFields = (source, targetCollection, runtime) => {
327
396
  if (typeof source === "string") {
328
- return [];
397
+ const references = runtime?.collections?.[source]?.references;
398
+ return [
399
+ ...references?.[targetCollection] || [],
400
+ ...references?.["*"] || []
401
+ ];
329
402
  }
330
403
  return collectTopLevelReferenceFields(source.schema, targetCollection);
331
404
  };
@@ -346,6 +419,9 @@ const backlinkWhere = (fields, candidates) => {
346
419
  }));
347
420
  return clauses.length ? { $or: clauses } : void 0;
348
421
  };
422
+ const createMissingBacklinkFieldsError = (sourceCollection, targetCollection) => new Error(
423
+ `Cannot infer backlink fields from "${sourceCollection}" to "${targetCollection}". Declare fields.relation('${targetCollection}') / fields.relations('${targetCollection}') in ${sourceCollection}.schema, or pass fields explicitly.`
424
+ );
349
425
  export async function backlinks(context, targetHandle, options) {
350
426
  const targetCollection = ensureCollectionName(targetHandle);
351
427
  const target = await one(context, targetHandle, {
@@ -366,9 +442,12 @@ export async function backlinks(context, targetHandle, options) {
366
442
  const fields = [
367
443
  .../* @__PURE__ */ new Set([
368
444
  ...resolveExplicitBacklinkFields(options.fields, sourceName),
369
- ...inferBacklinkFields(source, targetCollection)
445
+ ...inferBacklinkFields(source, targetCollection, context.runtime)
370
446
  ])
371
447
  ];
448
+ if (!fields.length) {
449
+ throw createMissingBacklinkFieldsError(sourceName, targetCollection);
450
+ }
372
451
  const where = backlinkWhere(fields, candidates);
373
452
  if (!where) {
374
453
  return [];
@@ -441,6 +520,8 @@ export async function tree(context, handle, options = {}) {
441
520
  return list;
442
521
  }
443
522
  export async function neighbors(context, handle, options) {
523
+ const collection = ensureCollectionName(handle);
524
+ const runtime = context.runtime;
444
525
  const seed = await one(context, handle, {
445
526
  by: options.by,
446
527
  locale: options.locale,
@@ -465,6 +546,12 @@ export async function neighbors(context, handle, options) {
465
546
  walk(fullTree);
466
547
  const targetPath = seed.path;
467
548
  const idx = flat.findIndex((entry) => entry.path === targetPath);
549
+ if (idx === -1 && (isCollectionRouteRoot(targetPath, collection, runtime) || isNavigationRootPath(targetPath, flat))) {
550
+ return {
551
+ prev: null,
552
+ next: flat[0]?.item ?? null
553
+ };
554
+ }
468
555
  if (idx === -1) return { prev: null, next: null };
469
556
  return {
470
557
  prev: flat[idx - 1]?.item ?? null,
@@ -1,2 +1,3 @@
1
- declare const _default: import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<import("..").MaybeContentProviderResult<import("../../../types").ContentSearchResult[]>>>;
1
+ import type { ContentSearchResult } from '../../../types/search';
2
+ declare const _default: import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<ContentSearchResult[]>>;
2
3
  export default _default;
@@ -3,6 +3,10 @@ import { useRuntimeConfig } from "nitropack/runtime";
3
3
  import { buildSearchIndex, searchRecords } from "../search.js";
4
4
  import { getContentProvider } from "../providers/index.js";
5
5
  import { createContentProviderError } from "../../../public/provider-errors";
6
+ const normalizeSearchResults = (results = []) => results.map((result) => ({
7
+ ...result,
8
+ collection: typeof result.collection === "string" ? result.collection : ""
9
+ }));
6
10
  export default defineEventHandler(async (event) => {
7
11
  const runtimeConfig = useRuntimeConfig(event);
8
12
  const searchConfig = runtimeConfig.content.search;
@@ -22,11 +26,11 @@ export default defineEventHandler(async (event) => {
22
26
  provider: provider.name
23
27
  });
24
28
  }
25
- return await provider.search(event, {
29
+ return normalizeSearchResults(await provider.search(event, {
26
30
  term,
27
31
  locale,
28
32
  collections: searchConfig.collections
29
- });
33
+ }));
30
34
  }
31
35
  const records = await buildSearchIndex(event, {
32
36
  collections: searchConfig.collections,
@@ -14,6 +14,7 @@ export async function writePagefindIndex(records, outputPath) {
14
14
  meta: {
15
15
  title: record.title,
16
16
  excerpt: record.excerpt,
17
+ collection: record.collection,
17
18
  locale: record.locale || "",
18
19
  anchor: record.anchor || "",
19
20
  path: record.path
@@ -1,7 +1,7 @@
1
1
  import { defineNitroPlugin } from "nitropack/runtime";
2
2
  import { getContentRuntimeConfig } from "../runtime-config.js";
3
+ import { GINKO_SITEMAP_SOURCE_NAME, resolveContentSitemapSource } from "../../utils/sitemap-source.js";
3
4
  const LEGACY_NUXT_CONTENT_V2_SOURCE = "@nuxt/content@v2:urls";
4
- const GINKO_SOURCE = "@lupinum/ginko-content:urls";
5
5
  export default defineNitroPlugin((nitro) => {
6
6
  nitro.hooks.hook("sitemap:sources", (ctx) => {
7
7
  const runtimeConfig = getContentRuntimeConfig();
@@ -10,13 +10,13 @@ export default defineNitroPlugin((nitro) => {
10
10
  if (!sitemap || !apiBaseURL) {
11
11
  return;
12
12
  }
13
- const fetch = `${apiBaseURL}${sitemap.path || "/sitemap"}`;
13
+ const fetch = resolveContentSitemapSource(apiBaseURL, sitemap.path || "/sitemap");
14
14
  ctx.sources = ctx.sources.filter((source) => {
15
- return source.context?.name !== LEGACY_NUXT_CONTENT_V2_SOURCE && source.context?.name !== GINKO_SOURCE;
15
+ return source.context?.name !== LEGACY_NUXT_CONTENT_V2_SOURCE && source.context?.name !== GINKO_SITEMAP_SOURCE_NAME;
16
16
  });
17
17
  ctx.sources.push({
18
18
  context: {
19
- name: GINKO_SOURCE
19
+ name: GINKO_SITEMAP_SOURCE_NAME
20
20
  },
21
21
  fetch,
22
22
  sourceType: "app"
@@ -4,6 +4,17 @@ import type { ContentQueryBuilderWhere } from '../../types/query';
4
4
  import type { ContentSearchIndexRecord } from '../../types/search';
5
5
  export { searchRecords } from '../shared/search';
6
6
  type SearchablePage = Pick<ParsedContent, '_path' | '_locale' | 'title' | 'description' | 'body'> & Record<string, unknown>;
7
+ type RuntimeSearchConfig = {
8
+ search?: {
9
+ collections?: string[];
10
+ } | false;
11
+ collections?: Record<string, {
12
+ type?: 'page' | 'data';
13
+ route?: unknown;
14
+ sitemap?: boolean;
15
+ } | unknown>;
16
+ };
17
+ export declare const resolveSearchCollections: (runtimeContent: RuntimeSearchConfig, collectionsOverride?: string[]) => string[];
7
18
  export declare const clearSearchRecordsCache: () => void;
8
19
  export declare function serverSearchContent(event: H3Event, filterQuery?: ContentQueryBuilderWhere, locale?: string, collectionsOverride?: string[], opts?: {
9
20
  allLocales?: boolean;
@@ -5,6 +5,21 @@ import { serverQueryCollection } from "./provider-query.js";
5
5
  import { createContentProviderError } from "../../public/provider-errors";
6
6
  export { searchRecords } from "../shared/search.js";
7
7
  const unique = (values) => Array.from(new Set(values.filter(Boolean)));
8
+ const isRecord = (value) => Boolean(value) && typeof value === "object" && !Array.isArray(value);
9
+ const hasRouteMount = (route) => route !== void 0 && route !== null && route !== "";
10
+ const isRouteBackedSearchCollection = (config) => {
11
+ if (!isRecord(config) || config.sitemap === false || config.type === "data") {
12
+ return false;
13
+ }
14
+ return config.type === "page" || hasRouteMount(config.route) || config.sitemap === true;
15
+ };
16
+ export const resolveSearchCollections = (runtimeContent, collectionsOverride) => {
17
+ const configuredCollections = collectionsOverride || (runtimeContent.search && runtimeContent.search !== false ? runtimeContent.search.collections : void 0);
18
+ if (configuredCollections?.length) {
19
+ return unique(configuredCollections);
20
+ }
21
+ return Object.entries(runtimeContent.collections || {}).filter(([, config]) => isRouteBackedSearchCollection(config)).map(([collection]) => collection);
22
+ };
8
23
  const searchRecordsCache = /* @__PURE__ */ new Map();
9
24
  const MAX_SEARCH_RECORDS_CACHE_ENTRIES = 12;
10
25
  export const clearSearchRecordsCache = () => {
@@ -32,9 +47,7 @@ const mergeSearchFilter = (filterQuery, locale) => {
32
47
  };
33
48
  export async function serverSearchContent(event, filterQuery, locale, collectionsOverride, opts = {}) {
34
49
  const runtimeConfig = useRuntimeConfig(event);
35
- const configuredCollections = collectionsOverride || runtimeConfig.content.search?.collections;
36
- const contentCollections = Object.keys(runtimeConfig.content.collections || {});
37
- const collections = (configuredCollections?.length ? configuredCollections : contentCollections).filter(Boolean);
50
+ const collections = resolveSearchCollections(runtimeConfig.content, collectionsOverride);
38
51
  const results = await Promise.all(collections.map(async (collection) => {
39
52
  const loadPages = async (queryLocale) => {
40
53
  const query = serverQueryCollection(event, collection).select("_path", "_locale", "title", "description", "body");
@@ -64,6 +77,7 @@ const toSearchRecord = (section) => {
64
77
  return {
65
78
  ...extraFields,
66
79
  id: section.id,
80
+ collection: section.collection || "",
67
81
  path,
68
82
  title: section.title,
69
83
  excerpt: section.content.slice(0, 240),
@@ -76,12 +90,15 @@ const toSearchRecord = (section) => {
76
90
  const buildProviderSearchSections = async (event, collections, provider, opts) => {
77
91
  const runtimeConfig = useRuntimeConfig(event);
78
92
  const sections = await Promise.all(collections.map(async (collection) => {
79
- const loadCollectionLocale = async (locale) => await provider.searchSections(event, collection, {
93
+ const loadCollectionLocale = async (locale) => (await provider.searchSections(event, collection, {
80
94
  ignoredTags: opts.ignoredTags || [],
81
95
  extraFields: unique(["_locale", ...opts.extraFields || []]),
82
96
  filterQuery: opts.filterQuery,
83
97
  locale
84
- });
98
+ })).map((section) => ({
99
+ ...section,
100
+ collection
101
+ }));
85
102
  if (opts.locale) {
86
103
  return await loadCollectionLocale(opts.locale);
87
104
  }
@@ -100,7 +117,7 @@ export async function buildSearchIndex(event, opts = {}) {
100
117
  const cacheKey = JSON.stringify({
101
118
  provider: provider.name,
102
119
  integrity: runtimeConfig.content.cacheIntegrity || runtimeConfig.public.content?.integrity,
103
- collections: opts.collections || runtimeConfig.content.search?.collections,
120
+ collections: resolveSearchCollections(runtimeConfig.content, opts.collections),
104
121
  ignoredTags: opts.ignoredTags || [],
105
122
  extraFields: opts.extraFields || runtimeConfig.content.search?.extraFields || [],
106
123
  filterQuery: opts.filterQuery,
@@ -111,7 +128,7 @@ export async function buildSearchIndex(event, opts = {}) {
111
128
  if (cached) {
112
129
  return cached;
113
130
  }
114
- const collections = (opts.collections || runtimeConfig.content.search?.collections || Object.keys(runtimeConfig.content.collections || {})).filter(Boolean);
131
+ const collections = resolveSearchCollections(runtimeConfig.content, opts.collections);
115
132
  if (!provider.capabilities.searchSections) {
116
133
  throw createContentProviderError("unsupported_provider_search_index", `${provider.name} does not support search index generation`, {
117
134
  provider: provider.name
@@ -1,7 +1,7 @@
1
1
  import MiniSearch from "minisearch";
2
2
  const DEFAULT_SEARCH_OPTIONS = {
3
3
  fields: ["title", "content", "headings"],
4
- storeFields: ["path", "title", "excerpt", "anchor", "locale"],
4
+ storeFields: ["path", "title", "excerpt", "anchor", "locale", "collection"],
5
5
  boost: {
6
6
  title: 4,
7
7
  headings: 2,
@@ -10,7 +10,7 @@ const DEFAULT_SEARCH_OPTIONS = {
10
10
  fuzzy: 0.2,
11
11
  prefix: true
12
12
  };
13
- const REQUIRED_STORE_FIELDS = ["path", "title", "excerpt"];
13
+ const REQUIRED_STORE_FIELDS = ["path", "title", "excerpt", "collection"];
14
14
  const MAX_SEARCH_INDEX_CACHE_ENTRIES = 12;
15
15
  const searchIndexCache = /* @__PURE__ */ new Map();
16
16
  const resolveSearchOptions = (options = {}) => {
@@ -79,6 +79,7 @@ export const searchRecords = (records, term, locale, searchOptions) => {
79
79
  return {
80
80
  ...storedFields,
81
81
  path: typeof result.path === "string" ? result.path : "",
82
+ collection: typeof result.collection === "string" ? result.collection : "",
82
83
  title: typeof result.title === "string" ? result.title : "",
83
84
  excerpt: typeof result.excerpt === "string" ? result.excerpt : "",
84
85
  score: result.score,
@@ -0,0 +1 @@
1
+ export * from './sitemap-source.js'
@@ -0,0 +1,2 @@
1
+ export declare const GINKO_SITEMAP_SOURCE_NAME = "@lupinum/ginko-content:urls";
2
+ export declare function resolveContentSitemapSource(apiBaseURL: string, sitemapPath?: string): string;
@@ -0,0 +1,6 @@
1
+ export const GINKO_SITEMAP_SOURCE_NAME = "@lupinum/ginko-content:urls";
2
+ export function resolveContentSitemapSource(apiBaseURL, sitemapPath = "/sitemap") {
3
+ const base = apiBaseURL.replace(/\/$/, "");
4
+ const path = sitemapPath.startsWith("/") ? sitemapPath : `/${sitemapPath}`;
5
+ return `${base}${path}`;
6
+ }
@@ -342,6 +342,7 @@ export const createFixtureContentProvider = (fixture, name = fixture.providerNam
342
342
  const term = request.term.toLocaleLowerCase();
343
343
  return fixture.documents.filter((doc) => !request.collections?.length || request.collections.includes(doc._collection || "")).filter((doc) => !request.locale || doc._locale === request.locale).filter((doc) => String(doc.title || "").toLocaleLowerCase().includes(term)).map((doc) => ({
344
344
  score: 1,
345
+ collection: doc._collection || "",
345
346
  title: doc.title || "",
346
347
  excerpt: String(doc.description || ""),
347
348
  path: localizePath(fixture, doc._collection || "", doc._path || "/", doc._locale),
@@ -20,6 +20,7 @@ export type ContentCollectionSource = string | string[];
20
20
  * express roots such as `{ en: '/docs', de: '/dokumentation' }`.
21
21
  */
22
22
  export type ContentCollectionRouteConfig = string | Record<string, string>;
23
+ export type ContentCollectionKind = 'page' | 'data';
23
24
  export 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';
24
25
  export interface ContentCmsRelationConfig {
25
26
  collectionId: string;
@@ -79,6 +80,11 @@ export interface ContentCollectionSourceObject {
79
80
  * Declarative collection definition used by `content.config.ts`.
80
81
  */
81
82
  export interface ContentCollectionConfig<TSchema extends ZodType | undefined = ZodType | undefined> {
83
+ /**
84
+ * Collection kind declared in `defineCollection`. Page collections are public
85
+ * content routes by default; data collections are app-owned records.
86
+ */
87
+ type?: ContentCollectionKind;
82
88
  /**
83
89
  * Source glob or source descriptor understood by the filesystem ingestion
84
90
  * layer. CMS-backed projects do not need a runtime source; filesystem
@@ -131,14 +137,13 @@ export interface ContentCollectionConfig<TSchema extends ZodType | undefined = Z
131
137
  */
132
138
  cms?: ContentCmsCollectionConfig;
133
139
  }
134
- export type ContentCollectionKind = 'page' | 'data';
135
- export type BuiltinContentProviderName = 'filesystem' | 'cms' | 'ginko-cms' | 'ginko';
136
- export type ContentProviderName = BuiltinContentProviderName | (string & {});
140
+ export type ContentProviderName = 'filesystem' | (string & {});
137
141
  export type DefineCollectionOptions<TSchema extends ZodType | undefined = ZodType | undefined> = Omit<ContentCollectionConfig<TSchema>, 'source' | 'exclude'>;
138
142
  export interface DefineCollectionObject<TSchema extends ZodType | undefined = ZodType | undefined> extends DefineCollectionOptions<TSchema> {
139
143
  /**
140
144
  * Nuxt Content v3-compatible collection kind. Ginko does not keep separate
141
- * page/data collection runtimes; `data` collections default to `sitemap: false`.
145
+ * page/data query builders, but the kind remains canonical metadata at runtime.
146
+ * `data` collections default to `sitemap: false`.
142
147
  */
143
148
  type: ContentCollectionKind;
144
149
  /**
@@ -153,14 +158,13 @@ export interface DefineCollectionObject<TSchema extends ZodType | undefined = Zo
153
158
  */
154
159
  export interface ContentConfig<TCollections extends Record<string, ContentCollectionConfig> = Record<string, ContentCollectionConfig>> {
155
160
  /**
156
- * Content backing implementation. `filesystem` is the default. `cms`,
157
- * `ginko-cms`, and `ginko` resolve to the official Ginko CMS provider when
158
- * `@lupinum/ginko-cms` is installed.
161
+ * Content backing implementation. `filesystem` is the default. Provider
162
+ * modules can register named implementations, for example `cms`.
159
163
  */
160
164
  provider?: ContentProviderName;
161
165
  /**
162
- * External provider modules keyed by provider name. Built-in providers do not
163
- * need to be registered here.
166
+ * External provider modules keyed by provider name. First-party provider
167
+ * modules register themselves, so app configs usually do not need this.
164
168
  */
165
169
  providers?: Record<string, string>;
166
170
  /**
@@ -215,6 +219,16 @@ export interface ContentCollectionHandle<Name extends string = string, TSchema e
215
219
  type IsI18nConfig<TConfig> = TConfig extends {
216
220
  i18n: true | ContentCollectionI18nConfig;
217
221
  } ? TConfig['i18n'] extends false ? false : true : false;
222
+ type CollectionNameFromConfigKey<Key extends string, TCollection> = TCollection extends ContentCollectionHandle<infer Name, ZodType | undefined, boolean> ? [Name] extends [never] ? Key : Name : Key;
223
+ type NamedContentCollection<Key extends string, TCollection> = TCollection extends ContentCollectionHandle<infer Name, infer TSchema, infer TI18n> ? Omit<TCollection, 'name' | '__schema' | '__i18n'> & ContentCollectionHandle<[
224
+ Name
225
+ ] extends [never] ? Key : Name, TSchema, TI18n> : TCollection extends ContentCollectionConfig<infer TSchema> ? Omit<TCollection, 'name' | '__schema' | '__i18n'> & ContentCollectionHandle<CollectionNameFromConfigKey<Key, TCollection>, TSchema, IsI18nConfig<TCollection>> : TCollection;
226
+ type NamedContentCollections<TCollections extends Record<string, ContentCollectionConfig>> = {
227
+ [Key in keyof TCollections]: Key extends string ? NamedContentCollection<Key, TCollections[Key]> : TCollections[Key];
228
+ };
229
+ type NamedContentConfig<TCollections extends Record<string, ContentCollectionConfig>> = Omit<ContentConfig<TCollections>, 'collections'> & {
230
+ collections: NamedContentCollections<TCollections>;
231
+ };
218
232
  /**
219
233
  * Define a content collection in `content.config.ts`.
220
234
  *
@@ -223,7 +237,7 @@ type IsI18nConfig<TConfig> = TConfig extends {
223
237
  * import { z } from 'zod'
224
238
  * import { defineCollection } from '@lupinum/ginko-content/config'
225
239
  *
226
- * export const docs = defineCollection('docs', {
240
+ * export const docs = defineCollection({
227
241
  * type: 'page',
228
242
  * source: 'docs/**\/*.md',
229
243
  * i18n: { locales: ['en', 'fr', 'de'], defaultLocale: 'en' },
@@ -237,7 +251,8 @@ type IsI18nConfig<TConfig> = TConfig extends {
237
251
  type SchemaOf<TConfig> = TConfig extends {
238
252
  schema?: infer S;
239
253
  } ? S extends ZodType ? S : undefined : undefined;
240
- export declare function defineCollection<const Name extends string, const TConfig extends DefineCollectionObject<ZodType | undefined>>(name: Name, config: TConfig): ContentCollectionHandle<Name, SchemaOf<TConfig>, IsI18nConfig<TConfig>>;
254
+ export declare function defineCollection<const TConfig extends DefineCollectionObject<ZodType | undefined>>(config: TConfig): ContentCollectionHandle<never, SchemaOf<TConfig>, IsI18nConfig<TConfig>>;
255
+ export declare function normalizeContentConfigCollectionNames<TCollections extends Record<string, ContentCollectionConfig>>(collections: TCollections): NamedContentCollections<TCollections>;
241
256
  /**
242
257
  * Wrap the root content configuration with full type inference.
243
258
  *
@@ -245,17 +260,20 @@ export declare function defineCollection<const Name extends string, const TConfi
245
260
  * ```ts
246
261
  * import { defineCollection, defineContentConfig } from '@lupinum/ginko-content/config'
247
262
  *
263
+ * export const docs = defineCollection({
264
+ * type: 'page',
265
+ * source: 'docs/*.md'
266
+ * })
267
+ *
248
268
  * export default defineContentConfig({
249
- * collections: {
250
- * docs: defineCollection({
251
- * type: 'page',
252
- * source: 'docs/*.md'
253
- * })
254
- * }
269
+ * collections: { docs }
255
270
  * })
256
271
  * ```
257
272
  */
258
- export declare function defineContentConfig<TCollections extends Record<string, ContentCollectionConfig>>(config: ContentConfig<TCollections>): ContentConfig<TCollections>;
273
+ export declare function defineContentConfig<const TCollections extends Record<string, ContentCollectionConfig>>(config: Omit<ContentConfig<TCollections>, 'collections'> & {
274
+ collections: TCollections;
275
+ }): NamedContentConfig<TCollections>;
276
+ export declare function defineContentConfig<const TConfig extends ContentConfig<Record<string, ContentCollectionConfig>>>(config: TConfig): TConfig;
259
277
  /**
260
278
  * Declare a schema field that stores a reference to another content entry.
261
279
  *
@@ -265,9 +283,9 @@ export declare function defineContentConfig<TCollections extends Record<string,
265
283
  * @example
266
284
  * ```ts
267
285
  * import { z } from 'zod'
268
- * import { defineCollection, reference } from '@lupinum/ginko-content/config'
286
+ * import { defineCollection, defineContentConfig, reference } from '@lupinum/ginko-content/config'
269
287
  *
270
- * export default defineCollection({
288
+ * export const blog = defineCollection({
271
289
  * type: 'page',
272
290
  * source: 'blog/*.md',
273
291
  * schema: z.object({
@@ -275,6 +293,10 @@ export declare function defineContentConfig<TCollections extends Record<string,
275
293
  * related: z.array(reference('blog')).default([])
276
294
  * })
277
295
  * })
296
+ *
297
+ * export default defineContentConfig({
298
+ * collections: { blog }
299
+ * })
278
300
  * ```
279
301
  */
280
302
  export declare function reference(collection?: string): ContentReferenceSchema;
@@ -1,10 +1,13 @@
1
1
  import { z } from "zod";
2
2
  import { CONTENT_REFERENCE_PREFIX } from "./reference.js";
3
- export function defineCollection(name, config) {
3
+ export function defineCollection(config) {
4
+ if (typeof config === "string" || arguments.length > 1) {
5
+ throw new TypeError("@lupinum/ginko-content defineCollection(name, config) was removed. Use defineCollection({ ... }) under the desired defineContentConfig({ collections: { docs: ... } }) map key.");
6
+ }
4
7
  const { type, source, sitemap, ...rest } = config;
5
8
  const normalized = normalizeCollectionSource(source);
6
9
  return {
7
- name,
10
+ type,
8
11
  ...normalized,
9
12
  sitemap: sitemap ?? (type === "data" ? false : void 0),
10
13
  ...rest
@@ -20,7 +23,28 @@ function normalizeCollectionSource(source) {
20
23
  }
21
24
  return { source };
22
25
  }
26
+ export function normalizeContentConfigCollectionNames(collections) {
27
+ for (const [key, collection] of Object.entries(collections)) {
28
+ const authoredName = collection.name;
29
+ if (typeof authoredName === "string") {
30
+ if (authoredName !== key) {
31
+ throw new Error(`@lupinum/ginko-content collection key "${key}" must match collection name "${authoredName}". Use defineCollection({ ... }) under collections: { ${key}: ... }, or rename the collections map key.`);
32
+ }
33
+ continue;
34
+ }
35
+ Object.defineProperty(collection, "name", {
36
+ value: key,
37
+ enumerable: true,
38
+ configurable: true,
39
+ writable: true
40
+ });
41
+ }
42
+ return collections;
43
+ }
23
44
  export function defineContentConfig(config) {
45
+ if (config.collections) {
46
+ normalizeContentConfigCollectionNames(config.collections);
47
+ }
24
48
  return config;
25
49
  }
26
50
  export function reference(collection) {