@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
package/dist/module.mjs
CHANGED
|
@@ -6,6 +6,8 @@ import { defu } from 'defu';
|
|
|
6
6
|
import { hash } from 'ohash';
|
|
7
7
|
import { resolve, join, relative, isAbsolute } from 'pathe';
|
|
8
8
|
import { withTrailingSlash } from 'ufo';
|
|
9
|
+
import { normalizeContentConfigCollectionNames } from '../dist/types/config';
|
|
10
|
+
export { defineCollection, defineContentConfig, reference } from '../dist/types/config';
|
|
9
11
|
import 'unstorage/drivers/fs';
|
|
10
12
|
import 'unstorage/drivers/http';
|
|
11
13
|
import 'unstorage/drivers/github';
|
|
@@ -18,6 +20,7 @@ import { genSafeVariableName, genImport } from 'knitwork';
|
|
|
18
20
|
import { listen } from 'listhen';
|
|
19
21
|
import { makeIgnored } from '../dist/core/content/ignore';
|
|
20
22
|
import { readFile, readdir } from 'node:fs/promises';
|
|
23
|
+
import { resolveContentSitemapSource, GINKO_SITEMAP_SOURCE_NAME } from '../dist/runtime/utils/sitemap-source';
|
|
21
24
|
import { resolveCollectionI18nConfig } from '../dist/features/localization/config';
|
|
22
25
|
import { globby } from 'globby';
|
|
23
26
|
import { transformContent } from '../dist/parsers/index';
|
|
@@ -26,10 +29,11 @@ import { buildContentGraph } from '../dist/core/content/graph';
|
|
|
26
29
|
import { normalizeCollectionSources, normalizeCollectionExcludes } from '../dist/core/content/sources';
|
|
27
30
|
import { resolveCollectionI18n, prefixPathWithLocale } from '../dist/features/localization/path';
|
|
28
31
|
import { resolveCollection } from '../dist/core/content/collection';
|
|
29
|
-
|
|
32
|
+
import { normalizeRouteMounts, normalizeContentPath } from '../dist/core/content/path';
|
|
33
|
+
import { collectTopLevelReferenceFieldsByTarget } from '../dist/core/references/schema';
|
|
30
34
|
|
|
31
35
|
const name = "@lupinum/ginko-content";
|
|
32
|
-
const version = "0.1.
|
|
36
|
+
const version = "0.1.2";
|
|
33
37
|
|
|
34
38
|
const logger = consola.withTag("@lupinum/ginko-content");
|
|
35
39
|
const CACHE_VERSION = 3;
|
|
@@ -240,16 +244,8 @@ const registerContentDevRuntime = (nuxt, options, contentContext) => {
|
|
|
240
244
|
|
|
241
245
|
const registerRuntimeImports = (resolveRuntimeModule) => {
|
|
242
246
|
addImports([
|
|
243
|
-
// Unified query API (ADR-0016) — app wrappers bind the Nuxt content context explicitly.
|
|
244
|
-
{ name: "one", as: "one", from: resolveRuntimeModule("./app/composables/query-api.js") },
|
|
245
|
-
{ name: "many", as: "many", from: resolveRuntimeModule("./app/composables/query-api.js") },
|
|
246
|
-
{ name: "paginate", as: "paginate", from: resolveRuntimeModule("./app/composables/query-api.js") },
|
|
247
|
-
{ name: "backlinks", as: "backlinks", from: resolveRuntimeModule("./app/composables/query-api.js") },
|
|
248
|
-
{ name: "resolveOne", as: "resolveOne", from: resolveRuntimeModule("./app/composables/query-api.js") },
|
|
249
|
-
{ name: "variants", as: "variants", from: resolveRuntimeModule("./app/composables/query-api.js") },
|
|
250
|
-
{ name: "tree", as: "tree", from: resolveRuntimeModule("./app/composables/query-api.js") },
|
|
251
|
-
{ name: "neighbors", as: "neighbors", from: resolveRuntimeModule("./app/composables/query-api.js") },
|
|
252
247
|
{ name: "getCollectionPath", as: "getCollectionPath", from: resolveRuntimeModule("./query/routes.js") },
|
|
248
|
+
{ name: "useContentHead", as: "useContentHead", from: resolveRuntimeModule("./app/composables/head.js") },
|
|
253
249
|
// Vue composables — reactive wrappers over the same options shape.
|
|
254
250
|
{ name: "useContentPage", as: "useContentPage", from: resolveRuntimeModule("./app/composables/use-content.js") },
|
|
255
251
|
{ name: "useContentOne", as: "useContentOne", from: resolveRuntimeModule("./app/composables/use-content.js") },
|
|
@@ -259,9 +255,12 @@ const registerRuntimeImports = (resolveRuntimeModule) => {
|
|
|
259
255
|
{ name: "useContentResolveOne", as: "useContentResolveOne", from: resolveRuntimeModule("./app/composables/use-content.js") },
|
|
260
256
|
{ name: "useContentVariants", as: "useContentVariants", from: resolveRuntimeModule("./app/composables/use-content.js") },
|
|
261
257
|
{ name: "useContentTree", as: "useContentTree", from: resolveRuntimeModule("./app/composables/use-content.js") },
|
|
258
|
+
{ name: "useContentNavigation", as: "useContentNavigation", from: resolveRuntimeModule("./app/composables/use-content.js") },
|
|
262
259
|
{ name: "useContentNeighbors", as: "useContentNeighbors", from: resolveRuntimeModule("./app/composables/use-content.js") },
|
|
263
260
|
{ name: "useContentLocaleSwitch", as: "useContentLocaleSwitch", from: resolveRuntimeModule("./app/composables/use-content.js") },
|
|
264
|
-
|
|
261
|
+
{ name: "useContentSwitchLocalePath", as: "useContentSwitchLocalePath", from: resolveRuntimeModule("./app/composables/route.js") },
|
|
262
|
+
// Search & site data — keep the headless useContentSearch export explicit
|
|
263
|
+
// because Nuxt UI owns the same auto-import name.
|
|
265
264
|
{ name: "useContentSearchData", as: "useContentSearchData", from: resolveRuntimeModule("./app/composables/search.js") },
|
|
266
265
|
{ name: "useContentSearchResults", as: "useContentSearchResults", from: resolveRuntimeModule("./app/composables/search.js") },
|
|
267
266
|
{ name: "querySiteData", as: "querySiteData", from: resolveRuntimeModule("./app/composables/site-data.js") }
|
|
@@ -356,25 +355,68 @@ const registerUserContentComponents = async (nuxt, resolve) => {
|
|
|
356
355
|
}
|
|
357
356
|
}
|
|
358
357
|
};
|
|
359
|
-
const registerGeneratedTypes = (contentConfigPath, resolveRuntimeModuleRoot) => {
|
|
358
|
+
const registerGeneratedTypes = (contentConfigPath, resolveRuntimeModuleRoot, collectionNames = [], i18nCollectionNames = []) => {
|
|
359
|
+
const collectionNameUnion = collectionNames.length ? collectionNames.map((name) => JSON.stringify(name)).join(" | ") : "never";
|
|
360
|
+
const i18nCollectionNameUnion = i18nCollectionNames.length ? i18nCollectionNames.map((name) => JSON.stringify(name)).join(" | ") : "never";
|
|
361
|
+
const collectionMapProperties = collectionNames.map((name) => {
|
|
362
|
+
const key = JSON.stringify(name);
|
|
363
|
+
return ` ${key}: StrictParsedContent & __GeneratedCollectionSchema<__ContentCollectionExport<${key}>>`;
|
|
364
|
+
});
|
|
360
365
|
return addTypeTemplate({
|
|
361
366
|
filename: "types/content.d.ts",
|
|
362
367
|
getContents: () => [
|
|
363
368
|
contentConfigPath ? `import type contentConfig from ${JSON.stringify(contentConfigPath)}` : "type __ContentConfig = { collections?: {} }",
|
|
369
|
+
contentConfigPath ? `import type * as contentConfigModule from ${JSON.stringify(contentConfigPath)}` : "declare const contentConfigModule: {}",
|
|
364
370
|
"import type { StrictParsedContent } from '@lupinum/ginko-content'",
|
|
365
371
|
"import type { CollectionSchema } from '@lupinum/ginko-content/config'",
|
|
366
372
|
contentConfigPath ? "type __ContentConfig = typeof contentConfig" : "",
|
|
367
373
|
"type __ContentCollections = NonNullable<__ContentConfig['collections']>",
|
|
374
|
+
`type __RuntimeContentCollectionNames = ${collectionNameUnion}`,
|
|
375
|
+
"type __NamedContentCollectionNames = {",
|
|
376
|
+
" [K in keyof typeof contentConfigModule & string]: typeof contentConfigModule[K] extends { name: K } ? K : never",
|
|
377
|
+
"}[keyof typeof contentConfigModule & string]",
|
|
378
|
+
"type __InferredContentCollectionNames = string extends keyof __ContentCollections",
|
|
379
|
+
" ? __NamedContentCollectionNames",
|
|
380
|
+
" : keyof __ContentCollections & string",
|
|
381
|
+
"type __ContentCollectionNames = [__RuntimeContentCollectionNames] extends [never]",
|
|
382
|
+
" ? __InferredContentCollectionNames",
|
|
383
|
+
" : __RuntimeContentCollectionNames",
|
|
384
|
+
"type __ContentCollectionExport<K extends string> = K extends keyof typeof contentConfigModule",
|
|
385
|
+
" ? typeof contentConfigModule[K]",
|
|
386
|
+
" : __ContentCollections[K]",
|
|
387
|
+
"type __GeneratedCollectionSchema<TCollection> = TCollection extends { __schema: infer TSchema }",
|
|
388
|
+
" ? TSchema extends { _output: infer TOutput } ? TOutput : {}",
|
|
389
|
+
" : CollectionSchema<TCollection>",
|
|
368
390
|
"type __GeneratedContentCollectionMap = {",
|
|
369
|
-
" [K in
|
|
391
|
+
" [K in __ContentCollectionNames]: StrictParsedContent & __GeneratedCollectionSchema<__ContentCollectionExport<K>>",
|
|
370
392
|
"}",
|
|
371
|
-
|
|
372
|
-
"
|
|
373
|
-
"
|
|
393
|
+
`type __RuntimeI18nCollectionNames = ${i18nCollectionNameUnion}`,
|
|
394
|
+
"type __InferredI18nCollectionNames = {",
|
|
395
|
+
" [K in __ContentCollectionNames]: __ContentCollectionExport<K> extends { __i18n: true }",
|
|
396
|
+
" ? K",
|
|
397
|
+
" : __ContentCollectionExport<K> extends { i18n: true } ? K : never",
|
|
398
|
+
"}[__ContentCollectionNames]",
|
|
399
|
+
"type __GeneratedI18nCollectionNames = [__RuntimeI18nCollectionNames] extends [never]",
|
|
400
|
+
" ? __InferredI18nCollectionNames",
|
|
401
|
+
" : __RuntimeI18nCollectionNames",
|
|
374
402
|
"declare module '@lupinum/ginko-content' {",
|
|
375
403
|
" interface ContentCollectionMap extends __GeneratedContentCollectionMap {}",
|
|
376
404
|
" interface ContentCollectionI18nMap extends Pick<__GeneratedContentCollectionMap, __GeneratedI18nCollectionNames> {}",
|
|
377
405
|
"}",
|
|
406
|
+
"declare global {",
|
|
407
|
+
" interface GinkoContentCollectionMap {",
|
|
408
|
+
...collectionMapProperties,
|
|
409
|
+
" }",
|
|
410
|
+
" interface GinkoContentCollectionI18nMap extends Pick<__GeneratedContentCollectionMap, __GeneratedI18nCollectionNames> {}",
|
|
411
|
+
"}",
|
|
412
|
+
"declare module '@lupinum/ginko-content/dist/types/query' {",
|
|
413
|
+
" interface ContentCollectionMap extends __GeneratedContentCollectionMap {}",
|
|
414
|
+
" interface ContentCollectionI18nMap extends Pick<__GeneratedContentCollectionMap, __GeneratedI18nCollectionNames> {}",
|
|
415
|
+
"}",
|
|
416
|
+
"declare module '@lupinum/ginko-content/dist/types/query.js' {",
|
|
417
|
+
" interface ContentCollectionMap extends __GeneratedContentCollectionMap {}",
|
|
418
|
+
" interface ContentCollectionI18nMap extends Pick<__GeneratedContentCollectionMap, __GeneratedI18nCollectionNames> {}",
|
|
419
|
+
"}",
|
|
378
420
|
"declare module '#content/server' {",
|
|
379
421
|
` const one: typeof import('${resolveRuntimeModuleRoot("./server")}').one`,
|
|
380
422
|
` const many: typeof import('${resolveRuntimeModuleRoot("./server")}').many`,
|
|
@@ -498,6 +540,23 @@ const toLocalPath = (value) => {
|
|
|
498
540
|
return value;
|
|
499
541
|
}
|
|
500
542
|
};
|
|
543
|
+
const placeholderHosts = /* @__PURE__ */ new Set([
|
|
544
|
+
"example.com",
|
|
545
|
+
"example.net",
|
|
546
|
+
"example.org",
|
|
547
|
+
"localhost",
|
|
548
|
+
"127.0.0.1",
|
|
549
|
+
"0.0.0.0",
|
|
550
|
+
"::1"
|
|
551
|
+
]);
|
|
552
|
+
const isPlaceholderHost = (host) => placeholderHosts.has(host) || host.endsWith(".example.com") || host.endsWith(".example.net") || host.endsWith(".example.org") || host.endsWith(".localhost");
|
|
553
|
+
const extractPlaceholderSiteUrls = (locValues) => Array.from(new Set(locValues.filter((value) => {
|
|
554
|
+
try {
|
|
555
|
+
return isPlaceholderHost(new URL(value).hostname);
|
|
556
|
+
} catch {
|
|
557
|
+
return false;
|
|
558
|
+
}
|
|
559
|
+
})));
|
|
501
560
|
const discoverSitemapsFromDisk = async (outputPublicDir) => {
|
|
502
561
|
const indexPath = join$1(outputPublicDir, SITEMAP_INDEX);
|
|
503
562
|
try {
|
|
@@ -553,6 +612,9 @@ const normalizeContentSitemapAssertOptions = (options) => ({
|
|
|
553
612
|
minUrlsPerSitemap: options?.minUrlsPerSitemap ?? 1,
|
|
554
613
|
requireImages: options?.requireImages ?? false,
|
|
555
614
|
requiredCollections: options?.requiredCollections ?? [],
|
|
615
|
+
requiredPaths: options?.requiredPaths ?? [],
|
|
616
|
+
forbiddenPathPrefixes: options?.forbiddenPathPrefixes ?? [],
|
|
617
|
+
requireProductionSiteUrl: options?.requireProductionSiteUrl ?? false,
|
|
556
618
|
sitemaps: options?.sitemaps ?? {}
|
|
557
619
|
});
|
|
558
620
|
const shouldRunSitemapAssertionOnCompiled = (options, nitro) => {
|
|
@@ -594,6 +656,29 @@ async function assertGeneratedSitemaps({
|
|
|
594
656
|
failures.push(`- ${target.name}: expected image entries but found none`);
|
|
595
657
|
}
|
|
596
658
|
}
|
|
659
|
+
const sitemapLocValues = discoveredSitemaps.flatMap((target) => extractLocValues(target.xml));
|
|
660
|
+
const sitemapPaths = Array.from(new Set(sitemapLocValues.map(toLocalPath)));
|
|
661
|
+
const pathSet = new Set(sitemapPaths);
|
|
662
|
+
const missingPaths = options.requiredPaths.filter((path) => !pathSet.has(path));
|
|
663
|
+
if (missingPaths.length) {
|
|
664
|
+
failures.push(`- Missing required sitemap paths: ${missingPaths.join(", ")}`);
|
|
665
|
+
}
|
|
666
|
+
const forbiddenPaths = sitemapPaths.filter(
|
|
667
|
+
(path) => options.forbiddenPathPrefixes.some((prefix) => path === prefix || path.startsWith(prefix.endsWith("/") ? prefix : `${prefix}/`))
|
|
668
|
+
);
|
|
669
|
+
if (forbiddenPaths.length) {
|
|
670
|
+
failures.push(`- Forbidden sitemap paths found: ${forbiddenPaths.join(", ")}`);
|
|
671
|
+
}
|
|
672
|
+
if (options.requireProductionSiteUrl) {
|
|
673
|
+
const placeholderSiteUrls = extractPlaceholderSiteUrls(sitemapLocValues);
|
|
674
|
+
if (placeholderSiteUrls.length) {
|
|
675
|
+
failures.push([
|
|
676
|
+
`- Placeholder sitemap URLs found: ${placeholderSiteUrls.join(", ")}`,
|
|
677
|
+
"Expected production URLs in generated sitemap loc values.",
|
|
678
|
+
"Set site.url or runtimeConfig.public.siteUrl to the deployed origin for production release checks."
|
|
679
|
+
].join(" "));
|
|
680
|
+
}
|
|
681
|
+
}
|
|
597
682
|
const missingCollections = options.requiredCollections.filter((collection) => (collectionRouteCounts[collection] || 0) === 0);
|
|
598
683
|
if (missingCollections.length) {
|
|
599
684
|
failures.push(`- Missing content sitemap routes for collections: ${missingCollections.join(", ")}`);
|
|
@@ -643,16 +728,40 @@ function hasNuxtI18nModule(modules = []) {
|
|
|
643
728
|
function hasNuxtSitemapModule(modules = []) {
|
|
644
729
|
return hasNuxtModule(modules, "@nuxtjs/sitemap");
|
|
645
730
|
}
|
|
731
|
+
function configureNuxtSitemapSource(nuxt, apiBaseURL, sitemapPath = "/sitemap") {
|
|
732
|
+
if (!hasNuxtSitemapModule(nuxt.options.modules)) {
|
|
733
|
+
return;
|
|
734
|
+
}
|
|
735
|
+
const source = {
|
|
736
|
+
context: {
|
|
737
|
+
name: GINKO_SITEMAP_SOURCE_NAME
|
|
738
|
+
},
|
|
739
|
+
fetch: resolveContentSitemapSource(apiBaseURL, sitemapPath)
|
|
740
|
+
};
|
|
741
|
+
const sitemap = nuxt.options.sitemap ??= {};
|
|
742
|
+
const sources = Array.isArray(sitemap.sources) ? sitemap.sources : [];
|
|
743
|
+
sitemap.sources = [
|
|
744
|
+
...sources.filter((item) => {
|
|
745
|
+
if (typeof item === "string") return item !== source.fetch;
|
|
746
|
+
return item?.fetch !== source.fetch && item?.context?.name !== GINKO_SITEMAP_SOURCE_NAME;
|
|
747
|
+
}),
|
|
748
|
+
source
|
|
749
|
+
];
|
|
750
|
+
}
|
|
646
751
|
function resolveNuxtSitemapPrerenderRoutes(nuxt) {
|
|
647
752
|
if (!hasNuxtSitemapModule(nuxt.options.modules)) {
|
|
648
753
|
return [];
|
|
649
754
|
}
|
|
755
|
+
const nuxtSitemap = nuxt.options.sitemap;
|
|
756
|
+
if (nuxtSitemap?.sitemaps === false) {
|
|
757
|
+
return ["/sitemap.xml"];
|
|
758
|
+
}
|
|
650
759
|
const nuxtI18n = nuxt.options.i18n || {};
|
|
651
760
|
if (!hasNuxtI18nModule(nuxt.options.modules) || !Array.isArray(nuxtI18n.locales) || nuxtI18n.locales.length === 0) {
|
|
652
|
-
return [];
|
|
761
|
+
return ["/sitemap.xml"];
|
|
653
762
|
}
|
|
654
763
|
const childRoutes = nuxtI18n.locales.map((locale) => typeof locale === "string" ? locale : locale.language || locale.code).filter(Boolean).map((locale) => `/__sitemap__/${locale}.xml`);
|
|
655
|
-
return Array.from(/* @__PURE__ */ new Set(["/sitemap_index.xml", ...childRoutes]));
|
|
764
|
+
return Array.from(/* @__PURE__ */ new Set(["/sitemap.xml", "/sitemap_index.xml", ...childRoutes]));
|
|
656
765
|
}
|
|
657
766
|
function resolveModuleI18nOptions(options, nuxt) {
|
|
658
767
|
if (options.i18n === false) {
|
|
@@ -711,7 +820,7 @@ function normalizeSearchOptions(options) {
|
|
|
711
820
|
}
|
|
712
821
|
const defaultMiniSearchOptions = {
|
|
713
822
|
fields: ["title", "content", "headings"],
|
|
714
|
-
storeFields: ["path", "title", "excerpt", "anchor", "locale"],
|
|
823
|
+
storeFields: ["path", "title", "excerpt", "anchor", "locale", "collection"],
|
|
715
824
|
boost: {
|
|
716
825
|
title: 4,
|
|
717
826
|
headings: 2,
|
|
@@ -1018,9 +1127,7 @@ const collectSitemapCollectionRouteCounts = async (rootDir, contentContext) => {
|
|
|
1018
1127
|
return counts;
|
|
1019
1128
|
};
|
|
1020
1129
|
const registerContentNitroIntegrationHooks = (nitroConfig, options, contentContext) => {
|
|
1021
|
-
|
|
1022
|
-
return;
|
|
1023
|
-
}
|
|
1130
|
+
const usesFilesystemProvider = !contentContext.provider || contentContext.provider === "filesystem";
|
|
1024
1131
|
nitroConfig.hooks ||= {};
|
|
1025
1132
|
if (contentContext.sitemap?.assert?.enabled) {
|
|
1026
1133
|
appendHook(nitroConfig.hooks, "compiled", async (nitro) => {
|
|
@@ -1032,7 +1139,7 @@ const registerContentNitroIntegrationHooks = (nitroConfig, options, contentConte
|
|
|
1032
1139
|
await assertGeneratedSitemaps({
|
|
1033
1140
|
outputPublicDir: nitro.options.output.publicDir,
|
|
1034
1141
|
options: assertOptions,
|
|
1035
|
-
collectionRouteCounts: await collectSitemapCollectionRouteCounts(options.rootDir, contentContext),
|
|
1142
|
+
collectionRouteCounts: usesFilesystemProvider ? await collectSitemapCollectionRouteCounts(options.rootDir, contentContext) : {},
|
|
1036
1143
|
logger: nitro.logger
|
|
1037
1144
|
});
|
|
1038
1145
|
} catch (error) {
|
|
@@ -1044,15 +1151,73 @@ const registerContentNitroIntegrationHooks = (nitroConfig, options, contentConte
|
|
|
1044
1151
|
});
|
|
1045
1152
|
}
|
|
1046
1153
|
appendHook(nitroConfig.hooks, "prerender:routes", async (routes) => {
|
|
1047
|
-
|
|
1048
|
-
|
|
1154
|
+
if (usesFilesystemProvider) {
|
|
1155
|
+
for (const route of await collectPrerenderRoutes(options.rootDir, contentContext)) {
|
|
1156
|
+
routes.add(route);
|
|
1157
|
+
}
|
|
1049
1158
|
}
|
|
1050
|
-
|
|
1159
|
+
const sitemapPrerenderRoutes = typeof options.sitemapPrerenderRoutes === "function" ? options.sitemapPrerenderRoutes() : options.sitemapPrerenderRoutes || [];
|
|
1160
|
+
for (const route of sitemapPrerenderRoutes) {
|
|
1051
1161
|
routes.add(route);
|
|
1052
1162
|
}
|
|
1053
1163
|
});
|
|
1054
1164
|
};
|
|
1055
1165
|
|
|
1166
|
+
const getContentPageMeta = (page) => {
|
|
1167
|
+
const content = page.meta?.content;
|
|
1168
|
+
return content && typeof content === "object" ? content : void 0;
|
|
1169
|
+
};
|
|
1170
|
+
const isRouteConfig = (value) => typeof value === "string" || value !== null && typeof value === "object" && Object.values(value).every((entry) => typeof entry === "string");
|
|
1171
|
+
const describePage = (page) => page.file || page.path || "<unknown page>";
|
|
1172
|
+
const expectedLocalesFor = (collection, locales, defaultLocale) => {
|
|
1173
|
+
const collectionI18n = collection.i18n && typeof collection.i18n === "object" ? collection.i18n : void 0;
|
|
1174
|
+
return collectionI18n?.locales?.length ? collectionI18n.locales : locales.length ? locales : defaultLocale ? [defaultLocale] : [];
|
|
1175
|
+
};
|
|
1176
|
+
function validateContentPageRouteMetadata(pages, collections, options = {}) {
|
|
1177
|
+
const visit = (page) => {
|
|
1178
|
+
const contentMeta = getContentPageMeta(page);
|
|
1179
|
+
if (contentMeta) {
|
|
1180
|
+
validatePage(page, contentMeta, collections, options);
|
|
1181
|
+
}
|
|
1182
|
+
for (const child of page.children || []) {
|
|
1183
|
+
visit(child);
|
|
1184
|
+
}
|
|
1185
|
+
};
|
|
1186
|
+
for (const page of pages) {
|
|
1187
|
+
visit(page);
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
function validatePage(page, contentMeta, collections, options) {
|
|
1191
|
+
const collectionName = contentMeta.collection;
|
|
1192
|
+
if (typeof collectionName !== "string" || !collectionName) {
|
|
1193
|
+
throw new Error(`@lupinum/ginko-content page "${describePage(page)}" declares content route metadata without a string collection name.`);
|
|
1194
|
+
}
|
|
1195
|
+
const collection = collections[collectionName];
|
|
1196
|
+
if (!collection) {
|
|
1197
|
+
throw new Error(`@lupinum/ginko-content page "${describePage(page)}" declares content collection "${collectionName}", but no matching collection exists in content.config.ts.`);
|
|
1198
|
+
}
|
|
1199
|
+
if (!isRouteConfig(contentMeta.route)) {
|
|
1200
|
+
throw new Error(`@lupinum/ginko-content page "${describePage(page)}" declares content collection "${collectionName}" but no route metadata. Add definePageMeta({ content: { collection: '${collectionName}', route: ... } }).`);
|
|
1201
|
+
}
|
|
1202
|
+
const locales = expectedLocalesFor(collection, options.locales || [], options.defaultLocale);
|
|
1203
|
+
const defaultLocale = collection.i18n && typeof collection.i18n === "object" ? collection.i18n.defaultLocale : options.defaultLocale;
|
|
1204
|
+
const expected = normalizeRouteMounts(collection.route, locales, defaultLocale);
|
|
1205
|
+
const actual = normalizeRouteMounts(contentMeta.route, locales, defaultLocale);
|
|
1206
|
+
if (!expected) {
|
|
1207
|
+
throw new Error(`@lupinum/ginko-content page "${describePage(page)}" declares route metadata for collection "${collectionName}", but that collection has no route config in content.config.ts.`);
|
|
1208
|
+
}
|
|
1209
|
+
for (const locale of Object.keys(expected)) {
|
|
1210
|
+
const expectedRoute = normalizeContentPath(expected[locale]);
|
|
1211
|
+
const actualRoute = actual?.[locale] ? normalizeContentPath(actual[locale]) : void 0;
|
|
1212
|
+
if (!actualRoute) {
|
|
1213
|
+
throw new Error(`@lupinum/ginko-content route metadata mismatch in "${describePage(page)}": collection "${collectionName}" is missing locale "${locale}" route "${expectedRoute}".`);
|
|
1214
|
+
}
|
|
1215
|
+
if (actualRoute !== expectedRoute) {
|
|
1216
|
+
throw new Error(`@lupinum/ginko-content route metadata mismatch in "${describePage(page)}": collection "${collectionName}" locale "${locale}" expected route "${expectedRoute}" but page metadata declares "${actualRoute}".`);
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1056
1221
|
const module$1 = defineNuxtModule({
|
|
1057
1222
|
meta: {
|
|
1058
1223
|
name,
|
|
@@ -1158,6 +1323,7 @@ const module$1 = defineNuxtModule({
|
|
|
1158
1323
|
const resolvedI18n = resolveModuleI18nOptions(options, nuxt);
|
|
1159
1324
|
const resolvedSitemap = normalizeSitemapOptions(options);
|
|
1160
1325
|
const resolvedSearch = normalizeSearchOptions(options);
|
|
1326
|
+
validateCollectionNames(appContentConfig.collections);
|
|
1161
1327
|
options.collections = Object.fromEntries(Object.entries(appContentConfig.collections).map(([name2, collection]) => [
|
|
1162
1328
|
name2,
|
|
1163
1329
|
{
|
|
@@ -1170,10 +1336,11 @@ const module$1 = defineNuxtModule({
|
|
|
1170
1336
|
}
|
|
1171
1337
|
]));
|
|
1172
1338
|
options.provider = appContentConfig.provider || options.provider || "filesystem";
|
|
1173
|
-
|
|
1339
|
+
const providerRegistry = {
|
|
1174
1340
|
...options.providers || {},
|
|
1175
1341
|
...appContentConfig.providers || {}
|
|
1176
1342
|
};
|
|
1343
|
+
options.providers = providerRegistry;
|
|
1177
1344
|
const buildIntegrity = nuxt.options.dev ? void 0 : Date.now();
|
|
1178
1345
|
const contentContext = {
|
|
1179
1346
|
...options,
|
|
@@ -1186,6 +1353,15 @@ const module$1 = defineNuxtModule({
|
|
|
1186
1353
|
sitemap: resolvedSitemap,
|
|
1187
1354
|
search: resolvedSearch
|
|
1188
1355
|
};
|
|
1356
|
+
if (resolvedSitemap !== false) {
|
|
1357
|
+
configureNuxtSitemapSource(nuxt, options.api.baseURL, resolvedSitemap.path);
|
|
1358
|
+
}
|
|
1359
|
+
nuxt.hook("pages:extend", (pages) => {
|
|
1360
|
+
validateContentPageRouteMetadata(pages, options.collections, {
|
|
1361
|
+
locales: resolvedI18n.locales,
|
|
1362
|
+
defaultLocale: resolvedI18n.defaultLocale
|
|
1363
|
+
});
|
|
1364
|
+
});
|
|
1189
1365
|
if (resolvedSitemap && resolvedSitemap.assert.enabled) {
|
|
1190
1366
|
nuxt.hook("sitemap:prerender:done", async ({ sitemaps }) => {
|
|
1191
1367
|
const assertOptions = contentContext.sitemap?.assert;
|
|
@@ -1207,7 +1383,12 @@ const module$1 = defineNuxtModule({
|
|
|
1207
1383
|
registerRuntimeImports(resolveRuntimeModule);
|
|
1208
1384
|
registerRuntimeComponents(resolve);
|
|
1209
1385
|
registerContentComponentsTemplate(addTemplate);
|
|
1210
|
-
registerGeneratedTypes(
|
|
1386
|
+
registerGeneratedTypes(
|
|
1387
|
+
contentConfigPath,
|
|
1388
|
+
resolveRuntimeModule,
|
|
1389
|
+
Object.keys(appContentConfig.collections),
|
|
1390
|
+
Object.entries(appContentConfig.collections).filter(([, collection]) => Boolean(collection.i18n)).map(([name2]) => name2)
|
|
1391
|
+
);
|
|
1211
1392
|
await registerUserContentComponents(nuxt, resolve);
|
|
1212
1393
|
const getSearchRuntime = () => contentContext.search === false ? false : createSearchRuntimeConfig(contentContext.search, options.api.baseURL);
|
|
1213
1394
|
nuxt.hook("nitro:config", (nitroConfig) => {
|
|
@@ -1265,7 +1446,7 @@ const module$1 = defineNuxtModule({
|
|
|
1265
1446
|
}
|
|
1266
1447
|
registerContentNitroIntegrationHooks(nitroConfig, {
|
|
1267
1448
|
rootDir: nuxt.options.rootDir,
|
|
1268
|
-
sitemapPrerenderRoutes: contentContext.sitemap === false ? [] : resolveNuxtSitemapPrerenderRoutes(nuxt)
|
|
1449
|
+
sitemapPrerenderRoutes: () => contentContext.sitemap === false ? [] : resolveNuxtSitemapPrerenderRoutes(nuxt)
|
|
1269
1450
|
}, {
|
|
1270
1451
|
collections: contentContext.collections,
|
|
1271
1452
|
locales: contentContext.locales,
|
|
@@ -1310,6 +1491,8 @@ const module$1 = defineNuxtModule({
|
|
|
1310
1491
|
});
|
|
1311
1492
|
}
|
|
1312
1493
|
nuxt.hook("modules:done", async () => {
|
|
1494
|
+
await nuxt.callHook("content:providers", contentContext.providers ||= {});
|
|
1495
|
+
assertConfiguredProviderAvailable(contentContext);
|
|
1313
1496
|
await nuxt.callHook("content:context", contentContext);
|
|
1314
1497
|
if (contentContext.search !== false) {
|
|
1315
1498
|
registerContentSearchServerHandlers(options.api.baseURL, contentContext.search, resolveRuntimeModule);
|
|
@@ -1317,17 +1500,22 @@ const module$1 = defineNuxtModule({
|
|
|
1317
1500
|
contentContext.defaultLocale = contentContext.defaultLocale || contentContext.locales[0];
|
|
1318
1501
|
contentContext.markdown = processMarkdownOptions(contentContext.markdown);
|
|
1319
1502
|
await validateBuiltinMarkdownPlugins(contentContext.markdown.plugins, resolvePath);
|
|
1320
|
-
const runtimeCollections = Object.fromEntries(Object.entries(options.collections || {}).map(([name2, collection]) =>
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1503
|
+
const runtimeCollections = Object.fromEntries(Object.entries(options.collections || {}).map(([name2, collection]) => {
|
|
1504
|
+
const references = collectTopLevelReferenceFieldsByTarget(collection.schema);
|
|
1505
|
+
return [
|
|
1506
|
+
name2,
|
|
1507
|
+
{
|
|
1508
|
+
...collection.source ? { source: collection.source } : {},
|
|
1509
|
+
...collection.exclude ? { exclude: collection.exclude } : {},
|
|
1510
|
+
...collection.type ? { type: collection.type } : {},
|
|
1511
|
+
strict: collection.strict ?? true,
|
|
1512
|
+
...collection.route ? { route: collection.route } : {},
|
|
1513
|
+
...typeof collection.sitemap === "boolean" ? { sitemap: collection.sitemap } : {},
|
|
1514
|
+
...collection.i18n && collection.i18n !== true ? { i18n: collection.i18n } : {},
|
|
1515
|
+
...Object.keys(references).length ? { references } : {}
|
|
1516
|
+
}
|
|
1517
|
+
];
|
|
1518
|
+
}));
|
|
1331
1519
|
const cacheIntegrity = hash({
|
|
1332
1520
|
locales: contentContext.locales,
|
|
1333
1521
|
defaultLocale: contentContext.defaultLocale,
|
|
@@ -1348,6 +1536,18 @@ const module$1 = defineNuxtModule({
|
|
|
1348
1536
|
}
|
|
1349
1537
|
}
|
|
1350
1538
|
});
|
|
1539
|
+
function assertConfiguredProviderAvailable(contentContext) {
|
|
1540
|
+
const provider = contentContext.provider || "filesystem";
|
|
1541
|
+
if (provider === "filesystem") return;
|
|
1542
|
+
if (contentContext.providers?.[provider]) return;
|
|
1543
|
+
if (provider === "cms") {
|
|
1544
|
+
throw new Error('content.config.ts sets provider "cms", but no CMS provider module registered it. Add @lupinum/ginko-cms to nuxt.config.ts modules or remove provider: "cms".');
|
|
1545
|
+
}
|
|
1546
|
+
throw new Error(`content.config.ts sets provider "${provider}", but no provider module registered it. Register a module for "${provider}" or add it to content providers.`);
|
|
1547
|
+
}
|
|
1548
|
+
function validateCollectionNames(collections) {
|
|
1549
|
+
normalizeContentConfigCollectionNames(collections);
|
|
1550
|
+
}
|
|
1351
1551
|
function validateRemovedMarkdownOptions(options) {
|
|
1352
1552
|
if (options.highlight !== void 0) {
|
|
1353
1553
|
throw new Error("`content.highlight` was removed. Enable syntax highlighting with `content.markdown.plugins`, for example `[['highlight', { ...options }]]`.");
|
package/dist/public/client.d.ts
CHANGED
|
@@ -7,9 +7,11 @@
|
|
|
7
7
|
export { one, many, paginate, backlinks, resolveOne, variants, tree, neighbors } from '../runtime/app/composables/query-api.js';
|
|
8
8
|
export { getCollectionPath } from '../runtime/query/routes.js';
|
|
9
9
|
export type { CollectionPathOptions } from '../runtime/query/routes.js';
|
|
10
|
-
export {
|
|
10
|
+
export { useContentHead } from '../runtime/app/composables/head.js';
|
|
11
|
+
export { useContentPage, useContentOne, useContentMany, useContentPagination, useContentBacklinks, useContentResolveOne, useContentVariants, useContentTree, useContentNavigation, useContentNeighbors, useContentLocaleSwitch } from '../runtime/app/composables/use-content.js';
|
|
11
12
|
export type { UseContentPageOptions } from '../runtime/app/composables/use-content.js';
|
|
12
|
-
export type { QueryWhere, QueryOperators, ContentSelector, ContentResolvedMeta, BacklinkFields, BacklinkSource, BacklinksOptions, BacklinksResult, LocaleFallback, DocumentFromHandle, OneOptions, ManyOptions, PaginationOptions, PaginationResult, PopulateSpec, PopulatedDocument, ResolveOneOptions, ResolveOneResult, LocalizedDoc, LocalePathEntry, NeighborsOptions, NeighborsResult, ResolutionEnvelope, SortSpec, TreeOptions, VariantsOptions, ContentVariant, ContentNavigationItem, ContentTreeItem, ContentRouteMeta, ContentSearchSection } from '../types/query.js';
|
|
13
|
+
export type { QueryWhere, QueryOperators, ContentSelector, ContentResolvedMeta, BacklinkFields, BacklinkSource, BacklinksOptions, BacklinksResult, LocaleFallback, DocumentFromHandle, OneOptions, ManyOptions, PaginationOptions, PaginationResult, PopulateSpec, PopulatedDocument, ResolveOneOptions, ResolveOneResult, LocalizedDoc, LocalePathEntry, NeighborsOptions, NeighborsResult, ResolutionEnvelope, SortSpec, TreeOptions, VariantsOptions, ContentVariant, ContentNavigationItem, ContentCollectionName, ContentCollectionStringName, ContentCollectionTarget, ContentTreeItem, ContentRouteMeta, ContentSearchSection } from '../types/query.js';
|
|
14
|
+
export type { ContentNavigationNode } from '../runtime/app/composables/use-content.js';
|
|
13
15
|
export { useContentSearch, useContentSearchData, useContentSearchResults } from '../runtime/app/composables/search.js';
|
|
14
16
|
export type { UseContentSearchDataOptions, UseContentSearchDataResult, UseContentSearchOptions, UseContentSearchResult, UseContentSearchResultsOptions, UseContentSearchResultsResult } from '../runtime/app/composables/search.js';
|
|
15
17
|
export { querySiteData } from '../runtime/app/composables/site-data.js';
|
package/dist/public/client.js
CHANGED
|
@@ -9,6 +9,7 @@ export {
|
|
|
9
9
|
neighbors
|
|
10
10
|
} from "../runtime/app/composables/query-api.js";
|
|
11
11
|
export { getCollectionPath } from "../runtime/query/routes.js";
|
|
12
|
+
export { useContentHead } from "../runtime/app/composables/head.js";
|
|
12
13
|
export {
|
|
13
14
|
useContentPage,
|
|
14
15
|
useContentOne,
|
|
@@ -18,6 +19,7 @@ export {
|
|
|
18
19
|
useContentResolveOne,
|
|
19
20
|
useContentVariants,
|
|
20
21
|
useContentTree,
|
|
22
|
+
useContentNavigation,
|
|
21
23
|
useContentNeighbors,
|
|
22
24
|
useContentLocaleSwitch
|
|
23
25
|
} from "../runtime/app/composables/use-content.js";
|
|
@@ -11,6 +11,13 @@ function normalizeBody(value, excerpt) {
|
|
|
11
11
|
}
|
|
12
12
|
return null;
|
|
13
13
|
}
|
|
14
|
+
function warnUnsupportedValue(value, excerpt) {
|
|
15
|
+
const path = value?._path ? ` for "${value._path}"` : "";
|
|
16
|
+
const target = excerpt ? "excerpt" : "body";
|
|
17
|
+
console.warn(
|
|
18
|
+
`[ginko-content] <ContentRenderer> could not render ${target}${path}. Pass the full content document with a markdown body, or provide an \`empty\` slot for empty/unsupported content.`
|
|
19
|
+
);
|
|
20
|
+
}
|
|
14
21
|
export default defineComponent({
|
|
15
22
|
name: "ContentRenderer",
|
|
16
23
|
props: {
|
|
@@ -74,11 +81,8 @@ export default defineComponent({
|
|
|
74
81
|
...this.$attrs
|
|
75
82
|
});
|
|
76
83
|
}
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
null,
|
|
80
|
-
JSON.stringify({ message: "You should use slots with <ContentRenderer>", value, excerpt, tag, prose, unwrap }, null, 2)
|
|
81
|
-
);
|
|
84
|
+
warnUnsupportedValue(value, excerpt);
|
|
85
|
+
return null;
|
|
82
86
|
}
|
|
83
87
|
});
|
|
84
88
|
</script>
|
|
@@ -57,8 +57,8 @@ declare const __VLS_export: import("vue").DefineComponent<import("vue").ExtractP
|
|
|
57
57
|
default: () => {};
|
|
58
58
|
};
|
|
59
59
|
}>> & Readonly<{}>, {
|
|
60
|
-
data: Record<string, any>;
|
|
61
60
|
excerpt: boolean;
|
|
61
|
+
data: Record<string, any>;
|
|
62
62
|
components: Record<string, any>;
|
|
63
63
|
prose: boolean;
|
|
64
64
|
tag: string;
|
|
@@ -57,8 +57,8 @@ declare const __VLS_export: import("vue").DefineComponent<import("vue").ExtractP
|
|
|
57
57
|
default: () => {};
|
|
58
58
|
};
|
|
59
59
|
}>> & Readonly<{}>, {
|
|
60
|
-
data: Record<string, any>;
|
|
61
60
|
excerpt: boolean;
|
|
61
|
+
data: Record<string, any>;
|
|
62
62
|
components: Record<string, any>;
|
|
63
63
|
prose: boolean;
|
|
64
64
|
tag: string;
|
|
@@ -1,11 +1,35 @@
|
|
|
1
1
|
import type { RouteLocationNormalized, RouteLocationNormalizedLoaded } from 'vue-router';
|
|
2
2
|
import type { Ref } from 'vue';
|
|
3
|
-
|
|
3
|
+
type ContentHeadObject = {
|
|
4
|
+
title?: string;
|
|
5
|
+
description?: string;
|
|
6
|
+
image?: string | Record<string, any>;
|
|
7
|
+
meta?: Array<Record<string, any>>;
|
|
8
|
+
link?: Array<Record<string, any>>;
|
|
9
|
+
[key: string]: any;
|
|
10
|
+
};
|
|
11
|
+
type ContentHeadDocument = {
|
|
12
|
+
title?: string;
|
|
13
|
+
description?: string;
|
|
14
|
+
image?: string | Record<string, any>;
|
|
15
|
+
head?: ContentHeadObject;
|
|
16
|
+
seo?: ContentHeadObject;
|
|
17
|
+
};
|
|
18
|
+
type ContentHeadRuntimeConfig = {
|
|
19
|
+
app: {
|
|
20
|
+
baseURL: string;
|
|
21
|
+
};
|
|
22
|
+
public: {
|
|
23
|
+
siteUrl?: unknown;
|
|
24
|
+
};
|
|
25
|
+
};
|
|
4
26
|
/**
|
|
5
27
|
* Apply sensible page head defaults from a parsed content document.
|
|
6
28
|
*
|
|
7
|
-
* The composable merges explicit `content.head` values with common fallbacks
|
|
29
|
+
* The composable merges explicit `content.head` or `content.seo` values with common fallbacks
|
|
8
30
|
* from the document (`title`, `description`, `image`) and adds canonical /
|
|
9
31
|
* Open Graph URL metadata when a public site URL is configured.
|
|
10
32
|
*/
|
|
11
|
-
export declare const useContentHead: (_content:
|
|
33
|
+
export declare const useContentHead: (_content: ContentHeadDocument | null | undefined | Ref<ContentHeadDocument | null | undefined>, to?: RouteLocationNormalized | RouteLocationNormalizedLoaded) => void;
|
|
34
|
+
export declare const resolveContentHead: (data: ContentHeadDocument | null | undefined, to: Pick<RouteLocationNormalized | RouteLocationNormalizedLoaded, "path" | "fullPath">, config: ContentHeadRuntimeConfig) => ContentHeadObject | null;
|
|
35
|
+
export {};
|