@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.
- package/README.md +93 -47
- package/compatibility.json +5 -5
- package/dist/cli.mjs +184 -29
- package/dist/core/references/schema.d.ts +1 -0
- package/dist/core/references/schema.js +33 -10
- package/dist/integrations/nitro/context.d.ts +4 -8
- package/dist/module.d.mts +90 -30
- package/dist/module.json +1 -1
- package/dist/module.mjs +241 -41
- package/dist/public/client.d.ts +4 -2
- package/dist/public/client.js +2 -0
- package/dist/runtime/app/components/ContentRenderer.vue +9 -5
- package/dist/runtime/app/components/internal/ContentRendererMarkdown.d.vue.ts +1 -1
- package/dist/runtime/app/components/internal/ContentRendererMarkdown.vue.d.ts +1 -1
- package/dist/runtime/app/composables/head.d.ts +27 -3
- package/dist/runtime/app/composables/head.js +82 -78
- package/dist/runtime/app/composables/query-api.d.ts +9 -10
- package/dist/runtime/app/composables/search.d.ts +4 -4
- package/dist/runtime/app/composables/search.js +11 -5
- package/dist/runtime/app/composables/use-content.d.ts +35 -15
- package/dist/runtime/app/composables/use-content.js +83 -13
- package/dist/runtime/query/unified.d.ts +1 -0
- package/dist/runtime/query/unified.js +93 -6
- package/dist/runtime/server/api/search.d.ts +2 -1
- package/dist/runtime/server/api/search.js +6 -2
- package/dist/runtime/server/pagefind.js +1 -0
- package/dist/runtime/server/plugins/sitemap.js +4 -4
- package/dist/runtime/server/search.d.ts +11 -0
- package/dist/runtime/server/search.js +24 -7
- package/dist/runtime/shared/search.js +3 -2
- package/dist/runtime/utils/sitemap-source +1 -0
- package/dist/runtime/utils/sitemap-source.d.ts +2 -0
- package/dist/runtime/utils/sitemap-source.js +6 -0
- package/dist/testing/provider-fixture.js +1 -0
- package/dist/types/config.d.ts +42 -20
- package/dist/types/config.js +26 -2
- package/dist/types/module.d.ts +39 -6
- package/dist/types/query.d.ts +20 -11
- package/dist/types/search.d.ts +3 -1
- package/dist/types.d.mts +2 -2
- package/dist/web-types.json +1 -1
- 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
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
@@ -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 =
|
|
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 !==
|
|
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:
|
|
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
|
|
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:
|
|
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 = (
|
|
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,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),
|
package/dist/types/config.d.ts
CHANGED
|
@@ -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
|
|
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
|
|
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.
|
|
157
|
-
*
|
|
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.
|
|
163
|
-
*
|
|
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(
|
|
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
|
|
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>
|
|
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
|
|
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;
|
package/dist/types/config.js
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { CONTENT_REFERENCE_PREFIX } from "./reference.js";
|
|
3
|
-
export function defineCollection(
|
|
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
|
-
|
|
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) {
|