@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/README.md
CHANGED
|
@@ -1,61 +1,80 @@
|
|
|
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({
|
|
49
|
+
type: 'page',
|
|
50
|
+
source: '**/*.md'
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
export default defineContentConfig({
|
|
54
|
+
collections: {
|
|
55
|
+
pages
|
|
56
|
+
}
|
|
57
|
+
})
|
|
58
|
+
```
|
|
38
59
|
|
|
39
|
-
|
|
40
|
-
runtime-neutral seams for package integration, not a bundled CMS runtime.
|
|
60
|
+
Create `content/index.md`:
|
|
41
61
|
|
|
42
|
-
|
|
62
|
+
```md
|
|
63
|
+
---
|
|
64
|
+
title: Welcome
|
|
65
|
+
---
|
|
43
66
|
|
|
44
|
-
|
|
67
|
+
# Welcome
|
|
45
68
|
|
|
46
|
-
|
|
47
|
-
content/
|
|
48
|
-
index.md
|
|
49
|
-
guide/getting-started.md
|
|
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
|
-
|
|
57
|
-
|
|
58
|
-
const { page } = await useContentPage(pages)
|
|
77
|
+
const { page } = await useContentPage('pages')
|
|
59
78
|
</script>
|
|
60
79
|
|
|
61
80
|
<template>
|
|
@@ -63,28 +82,55 @@ const { page } = await useContentPage(pages)
|
|
|
63
82
|
</template>
|
|
64
83
|
```
|
|
65
84
|
|
|
66
|
-
|
|
85
|
+
## Features
|
|
67
86
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
87
|
+
- file-authored pages and navigation from `content/`
|
|
88
|
+
- collection definitions as the source of truth for content shape and source
|
|
89
|
+
files
|
|
90
|
+
- Markdown, Comark component tags, YAML, JSON, and CSV ingestion
|
|
91
|
+
- locale-aware content routing
|
|
92
|
+
- route-aware page loading with `useContentPage('pages')`
|
|
93
|
+
- semantic previous/next route-page data through `useContentPage('pages', { surround })`
|
|
94
|
+
- server reads through `one`, `many`, `paginate`, `resolveOne`, `tree`, and
|
|
95
|
+
`neighbors`
|
|
96
|
+
- Vue composables for the same read model
|
|
97
|
+
- search helpers for MiniSearch, Pagefind, and provider-owned search
|
|
98
|
+
- sitemap integration for public content routes
|
|
99
|
+
- a server-side provider contract for advanced custom sources
|
|
71
100
|
|
|
72
|
-
|
|
73
|
-
type: 'page',
|
|
74
|
-
source: ['index.md', 'guide/**/*.md'],
|
|
75
|
-
schema: z.object({
|
|
76
|
-
title: fields.text()
|
|
77
|
-
})
|
|
78
|
-
})
|
|
101
|
+
## I18n, Sitemap, and Prerender Ownership
|
|
79
102
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
103
|
+
For localized Nuxt apps, keep one source of truth per route type:
|
|
104
|
+
|
|
105
|
+
- Static Nuxt page paths belong in Nuxt I18n `i18n.pages`.
|
|
106
|
+
- Content page paths belong in Ginko collection routes and content files.
|
|
107
|
+
- Sitemap XML is generated by `@nuxtjs/sitemap`.
|
|
108
|
+
- Ginko registers the content sitemap source and contributes content prerender
|
|
109
|
+
routes.
|
|
110
|
+
|
|
111
|
+
Do not duplicate docs, blog, pricing, privacy, or translated locale paths in
|
|
112
|
+
`sitemap.urls` or app-owned `nitro.prerender.routes` arrays. Use
|
|
113
|
+
`@nuxtjs/sitemap >= 8.0.15` when translated static app slugs such as `/preise`
|
|
114
|
+
and `/en/pricing` need cross-locale sitemap alternates.
|
|
115
|
+
|
|
116
|
+
See the public guide: [Sitemap and prerender](../../docs/content/docs/6.i18n/4.sitemap-prerender.md).
|
|
117
|
+
|
|
118
|
+
## Scope
|
|
119
|
+
|
|
120
|
+
The default provider reads files from your Nuxt project. The package does not
|
|
121
|
+
include a CMS UI, Studio, admin panel, or content editing workflow.
|
|
86
122
|
|
|
87
123
|
## Docs
|
|
88
124
|
|
|
89
125
|
- Documentation: [ginko-content.nuxt.dev](https://ginko-content.nuxt.dev)
|
|
90
126
|
- Repository: [github.com/lupinum-dev/ginko-content](https://github.com/lupinum-dev/ginko-content)
|
|
127
|
+
|
|
128
|
+
## Credits
|
|
129
|
+
|
|
130
|
+
Ginko Content is its own implementation, with clear inspiration from
|
|
131
|
+
[Nuxt Content](https://content.nuxt.com/), [Nuxt UI](https://ui.nuxt.com/), and
|
|
132
|
+
[Comark](https://comark.dev/), the successor to the previous MDC work.
|
|
133
|
+
|
|
134
|
+
## License
|
|
135
|
+
|
|
136
|
+
[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.2",
|
|
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"],
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
"convex": ["1.38.0", "^1.38.0"],
|
|
14
14
|
"convex-helpers": ["^0.1.117", "0.1.117"],
|
|
15
15
|
"h3": ["1.15.11"],
|
|
16
|
-
"nuxt": ["^4.4.
|
|
16
|
+
"nuxt": ["^4.4.7", "4.4.7", ">=4.4.7"],
|
|
17
17
|
"typescript": ["~6.0.3", "^6.0.3", "6.0.3"],
|
|
18
18
|
"vite": ["^7.3.3", "7.3.3"],
|
|
19
19
|
"vitest": ["^4.1.6", ">=4.1.6"],
|
package/dist/cli.mjs
CHANGED
|
@@ -42,32 +42,27 @@ const sourceChecks = [
|
|
|
42
42
|
{
|
|
43
43
|
pattern: /\bqueryCollectionItemSurroundings\s*\(/,
|
|
44
44
|
message: "Nuxt Content v3 surround helper found.",
|
|
45
|
-
suggestion: "Use
|
|
45
|
+
suggestion: "Use useContentPage('docs', { surround: true }) in route page components."
|
|
46
46
|
},
|
|
47
47
|
{
|
|
48
48
|
pattern: /\bqueryCollectionSearchSections\s*\(/,
|
|
49
49
|
message: "Nuxt Content v3 search sections helper found.",
|
|
50
|
-
suggestion: "Use useContentSearchData(
|
|
50
|
+
suggestion: "Use useContentSearchData('docs') for UI search data."
|
|
51
51
|
},
|
|
52
52
|
{
|
|
53
53
|
pattern: /\bqueryCollectionNavigation\s*\(/,
|
|
54
54
|
message: "Nuxt Content v3 navigation helper found.",
|
|
55
|
-
suggestion: "Use
|
|
55
|
+
suggestion: "Use useContentNavigation('docs') for layout navigation."
|
|
56
56
|
},
|
|
57
57
|
{
|
|
58
58
|
pattern: /\bqueryCollection\s*\(/,
|
|
59
59
|
message: "Removed collection query helper found.",
|
|
60
|
-
suggestion: "Use one(
|
|
60
|
+
suggestion: "Use one('docs', options), many('docs', options), paginate('docs', options), or the matching useContent* composable."
|
|
61
61
|
},
|
|
62
62
|
{
|
|
63
63
|
pattern: /\buseContentList\s*\(/,
|
|
64
64
|
message: "Removed content list composable found.",
|
|
65
|
-
suggestion: "Use useContentMany(
|
|
66
|
-
},
|
|
67
|
-
{
|
|
68
|
-
pattern: /\buseContentNavigation\s*\(/,
|
|
69
|
-
message: "Removed content navigation composable found.",
|
|
70
|
-
suggestion: "Use useContentTree(handle) for layout navigation."
|
|
65
|
+
suggestion: "Use useContentMany('docs', options) or many('docs', options)."
|
|
71
66
|
},
|
|
72
67
|
{
|
|
73
68
|
pattern: /\bcontent\.(database|preview|build)\b/,
|
|
@@ -84,21 +79,158 @@ const sourceChecks = [
|
|
|
84
79
|
message: "ContentRenderer is receiving a document body.",
|
|
85
80
|
suggestion: "Pass the full content document to ContentRenderer, not document.body."
|
|
86
81
|
},
|
|
87
|
-
{
|
|
88
|
-
pattern: /<NuxtLink\b[^>]*:to\s*=\s*["'][^"']+\.path["']/,
|
|
89
|
-
message: "NuxtLink may be using raw query .path.",
|
|
90
|
-
suggestion: "Use useContentMany(handle, options) for route-safe list items, or link route-page payloads with their explicit path field."
|
|
91
|
-
},
|
|
92
82
|
{
|
|
93
83
|
pattern: /<NuxtLink\b[^>]*:to\s*=\s*["'][^"']+\._path["']/,
|
|
94
84
|
message: "NuxtLink is using raw content _path.",
|
|
95
|
-
suggestion: "Use useContentMany(
|
|
85
|
+
suggestion: "Use useContentMany('docs', options) for list pages and bind the route-safe item.path field."
|
|
96
86
|
}
|
|
97
87
|
];
|
|
98
88
|
const lockfileNames = ["pnpm-lock.yaml", "package-lock.json", "yarn.lock", "bun.lockb"];
|
|
99
89
|
const localeCodePattern = /^[a-z]{2}(?:-[A-Z]{2})?$/;
|
|
100
90
|
const toRelativePath = (rootDir, file) => relative(rootDir, file) || ".";
|
|
101
91
|
const countSitemapUrls = (text) => (text.match(/<url>/g) || []).length;
|
|
92
|
+
function findMatchingBrace(text, start) {
|
|
93
|
+
let depth = 0;
|
|
94
|
+
let quote;
|
|
95
|
+
let escaped = false;
|
|
96
|
+
let lineComment = false;
|
|
97
|
+
let blockComment = false;
|
|
98
|
+
for (let index = start; index < text.length; index++) {
|
|
99
|
+
const char = text[index];
|
|
100
|
+
const next = text[index + 1];
|
|
101
|
+
if (lineComment) {
|
|
102
|
+
if (char === "\n") {
|
|
103
|
+
lineComment = false;
|
|
104
|
+
}
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
if (blockComment) {
|
|
108
|
+
if (char === "*" && next === "/") {
|
|
109
|
+
blockComment = false;
|
|
110
|
+
index++;
|
|
111
|
+
}
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
if (quote) {
|
|
115
|
+
if (escaped) {
|
|
116
|
+
escaped = false;
|
|
117
|
+
} else if (char === "\\") {
|
|
118
|
+
escaped = true;
|
|
119
|
+
} else if (char === quote) {
|
|
120
|
+
quote = void 0;
|
|
121
|
+
}
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
if (char === "/" && next === "/") {
|
|
125
|
+
lineComment = true;
|
|
126
|
+
index++;
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
if (char === "/" && next === "*") {
|
|
130
|
+
blockComment = true;
|
|
131
|
+
index++;
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
if (char === "'" || char === '"' || char === "`") {
|
|
135
|
+
quote = char;
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
if (char === "{") {
|
|
139
|
+
depth++;
|
|
140
|
+
} else if (char === "}") {
|
|
141
|
+
depth--;
|
|
142
|
+
if (depth === 0) {
|
|
143
|
+
return index;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return text.length - 1;
|
|
148
|
+
}
|
|
149
|
+
function findAuthoredCollectionDefinitions(text) {
|
|
150
|
+
const definitions = /* @__PURE__ */ new Map();
|
|
151
|
+
const callPattern = /\bdefineCollection\s*\(/g;
|
|
152
|
+
for (const match of text.matchAll(callPattern)) {
|
|
153
|
+
const callStart = match.index || 0;
|
|
154
|
+
const argsStart = callStart + match[0].length;
|
|
155
|
+
const args = text.slice(argsStart);
|
|
156
|
+
const objectMatch = args.match(/^\s*\{/);
|
|
157
|
+
if (!objectMatch) {
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
const bodyStart = argsStart + objectMatch[0].lastIndexOf("{");
|
|
161
|
+
const bodyEnd = findMatchingBrace(text, bodyStart);
|
|
162
|
+
const block = text.slice(bodyStart, bodyEnd + 1);
|
|
163
|
+
const prefix = text.slice(0, callStart);
|
|
164
|
+
const propertyMatch = prefix.match(/([a-z_$][\w$]*)\s*:\s*$/i);
|
|
165
|
+
if (propertyMatch) {
|
|
166
|
+
definitions.set(propertyMatch[1], {
|
|
167
|
+
key: propertyMatch[1],
|
|
168
|
+
block
|
|
169
|
+
});
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
const variableMatch = prefix.match(/(?:^|[\s;])(?:export\s+)?(?:const|let|var)\s+([a-z_$][\w$]*)\s*=\s*$/i);
|
|
173
|
+
if (variableMatch) {
|
|
174
|
+
definitions.set(variableMatch[1], { block });
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
return definitions;
|
|
178
|
+
}
|
|
179
|
+
function findCollectionDefinitions(text) {
|
|
180
|
+
const authoredCollections = findAuthoredCollectionDefinitions(text);
|
|
181
|
+
const definitions = /* @__PURE__ */ new Map();
|
|
182
|
+
for (const block of findObjectPropertyBlocks(text, "collections")) {
|
|
183
|
+
for (const match of block.matchAll(/([a-z_$][\w$]*)\s*:\s*([a-z_$][\w$]*)\b/g)) {
|
|
184
|
+
const [, key, identifier] = match;
|
|
185
|
+
const authored = authoredCollections.get(identifier);
|
|
186
|
+
if (authored) {
|
|
187
|
+
definitions.set(key, {
|
|
188
|
+
name: key,
|
|
189
|
+
block: authored.block
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
for (const match of block.matchAll(/(?:^|[,{]\s*)([a-z_$][\w$]*)\s*(?=,|\})/g)) {
|
|
194
|
+
const key = match[1];
|
|
195
|
+
const authored = authoredCollections.get(key);
|
|
196
|
+
if (authored) {
|
|
197
|
+
definitions.set(key, {
|
|
198
|
+
name: key,
|
|
199
|
+
block: authored.block
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
for (const authored of authoredCollections.values()) {
|
|
205
|
+
if (authored.key) {
|
|
206
|
+
definitions.set(authored.key, {
|
|
207
|
+
name: authored.key,
|
|
208
|
+
block: authored.block
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
return [...definitions.values()];
|
|
213
|
+
}
|
|
214
|
+
function findObjectPropertyBlocks(text, property) {
|
|
215
|
+
const blocks = [];
|
|
216
|
+
const pattern = new RegExp(`\\b${property}\\s*:\\s*\\{`, "g");
|
|
217
|
+
for (const match of text.matchAll(pattern)) {
|
|
218
|
+
const bodyStart = text.indexOf("{", match.index);
|
|
219
|
+
if (bodyStart === -1) {
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
const bodyEnd = findMatchingBrace(text, bodyStart);
|
|
223
|
+
blocks.push(text.slice(bodyStart, bodyEnd + 1));
|
|
224
|
+
}
|
|
225
|
+
return blocks;
|
|
226
|
+
}
|
|
227
|
+
function extractStringArrayProperty(block, property) {
|
|
228
|
+
const match = block.match(new RegExp(`\\b${property}\\s*:\\s*\\[([\\s\\S]*?)\\]`));
|
|
229
|
+
if (!match) {
|
|
230
|
+
return [];
|
|
231
|
+
}
|
|
232
|
+
return [...match[1].matchAll(/['"]([^'"]+)['"]/g)].map((item) => item[1]);
|
|
233
|
+
}
|
|
102
234
|
async function collectFiles(dir, rootDir, files = []) {
|
|
103
235
|
const entries = await readdir(dir, { withFileTypes: true });
|
|
104
236
|
for (const entry of entries) {
|
|
@@ -174,6 +306,32 @@ async function inspectSourceFiles(rootDir) {
|
|
|
174
306
|
}
|
|
175
307
|
return findings;
|
|
176
308
|
}
|
|
309
|
+
async function inspectSearchCollections(rootDir) {
|
|
310
|
+
const contentConfig = await readTextIfPresent(join(rootDir, "content.config.ts"));
|
|
311
|
+
const nuxtConfig = await readTextIfPresent(join(rootDir, "nuxt.config.ts"));
|
|
312
|
+
if (!contentConfig || !nuxtConfig) {
|
|
313
|
+
return [];
|
|
314
|
+
}
|
|
315
|
+
const dataCollections = new Set(
|
|
316
|
+
findCollectionDefinitions(contentConfig).filter((collection) => /\btype\s*:\s*['"]data['"]/.test(collection.block)).map((collection) => collection.name)
|
|
317
|
+
);
|
|
318
|
+
if (!dataCollections.size) {
|
|
319
|
+
return [];
|
|
320
|
+
}
|
|
321
|
+
const configuredCollections = new Set(
|
|
322
|
+
findObjectPropertyBlocks(nuxtConfig, "search").flatMap((block) => extractStringArrayProperty(block, "collections"))
|
|
323
|
+
);
|
|
324
|
+
const dataSearchCollections = [...configuredCollections].filter((collection) => dataCollections.has(collection));
|
|
325
|
+
if (!dataSearchCollections.length) {
|
|
326
|
+
return [];
|
|
327
|
+
}
|
|
328
|
+
return [{
|
|
329
|
+
severity: "info",
|
|
330
|
+
file: "nuxt.config.ts",
|
|
331
|
+
message: `Data-only collections listed in content.search.collections: ${dataSearchCollections.join(", ")}.`,
|
|
332
|
+
suggestion: "Remove data-only collections from the static public search index, make them route-backed pages, or use provider-backed search with route-safe results."
|
|
333
|
+
}];
|
|
334
|
+
}
|
|
177
335
|
async function inspectLockfiles(rootDir) {
|
|
178
336
|
const findings = [];
|
|
179
337
|
for (const lockfileName of lockfileNames) {
|
|
@@ -372,25 +530,21 @@ async function inspectI18nCollections(rootDir) {
|
|
|
372
530
|
suggestion: "Declare i18n collections with defineContentConfig({ collections })."
|
|
373
531
|
}];
|
|
374
532
|
}
|
|
375
|
-
const
|
|
376
|
-
if (!
|
|
533
|
+
const collections = findCollectionDefinitions(text);
|
|
534
|
+
if (!collections.length) {
|
|
377
535
|
return [];
|
|
378
536
|
}
|
|
379
537
|
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)) {
|
|
538
|
+
for (const collection of collections) {
|
|
539
|
+
if (!/\bi18n\s*:\s*(true|\{)/.test(collection.block)) {
|
|
386
540
|
findings.push({
|
|
387
541
|
severity: "error",
|
|
388
542
|
file: "content.config.ts",
|
|
389
|
-
message: `Collection "${
|
|
390
|
-
suggestion: `Add i18n: true to the ${
|
|
543
|
+
message: `Collection "${collection.name}" is not marked as i18n-aware.`,
|
|
544
|
+
suggestion: `Add i18n: true to the ${collection.name} collection or provide collection-level i18n locales.`
|
|
391
545
|
});
|
|
392
546
|
}
|
|
393
|
-
}
|
|
547
|
+
}
|
|
394
548
|
return findings;
|
|
395
549
|
}
|
|
396
550
|
async function inspectI18nContentFolders(rootDir, locales) {
|
|
@@ -427,7 +581,7 @@ async function inspectI18nSourceSmells(rootDir, locales) {
|
|
|
427
581
|
severity: "error",
|
|
428
582
|
file: relativeFile,
|
|
429
583
|
message: "Hardcoded locale route branch found.",
|
|
430
|
-
suggestion: "Use @nuxtjs/i18n route helpers,
|
|
584
|
+
suggestion: "Use @nuxtjs/i18n route helpers, useContentNavigation('docs'), or useContentMany('docs', options) item paths instead of branching on locale codes."
|
|
431
585
|
});
|
|
432
586
|
}
|
|
433
587
|
if (/<(?:NuxtLink|U[A-Za-z]+)\b[^>]*:to\s*=\s*["'][^"']+\._path["']/.test(text)) {
|
|
@@ -435,7 +589,7 @@ async function inspectI18nSourceSmells(rootDir, locales) {
|
|
|
435
589
|
severity: "error",
|
|
436
590
|
file: relativeFile,
|
|
437
591
|
message: "UI link is bound to raw content _path.",
|
|
438
|
-
suggestion: "Use useContentMany(
|
|
592
|
+
suggestion: "Use useContentMany('docs', options) for localized list pages and bind item.path."
|
|
439
593
|
});
|
|
440
594
|
}
|
|
441
595
|
}
|
|
@@ -620,6 +774,7 @@ async function runDoctor(options = {}) {
|
|
|
620
774
|
const findings = [
|
|
621
775
|
...await inspectPackageJson(rootDir),
|
|
622
776
|
...await inspectSourceFiles(rootDir),
|
|
777
|
+
...await inspectSearchCollections(rootDir),
|
|
623
778
|
...await inspectLockfiles(rootDir),
|
|
624
779
|
...await inspectSitemap(rootDir),
|
|
625
780
|
...options.i18n ? await inspectI18n(rootDir) : []
|
|
@@ -5,4 +5,5 @@ export declare const getObjectShape: (schema: any) => Record<string, any>;
|
|
|
5
5
|
export declare const getReferenceDescriptor: (schema: any) => {
|
|
6
6
|
collection?: string;
|
|
7
7
|
} | null;
|
|
8
|
+
export declare const collectTopLevelReferenceFieldsByTarget: (schema: unknown) => Record<string, string[]>;
|
|
8
9
|
export declare const collectTopLevelReferenceFields: (schema: unknown, targetCollection?: string) => string[];
|
|
@@ -53,18 +53,41 @@ const getArrayElement = (schema) => {
|
|
|
53
53
|
const def = getSchemaDef(current);
|
|
54
54
|
return def.element || def.type || null;
|
|
55
55
|
};
|
|
56
|
-
|
|
56
|
+
const wildcardReferenceTarget = "*";
|
|
57
|
+
const getTopLevelReferenceDescriptor = (schema) => {
|
|
58
|
+
const direct = getReferenceDescriptor(schema);
|
|
59
|
+
if (direct) {
|
|
60
|
+
return direct;
|
|
61
|
+
}
|
|
62
|
+
const arrayElement = getArrayElement(schema);
|
|
63
|
+
return arrayElement ? getReferenceDescriptor(arrayElement) : null;
|
|
64
|
+
};
|
|
65
|
+
export const collectTopLevelReferenceFieldsByTarget = (schema) => {
|
|
57
66
|
const current = unwrapSchema(schema);
|
|
58
67
|
if (getSchemaTypeName(current) !== "ZodObject") {
|
|
59
|
-
return
|
|
68
|
+
return {};
|
|
60
69
|
}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
70
|
+
const references = {};
|
|
71
|
+
for (const [field, childSchema] of Object.entries(getObjectShape(current))) {
|
|
72
|
+
const descriptor = getTopLevelReferenceDescriptor(childSchema);
|
|
73
|
+
if (!descriptor) {
|
|
74
|
+
continue;
|
|
65
75
|
}
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
}
|
|
76
|
+
const target = descriptor.collection || wildcardReferenceTarget;
|
|
77
|
+
references[target] ||= [];
|
|
78
|
+
references[target].push(field);
|
|
79
|
+
}
|
|
80
|
+
return references;
|
|
81
|
+
};
|
|
82
|
+
export const collectTopLevelReferenceFields = (schema, targetCollection) => {
|
|
83
|
+
const references = collectTopLevelReferenceFieldsByTarget(schema);
|
|
84
|
+
if (!targetCollection) {
|
|
85
|
+
return [...new Set(Object.values(references).flat())];
|
|
86
|
+
}
|
|
87
|
+
return [
|
|
88
|
+
.../* @__PURE__ */ new Set([
|
|
89
|
+
...references[targetCollection] || [],
|
|
90
|
+
...references[wildcardReferenceTarget] || []
|
|
91
|
+
])
|
|
92
|
+
];
|
|
70
93
|
};
|
|
@@ -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>;
|