@lupinum/ginko-content 0.1.0 → 0.1.1
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 +74 -44
- package/compatibility.json +4 -4
- package/dist/cli.mjs +94 -11
- package/dist/integrations/nitro/context.d.ts +4 -8
- package/dist/module.d.mts +13 -12
- package/dist/module.json +1 -1
- package/dist/module.mjs +47 -10
- 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/server/plugins/sitemap.js +4 -4
- 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/types/config.d.ts +17 -15
- package/dist/types/module.d.ts +4 -3
- package/dist/types/query.d.ts +1 -0
- package/dist/types.d.mts +1 -1
- package/dist/web-types.json +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,57 +1,78 @@
|
|
|
1
1
|
# @lupinum/ginko-content
|
|
2
2
|
|
|
3
|
-
Filesystem-first
|
|
3
|
+
Filesystem-first content for Nuxt 4.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Write Markdown and data files in `content/`, define collections once in
|
|
6
|
+
`content.config.ts`, then use those collection handles for route pages, lists,
|
|
7
|
+
navigation, search, i18n, and sitemap output.
|
|
6
8
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
9
|
+
Use it when you want content files to stay simple, but your Nuxt app still
|
|
10
|
+
needs explicit APIs for route resolution, typed frontmatter, localized content,
|
|
11
|
+
and server-side reads.
|
|
12
|
+
|
|
13
|
+
## Requirements
|
|
14
|
+
|
|
15
|
+
- Node.js 20 or later
|
|
16
|
+
- Nuxt 4.0 or later
|
|
13
17
|
|
|
14
18
|
## Install
|
|
15
19
|
|
|
16
20
|
```bash
|
|
17
|
-
|
|
21
|
+
npx nuxi module add @lupinum/ginko-content
|
|
18
22
|
```
|
|
19
23
|
|
|
20
|
-
|
|
24
|
+
The Nuxt CLI installs the package and registers the module in `nuxt.config.ts`.
|
|
25
|
+
If you prefer to install by hand:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
pnpm add @lupinum/ginko-content
|
|
29
|
+
```
|
|
21
30
|
|
|
22
31
|
```ts
|
|
23
32
|
export default defineNuxtConfig({
|
|
24
|
-
modules: ['@lupinum/ginko-content']
|
|
33
|
+
modules: ['@lupinum/ginko-content'],
|
|
34
|
+
imports: {
|
|
35
|
+
autoImport: true
|
|
36
|
+
}
|
|
25
37
|
})
|
|
26
38
|
```
|
|
27
39
|
|
|
28
|
-
|
|
40
|
+
## Quick Start
|
|
29
41
|
|
|
30
|
-
|
|
42
|
+
Define a collection:
|
|
31
43
|
|
|
32
|
-
|
|
44
|
+
```ts
|
|
45
|
+
// content.config.ts
|
|
46
|
+
import { defineCollection, defineContentConfig } from '@lupinum/ginko-content/config'
|
|
33
47
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
48
|
+
export const pages = defineCollection('pages', {
|
|
49
|
+
type: 'page',
|
|
50
|
+
source: '**/*.md'
|
|
51
|
+
})
|
|
38
52
|
|
|
39
|
-
|
|
40
|
-
|
|
53
|
+
export default defineContentConfig({
|
|
54
|
+
collections: {
|
|
55
|
+
pages
|
|
56
|
+
}
|
|
57
|
+
})
|
|
58
|
+
```
|
|
41
59
|
|
|
42
|
-
|
|
60
|
+
Create `content/index.md`:
|
|
43
61
|
|
|
44
|
-
|
|
62
|
+
```md
|
|
63
|
+
---
|
|
64
|
+
title: Welcome
|
|
65
|
+
---
|
|
45
66
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
guide/getting-started.md
|
|
67
|
+
# Welcome
|
|
68
|
+
|
|
69
|
+
This file renders at `/`.
|
|
50
70
|
```
|
|
51
71
|
|
|
52
|
-
Render
|
|
72
|
+
Render the current route through the collection:
|
|
53
73
|
|
|
54
74
|
```vue
|
|
75
|
+
<!-- pages/[...slug].vue -->
|
|
55
76
|
<script setup lang="ts">
|
|
56
77
|
import { pages } from '~/content.config'
|
|
57
78
|
|
|
@@ -63,28 +84,37 @@ const { page } = await useContentPage(pages)
|
|
|
63
84
|
</template>
|
|
64
85
|
```
|
|
65
86
|
|
|
66
|
-
|
|
87
|
+
## Features
|
|
67
88
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
89
|
+
- file-authored pages and navigation from `content/`
|
|
90
|
+
- collection definitions as the source of truth for content shape and source
|
|
91
|
+
files
|
|
92
|
+
- Markdown, MDC, YAML, JSON, and CSV ingestion
|
|
93
|
+
- locale-aware content routing
|
|
94
|
+
- route-aware page loading with `useContentPage(handle)`
|
|
95
|
+
- server reads through `one`, `many`, `paginate`, `resolveOne`, `tree`, and
|
|
96
|
+
`neighbors`
|
|
97
|
+
- Vue composables for the same read model
|
|
98
|
+
- search helpers for MiniSearch, Pagefind, and provider-owned search
|
|
99
|
+
- sitemap integration for public content routes
|
|
100
|
+
- a server-side provider contract for advanced custom sources
|
|
71
101
|
|
|
72
|
-
|
|
73
|
-
type: 'page',
|
|
74
|
-
source: ['index.md', 'guide/**/*.md'],
|
|
75
|
-
schema: z.object({
|
|
76
|
-
title: fields.text()
|
|
77
|
-
})
|
|
78
|
-
})
|
|
102
|
+
## Scope
|
|
79
103
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
pages
|
|
83
|
-
}
|
|
84
|
-
})
|
|
85
|
-
```
|
|
104
|
+
The default provider reads files from your Nuxt project. The package does not
|
|
105
|
+
include a CMS UI, Studio, admin panel, or content editing workflow.
|
|
86
106
|
|
|
87
107
|
## Docs
|
|
88
108
|
|
|
89
109
|
- Documentation: [ginko-content.nuxt.dev](https://ginko-content.nuxt.dev)
|
|
90
110
|
- Repository: [github.com/lupinum-dev/ginko-content](https://github.com/lupinum-dev/ginko-content)
|
|
111
|
+
|
|
112
|
+
## Credits
|
|
113
|
+
|
|
114
|
+
Ginko Content is its own implementation, with clear inspiration from
|
|
115
|
+
[Nuxt Content](https://content.nuxt.com/), [Nuxt UI](https://ui.nuxt.com/), and
|
|
116
|
+
[Comark](https://comark.dev/), the successor to the previous MDC work.
|
|
117
|
+
|
|
118
|
+
## License
|
|
119
|
+
|
|
120
|
+
[MIT](./LICENSE)
|
package/compatibility.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "Ginko Content release compatibility",
|
|
3
3
|
"releaseStack": {
|
|
4
|
-
"@lupinum/ginko-content": "0.1.
|
|
5
|
-
"@lupinum/ginko-cms": "0.1.
|
|
6
|
-
"@lupinum/ginko-cms-convex": "0.1.
|
|
7
|
-
"@lupinum/ginko-cms-contract": "0.1.
|
|
4
|
+
"@lupinum/ginko-content": "0.1.1",
|
|
5
|
+
"@lupinum/ginko-cms": "0.1.2",
|
|
6
|
+
"@lupinum/ginko-cms-convex": "0.1.1",
|
|
7
|
+
"@lupinum/ginko-cms-contract": "0.1.1"
|
|
8
8
|
},
|
|
9
9
|
"tracked": {
|
|
10
10
|
"@convex-dev/better-auth": ["^0.12.2", "0.12.2"],
|
package/dist/cli.mjs
CHANGED
|
@@ -99,6 +99,93 @@ const lockfileNames = ["pnpm-lock.yaml", "package-lock.json", "yarn.lock", "bun.
|
|
|
99
99
|
const localeCodePattern = /^[a-z]{2}(?:-[A-Z]{2})?$/;
|
|
100
100
|
const toRelativePath = (rootDir, file) => relative(rootDir, file) || ".";
|
|
101
101
|
const countSitemapUrls = (text) => (text.match(/<url>/g) || []).length;
|
|
102
|
+
function findMatchingBrace(text, start) {
|
|
103
|
+
let depth = 0;
|
|
104
|
+
let quote;
|
|
105
|
+
let escaped = false;
|
|
106
|
+
let lineComment = false;
|
|
107
|
+
let blockComment = false;
|
|
108
|
+
for (let index = start; index < text.length; index++) {
|
|
109
|
+
const char = text[index];
|
|
110
|
+
const next = text[index + 1];
|
|
111
|
+
if (lineComment) {
|
|
112
|
+
if (char === "\n") {
|
|
113
|
+
lineComment = false;
|
|
114
|
+
}
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
if (blockComment) {
|
|
118
|
+
if (char === "*" && next === "/") {
|
|
119
|
+
blockComment = false;
|
|
120
|
+
index++;
|
|
121
|
+
}
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
if (quote) {
|
|
125
|
+
if (escaped) {
|
|
126
|
+
escaped = false;
|
|
127
|
+
} else if (char === "\\") {
|
|
128
|
+
escaped = true;
|
|
129
|
+
} else if (char === quote) {
|
|
130
|
+
quote = void 0;
|
|
131
|
+
}
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
if (char === "/" && next === "/") {
|
|
135
|
+
lineComment = true;
|
|
136
|
+
index++;
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
if (char === "/" && next === "*") {
|
|
140
|
+
blockComment = true;
|
|
141
|
+
index++;
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
if (char === "'" || char === '"' || char === "`") {
|
|
145
|
+
quote = char;
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
if (char === "{") {
|
|
149
|
+
depth++;
|
|
150
|
+
} else if (char === "}") {
|
|
151
|
+
depth--;
|
|
152
|
+
if (depth === 0) {
|
|
153
|
+
return index;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return text.length - 1;
|
|
158
|
+
}
|
|
159
|
+
function findCollectionDefinitions(text) {
|
|
160
|
+
const definitions = [];
|
|
161
|
+
const callPattern = /\bdefineCollection\s*\(/g;
|
|
162
|
+
for (const match of text.matchAll(callPattern)) {
|
|
163
|
+
const callStart = match.index || 0;
|
|
164
|
+
const argsStart = callStart + match[0].length;
|
|
165
|
+
const args = text.slice(argsStart);
|
|
166
|
+
const namedMatch = args.match(/^\s*(['"])([^'"]+)\1\s*,\s*\{/);
|
|
167
|
+
if (namedMatch) {
|
|
168
|
+
const bodyStart = argsStart + namedMatch[0].lastIndexOf("{");
|
|
169
|
+
const bodyEnd = findMatchingBrace(text, bodyStart);
|
|
170
|
+
definitions.push({
|
|
171
|
+
name: namedMatch[2],
|
|
172
|
+
block: text.slice(bodyStart, bodyEnd + 1)
|
|
173
|
+
});
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
const objectMatch = args.match(/^\s*\{/);
|
|
177
|
+
const propertyMatch = text.slice(0, callStart).match(/([a-z_$][\w$]*)\s*:\s*$/i);
|
|
178
|
+
if (objectMatch && propertyMatch) {
|
|
179
|
+
const bodyStart = argsStart + objectMatch[0].lastIndexOf("{");
|
|
180
|
+
const bodyEnd = findMatchingBrace(text, bodyStart);
|
|
181
|
+
definitions.push({
|
|
182
|
+
name: propertyMatch[1],
|
|
183
|
+
block: text.slice(bodyStart, bodyEnd + 1)
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return definitions;
|
|
188
|
+
}
|
|
102
189
|
async function collectFiles(dir, rootDir, files = []) {
|
|
103
190
|
const entries = await readdir(dir, { withFileTypes: true });
|
|
104
191
|
for (const entry of entries) {
|
|
@@ -372,25 +459,21 @@ async function inspectI18nCollections(rootDir) {
|
|
|
372
459
|
suggestion: "Declare i18n collections with defineContentConfig({ collections })."
|
|
373
460
|
}];
|
|
374
461
|
}
|
|
375
|
-
const
|
|
376
|
-
if (!
|
|
462
|
+
const collections = findCollectionDefinitions(text);
|
|
463
|
+
if (!collections.length) {
|
|
377
464
|
return [];
|
|
378
465
|
}
|
|
379
466
|
const findings = [];
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
const start = match.index || 0;
|
|
383
|
-
const end = matches[index + 1]?.index || text.length;
|
|
384
|
-
const collectionBlock = text.slice(start, end);
|
|
385
|
-
if (!/\bi18n\s*:\s*(true|\{)/.test(collectionBlock)) {
|
|
467
|
+
for (const collection of collections) {
|
|
468
|
+
if (!/\bi18n\s*:\s*(true|\{)/.test(collection.block)) {
|
|
386
469
|
findings.push({
|
|
387
470
|
severity: "error",
|
|
388
471
|
file: "content.config.ts",
|
|
389
|
-
message: `Collection "${
|
|
390
|
-
suggestion: `Add i18n: true to the ${
|
|
472
|
+
message: `Collection "${collection.name}" is not marked as i18n-aware.`,
|
|
473
|
+
suggestion: `Add i18n: true to the ${collection.name} collection or provide collection-level i18n locales.`
|
|
391
474
|
});
|
|
392
475
|
}
|
|
393
|
-
}
|
|
476
|
+
}
|
|
394
477
|
return findings;
|
|
395
478
|
}
|
|
396
479
|
async function inspectI18nContentFolders(rootDir, locales) {
|
|
@@ -13,14 +13,13 @@
|
|
|
13
13
|
*
|
|
14
14
|
* The context is attached to `event.context.__contentRuntime` and created
|
|
15
15
|
* lazily on first access. Memoization uses `memoizeRuntimeValue` so the
|
|
16
|
-
*
|
|
16
|
+
* expensive per-request values are shared across helpers within a single
|
|
17
17
|
* request but torn down with the event.
|
|
18
18
|
*/
|
|
19
19
|
import type { H3Event } from 'h3';
|
|
20
20
|
import type { Storage } from 'unstorage';
|
|
21
21
|
import type { ParsedContent } from '../../types/content';
|
|
22
22
|
import type { ContentContext as RuntimeContentConfig } from '../../types/module';
|
|
23
|
-
import type { ContentGraph } from '../../core/content/graph';
|
|
24
23
|
import type { ContentCacheStore } from '../../core/cache';
|
|
25
24
|
import type { ContentCacheHint } from '../../public/provider';
|
|
26
25
|
/**
|
|
@@ -41,16 +40,13 @@ export interface ContentRuntimeContext {
|
|
|
41
40
|
cacheStorage: Storage;
|
|
42
41
|
cacheParsedStorage: Storage;
|
|
43
42
|
};
|
|
44
|
-
memo:
|
|
45
|
-
contents?: ParsedContent[] | Promise<ParsedContent[]>;
|
|
46
|
-
graph?: ContentGraph | Promise<ContentGraph>;
|
|
47
|
-
};
|
|
43
|
+
memo: Record<string, unknown | Promise<unknown> | undefined>;
|
|
48
44
|
caches?: ContentCacheStore<ParsedContent>;
|
|
49
45
|
cacheHint?: ContentCacheHint | false;
|
|
50
46
|
}
|
|
51
47
|
export declare const getContentRuntimeContext: (event: H3Event) => ContentRuntimeContext;
|
|
52
48
|
/**
|
|
53
|
-
* Memoize an expensive per-request value
|
|
49
|
+
* Memoize an expensive per-request value.
|
|
54
50
|
*
|
|
55
51
|
* Concurrent callers within a single request that hit the same key all await
|
|
56
52
|
* the one `create()` promise — we do not start a second compute. The cache
|
|
@@ -60,4 +56,4 @@ export declare const getContentRuntimeContext: (event: H3Event) => ContentRuntim
|
|
|
60
56
|
* you cannot use this helper — mutate `runtime.memo[key]` directly. In
|
|
61
57
|
* practice, nothing we build mid-request invalidates the graph.
|
|
62
58
|
*/
|
|
63
|
-
export declare const memoizeRuntimeValue: <T>(event: H3Event, key:
|
|
59
|
+
export declare const memoizeRuntimeValue: <T>(event: H3Event, key: string, create: () => Promise<T>) => Promise<T>;
|
package/dist/module.d.mts
CHANGED
|
@@ -141,8 +141,7 @@ interface ContentCollectionConfig<TSchema extends ZodType | undefined = ZodType
|
|
|
141
141
|
cms?: ContentCmsCollectionConfig;
|
|
142
142
|
}
|
|
143
143
|
type ContentCollectionKind = 'page' | 'data';
|
|
144
|
-
type
|
|
145
|
-
type ContentProviderName = BuiltinContentProviderName | (string & {});
|
|
144
|
+
type ContentProviderName = 'filesystem' | (string & {});
|
|
146
145
|
type DefineCollectionOptions<TSchema extends ZodType | undefined = ZodType | undefined> = Omit<ContentCollectionConfig<TSchema>, 'source' | 'exclude'>;
|
|
147
146
|
interface DefineCollectionObject<TSchema extends ZodType | undefined = ZodType | undefined> extends DefineCollectionOptions<TSchema> {
|
|
148
147
|
/**
|
|
@@ -162,14 +161,13 @@ interface DefineCollectionObject<TSchema extends ZodType | undefined = ZodType |
|
|
|
162
161
|
*/
|
|
163
162
|
interface ContentConfig<TCollections extends Record<string, ContentCollectionConfig> = Record<string, ContentCollectionConfig>> {
|
|
164
163
|
/**
|
|
165
|
-
* Content backing implementation. `filesystem` is the default.
|
|
166
|
-
*
|
|
167
|
-
* `@lupinum/ginko-cms` is installed.
|
|
164
|
+
* Content backing implementation. `filesystem` is the default. Provider
|
|
165
|
+
* modules can register named implementations, for example `cms`.
|
|
168
166
|
*/
|
|
169
167
|
provider?: ContentProviderName;
|
|
170
168
|
/**
|
|
171
|
-
* External provider modules keyed by provider name.
|
|
172
|
-
*
|
|
169
|
+
* External provider modules keyed by provider name. First-party provider
|
|
170
|
+
* modules register themselves, so app configs usually do not need this.
|
|
173
171
|
*/
|
|
174
172
|
providers?: Record<string, string>;
|
|
175
173
|
/**
|
|
@@ -1012,6 +1010,7 @@ interface ContentSitemapImage {
|
|
|
1012
1010
|
interface ContentSitemapEntry {
|
|
1013
1011
|
loc: string;
|
|
1014
1012
|
_sitemap?: string;
|
|
1013
|
+
lastmod?: string;
|
|
1015
1014
|
alternatives?: ContentSitemapAlternative[];
|
|
1016
1015
|
images?: ContentSitemapImage[];
|
|
1017
1016
|
}
|
|
@@ -1886,12 +1885,13 @@ interface ModuleOptions {
|
|
|
1886
1885
|
/**
|
|
1887
1886
|
* Backing implementation for public content reads.
|
|
1888
1887
|
*
|
|
1889
|
-
* `filesystem` is the default.
|
|
1890
|
-
*
|
|
1888
|
+
* `filesystem` is the default. Provider modules can register named
|
|
1889
|
+
* implementations, for example `cms`.
|
|
1891
1890
|
*/
|
|
1892
1891
|
provider?: ContentProviderName;
|
|
1893
1892
|
/**
|
|
1894
|
-
* External provider modules keyed by provider name.
|
|
1893
|
+
* External provider modules keyed by provider name. First-party provider
|
|
1894
|
+
* modules register themselves, so app configs usually do not need this.
|
|
1895
1895
|
*/
|
|
1896
1896
|
providers?: Record<string, string>;
|
|
1897
1897
|
/**
|
|
@@ -1973,7 +1973,8 @@ declare function createSearchRuntimeConfig(search: Exclude<ModuleOptions$1['sear
|
|
|
1973
1973
|
declare const _default: _nuxt_schema.NuxtModule<ModuleOptions$1, ModuleOptions$1, false>;
|
|
1974
1974
|
|
|
1975
1975
|
interface ModuleHooks {
|
|
1976
|
-
'content:
|
|
1976
|
+
'content:providers'(providers: Record<string, string>): void | Promise<void>;
|
|
1977
|
+
'content:context'(ctx: ContentContext$1): void | Promise<void>;
|
|
1977
1978
|
}
|
|
1978
1979
|
interface ModulePublicRuntimeConfig {
|
|
1979
1980
|
experimental: {
|
|
@@ -2041,4 +2042,4 @@ declare module 'nitropack' {
|
|
|
2041
2042
|
}
|
|
2042
2043
|
|
|
2043
2044
|
export { _default as default };
|
|
2044
|
-
export type { BacklinkFields, BacklinkSource, BacklinksOptions, BacklinksResult,
|
|
2045
|
+
export type { BacklinkFields, BacklinkSource, BacklinksOptions, BacklinksResult, CollectionQueryBuilder, CollectionQueryOperator, CollectionQueryValue, CollectionSchema, ContentCacheArtifact, ContentCmsCollectionConfig, ContentCmsFieldConfig, ContentCmsFieldType, ContentCmsRelationConfig, ContentCollectionConfig, ContentCollectionHandle, ContentCollectionI18nConfig, ContentCollectionI18nMap, ContentCollectionItem, ContentCollectionItemSurroundingsOptions, ContentCollectionKind, ContentCollectionMap, ContentCollectionName, ContentCollectionNavigationOptions, ContentCollectionPageOptions, ContentCollectionRouteConfig, ContentCollectionRouteMetaOptions, ContentCollectionSearchSectionsOptions, ContentCollectionSource, ContentCollectionSourceObject, ContentConfig, ContentContext, ContentI18nOptions, ContentLocaleEntry, ContentLocaleRoute, ContentManifest, ContentMiniSearchOptions, ContentNavigationItem, ContentPageResult, ContentPreviewOptions, ContentProviderName, ContentProviderSearchRequest, ContentQueryBuilder, ContentQueryBuilderParams, ContentQueryBuilderWhere, ContentQueryFetcher, ContentQueryRequest, ContentQuerySortFields, ContentQuerySortOptions, ContentQuerySortParams, ContentReferenceSchema, ContentResolvedMeta, ContentRevalidateOptions, ContentRouteMeta, ContentSearchEngine, ContentSearchIndexRecord, ContentSearchOptions, ContentSearchPublicRuntimeConfig, ContentSearchResult, ContentSearchSection, ContentSelector, ContentSeoImage, ContentSeoMeta, ContentSitemapAlternative, ContentSitemapAssertOptions, ContentSitemapAssertSitemapOptions, ContentSitemapEntry, ContentSitemapImage, ContentSitemapOptions, ContentTransformer, ContentTreeItem, ContentVariant, DefineCollectionObject, DefineCollectionOptions, DocumentFromHandle, LocaleFallback, LocalePathEntry, LocalizedDoc, ManifestVariant, ManyOptions, MarkdownNode, MarkdownOptions, MarkdownParsedContent, MarkdownPluginDescriptor, MarkdownPluginOptions, MarkdownRoot, ModuleHooks, ModuleOptions, MountOptions, NavItem, NeighborsOptions, NeighborsResult, OneOptions, PaginationOptions, PaginationResult, ParseContentOptions, ParsedContent, ParsedContentInternalMeta, ParsedContentMeta, PopulateFromOptions, PopulateSpec, PopulatedDocument, QueryGroupBuilder, QueryGroupFunction, QueryMatchOperator, QueryOperators, QueryOrderDirection, QueryOrderOptions, QueryWhere, ResolutionEnvelope, ResolveContentReferenceOptions, ResolveOneOptions, ResolveOneResult, ResolvedContentI18nOptions, ResolvedMarkdownPlugin, ResolvedVariant, SortDirection, SortSpec, StrictParsedContent, StrictParsedContentMeta, Toc, TocLink, TransformContentOptions, TransformContentSource, TreeOptions, VariantsOptions };
|
package/dist/module.json
CHANGED
package/dist/module.mjs
CHANGED
|
@@ -18,6 +18,7 @@ import { genSafeVariableName, genImport } from 'knitwork';
|
|
|
18
18
|
import { listen } from 'listhen';
|
|
19
19
|
import { makeIgnored } from '../dist/core/content/ignore';
|
|
20
20
|
import { readFile, readdir } from 'node:fs/promises';
|
|
21
|
+
import { resolveContentSitemapSource, GINKO_SITEMAP_SOURCE_NAME } from '../dist/runtime/utils/sitemap-source';
|
|
21
22
|
import { resolveCollectionI18nConfig } from '../dist/features/localization/config';
|
|
22
23
|
import { globby } from 'globby';
|
|
23
24
|
import { transformContent } from '../dist/parsers/index';
|
|
@@ -29,7 +30,7 @@ import { resolveCollection } from '../dist/core/content/collection';
|
|
|
29
30
|
export { defineCollection, defineContentConfig, reference } from '../dist/types/config';
|
|
30
31
|
|
|
31
32
|
const name = "@lupinum/ginko-content";
|
|
32
|
-
const version = "0.1.
|
|
33
|
+
const version = "0.1.1";
|
|
33
34
|
|
|
34
35
|
const logger = consola.withTag("@lupinum/ginko-content");
|
|
35
36
|
const CACHE_VERSION = 3;
|
|
@@ -643,16 +644,37 @@ function hasNuxtI18nModule(modules = []) {
|
|
|
643
644
|
function hasNuxtSitemapModule(modules = []) {
|
|
644
645
|
return hasNuxtModule(modules, "@nuxtjs/sitemap");
|
|
645
646
|
}
|
|
647
|
+
function configureNuxtSitemapSource(nuxt, apiBaseURL, sitemapPath = "/sitemap") {
|
|
648
|
+
if (!hasNuxtSitemapModule(nuxt.options.modules)) {
|
|
649
|
+
return;
|
|
650
|
+
}
|
|
651
|
+
const source = {
|
|
652
|
+
context: {
|
|
653
|
+
name: GINKO_SITEMAP_SOURCE_NAME
|
|
654
|
+
},
|
|
655
|
+
fetch: resolveContentSitemapSource(apiBaseURL, sitemapPath)
|
|
656
|
+
};
|
|
657
|
+
const sitemap = nuxt.options.sitemap ??= {};
|
|
658
|
+
const sources = Array.isArray(sitemap.sources) ? sitemap.sources : [];
|
|
659
|
+
sitemap.sources = [
|
|
660
|
+
...sources.filter((item) => {
|
|
661
|
+
if (typeof item === "string") return item !== source.fetch;
|
|
662
|
+
return item?.fetch !== source.fetch && item?.context?.name !== GINKO_SITEMAP_SOURCE_NAME;
|
|
663
|
+
}),
|
|
664
|
+
source
|
|
665
|
+
];
|
|
666
|
+
sitemap.excludeAppSources = true;
|
|
667
|
+
}
|
|
646
668
|
function resolveNuxtSitemapPrerenderRoutes(nuxt) {
|
|
647
669
|
if (!hasNuxtSitemapModule(nuxt.options.modules)) {
|
|
648
670
|
return [];
|
|
649
671
|
}
|
|
650
672
|
const nuxtI18n = nuxt.options.i18n || {};
|
|
651
673
|
if (!hasNuxtI18nModule(nuxt.options.modules) || !Array.isArray(nuxtI18n.locales) || nuxtI18n.locales.length === 0) {
|
|
652
|
-
return [];
|
|
674
|
+
return ["/sitemap.xml"];
|
|
653
675
|
}
|
|
654
676
|
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]));
|
|
677
|
+
return Array.from(/* @__PURE__ */ new Set(["/sitemap.xml", "/sitemap_index.xml", ...childRoutes]));
|
|
656
678
|
}
|
|
657
679
|
function resolveModuleI18nOptions(options, nuxt) {
|
|
658
680
|
if (options.i18n === false) {
|
|
@@ -1018,9 +1040,7 @@ const collectSitemapCollectionRouteCounts = async (rootDir, contentContext) => {
|
|
|
1018
1040
|
return counts;
|
|
1019
1041
|
};
|
|
1020
1042
|
const registerContentNitroIntegrationHooks = (nitroConfig, options, contentContext) => {
|
|
1021
|
-
|
|
1022
|
-
return;
|
|
1023
|
-
}
|
|
1043
|
+
const usesFilesystemProvider = !contentContext.provider || contentContext.provider === "filesystem";
|
|
1024
1044
|
nitroConfig.hooks ||= {};
|
|
1025
1045
|
if (contentContext.sitemap?.assert?.enabled) {
|
|
1026
1046
|
appendHook(nitroConfig.hooks, "compiled", async (nitro) => {
|
|
@@ -1032,7 +1052,7 @@ const registerContentNitroIntegrationHooks = (nitroConfig, options, contentConte
|
|
|
1032
1052
|
await assertGeneratedSitemaps({
|
|
1033
1053
|
outputPublicDir: nitro.options.output.publicDir,
|
|
1034
1054
|
options: assertOptions,
|
|
1035
|
-
collectionRouteCounts: await collectSitemapCollectionRouteCounts(options.rootDir, contentContext),
|
|
1055
|
+
collectionRouteCounts: usesFilesystemProvider ? await collectSitemapCollectionRouteCounts(options.rootDir, contentContext) : {},
|
|
1036
1056
|
logger: nitro.logger
|
|
1037
1057
|
});
|
|
1038
1058
|
} catch (error) {
|
|
@@ -1044,8 +1064,10 @@ const registerContentNitroIntegrationHooks = (nitroConfig, options, contentConte
|
|
|
1044
1064
|
});
|
|
1045
1065
|
}
|
|
1046
1066
|
appendHook(nitroConfig.hooks, "prerender:routes", async (routes) => {
|
|
1047
|
-
|
|
1048
|
-
|
|
1067
|
+
if (usesFilesystemProvider) {
|
|
1068
|
+
for (const route of await collectPrerenderRoutes(options.rootDir, contentContext)) {
|
|
1069
|
+
routes.add(route);
|
|
1070
|
+
}
|
|
1049
1071
|
}
|
|
1050
1072
|
for (const route of options.sitemapPrerenderRoutes || []) {
|
|
1051
1073
|
routes.add(route);
|
|
@@ -1170,10 +1192,11 @@ const module$1 = defineNuxtModule({
|
|
|
1170
1192
|
}
|
|
1171
1193
|
]));
|
|
1172
1194
|
options.provider = appContentConfig.provider || options.provider || "filesystem";
|
|
1173
|
-
|
|
1195
|
+
const providerRegistry = {
|
|
1174
1196
|
...options.providers || {},
|
|
1175
1197
|
...appContentConfig.providers || {}
|
|
1176
1198
|
};
|
|
1199
|
+
options.providers = providerRegistry;
|
|
1177
1200
|
const buildIntegrity = nuxt.options.dev ? void 0 : Date.now();
|
|
1178
1201
|
const contentContext = {
|
|
1179
1202
|
...options,
|
|
@@ -1186,6 +1209,9 @@ const module$1 = defineNuxtModule({
|
|
|
1186
1209
|
sitemap: resolvedSitemap,
|
|
1187
1210
|
search: resolvedSearch
|
|
1188
1211
|
};
|
|
1212
|
+
if (resolvedSitemap !== false) {
|
|
1213
|
+
configureNuxtSitemapSource(nuxt, options.api.baseURL, resolvedSitemap.path);
|
|
1214
|
+
}
|
|
1189
1215
|
if (resolvedSitemap && resolvedSitemap.assert.enabled) {
|
|
1190
1216
|
nuxt.hook("sitemap:prerender:done", async ({ sitemaps }) => {
|
|
1191
1217
|
const assertOptions = contentContext.sitemap?.assert;
|
|
@@ -1310,6 +1336,8 @@ const module$1 = defineNuxtModule({
|
|
|
1310
1336
|
});
|
|
1311
1337
|
}
|
|
1312
1338
|
nuxt.hook("modules:done", async () => {
|
|
1339
|
+
await nuxt.callHook("content:providers", contentContext.providers ||= {});
|
|
1340
|
+
assertConfiguredProviderAvailable(contentContext);
|
|
1313
1341
|
await nuxt.callHook("content:context", contentContext);
|
|
1314
1342
|
if (contentContext.search !== false) {
|
|
1315
1343
|
registerContentSearchServerHandlers(options.api.baseURL, contentContext.search, resolveRuntimeModule);
|
|
@@ -1348,6 +1376,15 @@ const module$1 = defineNuxtModule({
|
|
|
1348
1376
|
}
|
|
1349
1377
|
}
|
|
1350
1378
|
});
|
|
1379
|
+
function assertConfiguredProviderAvailable(contentContext) {
|
|
1380
|
+
const provider = contentContext.provider || "filesystem";
|
|
1381
|
+
if (provider === "filesystem") return;
|
|
1382
|
+
if (contentContext.providers?.[provider]) return;
|
|
1383
|
+
if (provider === "cms") {
|
|
1384
|
+
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".');
|
|
1385
|
+
}
|
|
1386
|
+
throw new Error(`content.config.ts sets provider "${provider}", but no provider module registered it. Register a module for "${provider}" or add it to content providers.`);
|
|
1387
|
+
}
|
|
1351
1388
|
function validateRemovedMarkdownOptions(options) {
|
|
1352
1389
|
if (options.highlight !== void 0) {
|
|
1353
1390
|
throw new Error("`content.highlight` was removed. Enable syntax highlighting with `content.markdown.plugins`, for example `[['highlight', { ...options }]]`.");
|
|
@@ -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,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"
|
|
@@ -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
|
+
}
|
package/dist/types/config.d.ts
CHANGED
|
@@ -132,8 +132,7 @@ export interface ContentCollectionConfig<TSchema extends ZodType | undefined = Z
|
|
|
132
132
|
cms?: ContentCmsCollectionConfig;
|
|
133
133
|
}
|
|
134
134
|
export type ContentCollectionKind = 'page' | 'data';
|
|
135
|
-
export type
|
|
136
|
-
export type ContentProviderName = BuiltinContentProviderName | (string & {});
|
|
135
|
+
export type ContentProviderName = 'filesystem' | (string & {});
|
|
137
136
|
export type DefineCollectionOptions<TSchema extends ZodType | undefined = ZodType | undefined> = Omit<ContentCollectionConfig<TSchema>, 'source' | 'exclude'>;
|
|
138
137
|
export interface DefineCollectionObject<TSchema extends ZodType | undefined = ZodType | undefined> extends DefineCollectionOptions<TSchema> {
|
|
139
138
|
/**
|
|
@@ -153,14 +152,13 @@ export interface DefineCollectionObject<TSchema extends ZodType | undefined = Zo
|
|
|
153
152
|
*/
|
|
154
153
|
export interface ContentConfig<TCollections extends Record<string, ContentCollectionConfig> = Record<string, ContentCollectionConfig>> {
|
|
155
154
|
/**
|
|
156
|
-
* Content backing implementation. `filesystem` is the default.
|
|
157
|
-
*
|
|
158
|
-
* `@lupinum/ginko-cms` is installed.
|
|
155
|
+
* Content backing implementation. `filesystem` is the default. Provider
|
|
156
|
+
* modules can register named implementations, for example `cms`.
|
|
159
157
|
*/
|
|
160
158
|
provider?: ContentProviderName;
|
|
161
159
|
/**
|
|
162
|
-
* External provider modules keyed by provider name.
|
|
163
|
-
*
|
|
160
|
+
* External provider modules keyed by provider name. First-party provider
|
|
161
|
+
* modules register themselves, so app configs usually do not need this.
|
|
164
162
|
*/
|
|
165
163
|
providers?: Record<string, string>;
|
|
166
164
|
/**
|
|
@@ -245,13 +243,13 @@ export declare function defineCollection<const Name extends string, const TConfi
|
|
|
245
243
|
* ```ts
|
|
246
244
|
* import { defineCollection, defineContentConfig } from '@lupinum/ginko-content/config'
|
|
247
245
|
*
|
|
246
|
+
* export const docs = defineCollection('docs', {
|
|
247
|
+
* type: 'page',
|
|
248
|
+
* source: 'docs/*.md'
|
|
249
|
+
* })
|
|
250
|
+
*
|
|
248
251
|
* export default defineContentConfig({
|
|
249
|
-
* collections: {
|
|
250
|
-
* docs: defineCollection({
|
|
251
|
-
* type: 'page',
|
|
252
|
-
* source: 'docs/*.md'
|
|
253
|
-
* })
|
|
254
|
-
* }
|
|
252
|
+
* collections: { docs }
|
|
255
253
|
* })
|
|
256
254
|
* ```
|
|
257
255
|
*/
|
|
@@ -265,9 +263,9 @@ export declare function defineContentConfig<TCollections extends Record<string,
|
|
|
265
263
|
* @example
|
|
266
264
|
* ```ts
|
|
267
265
|
* import { z } from 'zod'
|
|
268
|
-
* import { defineCollection, reference } from '@lupinum/ginko-content/config'
|
|
266
|
+
* import { defineCollection, defineContentConfig, reference } from '@lupinum/ginko-content/config'
|
|
269
267
|
*
|
|
270
|
-
* export
|
|
268
|
+
* export const blog = defineCollection('blog', {
|
|
271
269
|
* type: 'page',
|
|
272
270
|
* source: 'blog/*.md',
|
|
273
271
|
* schema: z.object({
|
|
@@ -275,6 +273,10 @@ export declare function defineContentConfig<TCollections extends Record<string,
|
|
|
275
273
|
* related: z.array(reference('blog')).default([])
|
|
276
274
|
* })
|
|
277
275
|
* })
|
|
276
|
+
*
|
|
277
|
+
* export default defineContentConfig({
|
|
278
|
+
* collections: { blog }
|
|
279
|
+
* })
|
|
278
280
|
* ```
|
|
279
281
|
*/
|
|
280
282
|
export declare function reference(collection?: string): ContentReferenceSchema;
|
package/dist/types/module.d.ts
CHANGED
|
@@ -386,12 +386,13 @@ export interface ModuleOptions {
|
|
|
386
386
|
/**
|
|
387
387
|
* Backing implementation for public content reads.
|
|
388
388
|
*
|
|
389
|
-
* `filesystem` is the default.
|
|
390
|
-
*
|
|
389
|
+
* `filesystem` is the default. Provider modules can register named
|
|
390
|
+
* implementations, for example `cms`.
|
|
391
391
|
*/
|
|
392
392
|
provider?: ContentProviderName;
|
|
393
393
|
/**
|
|
394
|
-
* External provider modules keyed by provider name.
|
|
394
|
+
* External provider modules keyed by provider name. First-party provider
|
|
395
|
+
* modules register themselves, so app configs usually do not need this.
|
|
395
396
|
*/
|
|
396
397
|
providers?: Record<string, string>;
|
|
397
398
|
/**
|
package/dist/types/query.d.ts
CHANGED
package/dist/types.d.mts
CHANGED
|
@@ -8,4 +8,4 @@ export { type defineCollection, type defineContentConfig, type reference } from
|
|
|
8
8
|
|
|
9
9
|
export { default } from './module.mjs'
|
|
10
10
|
|
|
11
|
-
export { type BacklinkFields, type BacklinkSource, type BacklinksOptions, type BacklinksResult, type
|
|
11
|
+
export { type BacklinkFields, type BacklinkSource, type BacklinksOptions, type BacklinksResult, type CollectionQueryBuilder, type CollectionQueryOperator, type CollectionQueryValue, type CollectionSchema, type ContentCacheArtifact, type ContentCmsCollectionConfig, type ContentCmsFieldConfig, type ContentCmsFieldType, type ContentCmsRelationConfig, type ContentCollectionConfig, type ContentCollectionHandle, type ContentCollectionI18nConfig, type ContentCollectionI18nMap, type ContentCollectionItem, type ContentCollectionItemSurroundingsOptions, type ContentCollectionKind, type ContentCollectionMap, type ContentCollectionName, type ContentCollectionNavigationOptions, type ContentCollectionPageOptions, type ContentCollectionRouteConfig, type ContentCollectionRouteMetaOptions, type ContentCollectionSearchSectionsOptions, type ContentCollectionSource, type ContentCollectionSourceObject, type ContentConfig, type ContentContext, type ContentI18nOptions, type ContentLocaleEntry, type ContentLocaleRoute, type ContentManifest, type ContentMiniSearchOptions, type ContentNavigationItem, type ContentPageResult, type ContentPreviewOptions, type ContentProviderName, type ContentProviderSearchRequest, type ContentQueryBuilder, type ContentQueryBuilderParams, type ContentQueryBuilderWhere, type ContentQueryFetcher, type ContentQueryRequest, type ContentQuerySortFields, type ContentQuerySortOptions, type ContentQuerySortParams, type ContentReferenceSchema, type ContentResolvedMeta, type ContentRevalidateOptions, type ContentRouteMeta, type ContentSearchEngine, type ContentSearchIndexRecord, type ContentSearchOptions, type ContentSearchPublicRuntimeConfig, type ContentSearchResult, type ContentSearchSection, type ContentSelector, type ContentSeoImage, type ContentSeoMeta, type ContentSitemapAlternative, type ContentSitemapAssertOptions, type ContentSitemapAssertSitemapOptions, type ContentSitemapEntry, type ContentSitemapImage, type ContentSitemapOptions, type ContentTransformer, type ContentTreeItem, type ContentVariant, type DefineCollectionObject, type DefineCollectionOptions, type DocumentFromHandle, type LocaleFallback, type LocalePathEntry, type LocalizedDoc, type ManifestVariant, type ManyOptions, type MarkdownNode, type MarkdownOptions, type MarkdownParsedContent, type MarkdownPluginDescriptor, type MarkdownPluginOptions, type MarkdownRoot, type ModuleHooks, type ModuleOptions, type MountOptions, type NavItem, type NeighborsOptions, type NeighborsResult, type OneOptions, type PaginationOptions, type PaginationResult, type ParseContentOptions, type ParsedContent, type ParsedContentInternalMeta, type ParsedContentMeta, type PopulateFromOptions, type PopulateSpec, type PopulatedDocument, type QueryGroupBuilder, type QueryGroupFunction, type QueryMatchOperator, type QueryOperators, type QueryOrderDirection, type QueryOrderOptions, type QueryWhere, type ResolutionEnvelope, type ResolveContentReferenceOptions, type ResolveOneOptions, type ResolveOneResult, type ResolvedContentI18nOptions, type ResolvedMarkdownPlugin, type ResolvedVariant, type SortDirection, type SortSpec, type StrictParsedContent, type StrictParsedContentMeta, type Toc, type TocLink, type TransformContentOptions, type TransformContentSource, type TreeOptions, type VariantsOptions } from './module.mjs'
|
package/dist/web-types.json
CHANGED