@kirchdev/duxt 0.1.1-dev-main.30-1e18e2e → 0.1.1-dev.14-636388d
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/app/components/DuxtHeader.vue +2 -9
- package/app/components/DuxtLandingProse.vue +2 -1
- package/app/components/DuxtOpenApiClient.vue +3 -3
- package/app/components/DuxtVersion.vue +3 -33
- package/app/components/content/ProseA.vue +8 -13
- package/app/composables/useDuxtSearch.ts +43 -89
- package/app/types/duxt.d.ts +0 -7
- package/app/utils/version-choices.ts +0 -10
- package/package.json +1 -1
- package/public/devtools/checks.html +1 -1
- package/public/devtools/config.html +0 -1
- package/sections-changelog.ts +7 -17
- package/sections-resolve.ts +5 -19
- package/server/middleware/raw-markdown.ts +50 -37
- package/server/routes/llms-full.txt.get.ts +14 -2
- package/server/routes/llms.txt.get.ts +16 -2
- package/sources-git.ts +1 -8
- package/sources-resolve.ts +24 -80
- package/sources.ts +1 -5
- package/validate-report.ts +11 -20
- package/app/utils/documentation-link.ts +0 -14
- package/app/utils/search-scope.ts +0 -41
- package/server/utils/llms-pages.ts +0 -87
|
@@ -156,14 +156,7 @@ function current(to?: string) {
|
|
|
156
156
|
<UiSheetContent side="left" class="flex w-80 flex-col gap-0 p-0">
|
|
157
157
|
<UiSheetHeader class="border-b pr-14">
|
|
158
158
|
<UiSheetTitle class="flex items-center gap-2">
|
|
159
|
-
<
|
|
160
|
-
:to="localeLink('/')"
|
|
161
|
-
:aria-label="duxt.logo?.alt || duxt.title"
|
|
162
|
-
class="flex items-center gap-2"
|
|
163
|
-
@click="sheetOpen = false"
|
|
164
|
-
>
|
|
165
|
-
<DuxtBrand />
|
|
166
|
-
</NuxtLink>
|
|
159
|
+
<DuxtBrand />
|
|
167
160
|
|
|
168
161
|
<!-- One version and no choice: a fact about the project, so it
|
|
169
162
|
sits with the identity as a badge rather than filling a
|
|
@@ -335,7 +328,7 @@ function current(to?: string) {
|
|
|
335
328
|
</ul>
|
|
336
329
|
</template>
|
|
337
330
|
|
|
338
|
-
<template v-if="
|
|
331
|
+
<template v-if="items.length">
|
|
339
332
|
<p
|
|
340
333
|
class="mt-6 mb-2 border-t px-2 pt-4 text-[11px] font-semibold tracking-wider text-muted-foreground uppercase first:mt-0 first:border-t-0 first:pt-0"
|
|
341
334
|
>
|
|
@@ -42,7 +42,8 @@ const localeLink = useDuxtLink();
|
|
|
42
42
|
<component
|
|
43
43
|
:is="`h${level}`"
|
|
44
44
|
:id="headingId"
|
|
45
|
-
class="
|
|
45
|
+
class="font-semibold tracking-tight text-balance"
|
|
46
|
+
:class="level === 2 ? 'text-2xl sm:text-3xl' : 'text-xl sm:text-2xl'"
|
|
46
47
|
>
|
|
47
48
|
{{ prose.title }}
|
|
48
49
|
</component>
|
|
@@ -1161,7 +1161,7 @@ function pretty(text: string): string {
|
|
|
1161
1161
|
<div
|
|
1162
1162
|
:class="
|
|
1163
1163
|
split
|
|
1164
|
-
? ['
|
|
1164
|
+
? ['self-start', reverse ? 'lg:order-2' : 'lg:order-1']
|
|
1165
1165
|
: 'border-t px-4 py-3'
|
|
1166
1166
|
"
|
|
1167
1167
|
>
|
|
@@ -1199,14 +1199,14 @@ function pretty(text: string): string {
|
|
|
1199
1199
|
class="flex min-h-11 items-center gap-1 border-b bg-muted/40 px-2 py-1.5"
|
|
1200
1200
|
>
|
|
1201
1201
|
<TabsList
|
|
1202
|
-
class="flex
|
|
1202
|
+
class="flex items-center gap-1"
|
|
1203
1203
|
:aria-label="$t('duxt.openapi.client.samples') as string"
|
|
1204
1204
|
>
|
|
1205
1205
|
<TabsTrigger
|
|
1206
1206
|
v-for="entry in groups"
|
|
1207
1207
|
:key="entry"
|
|
1208
1208
|
:value="entry"
|
|
1209
|
-
class="flex
|
|
1209
|
+
class="flex cursor-pointer items-center gap-1.5 rounded-md px-2.5 py-1 font-mono text-xs font-medium text-muted-foreground transition-colors hover:bg-accent hover:text-foreground data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-sm"
|
|
1210
1210
|
>
|
|
1211
1211
|
<Icon :name="groupIcon(entry)" class="size-3.5" />
|
|
1212
1212
|
{{ entry }}
|
|
@@ -24,7 +24,6 @@ const props = withDefaults(defineProps<{ variant?: 'badge' | 'block' }>(), {
|
|
|
24
24
|
const duxt = useDuxtConfig();
|
|
25
25
|
const path = useDuxtPath();
|
|
26
26
|
const localeLink = useDuxtLink();
|
|
27
|
-
const { collection, source } = useDuxtCollection();
|
|
28
27
|
|
|
29
28
|
/**
|
|
30
29
|
* Versions come from the resolved source manifest, so the control can only
|
|
@@ -35,31 +34,6 @@ const { collection, source } = useDuxtCollection();
|
|
|
35
34
|
*/
|
|
36
35
|
const { choices: versions } = useDuxtVersion();
|
|
37
36
|
|
|
38
|
-
/**
|
|
39
|
-
* A changelog is one global history, so it deliberately has no version
|
|
40
|
-
* selector. Its current release is still useful context, though: read the
|
|
41
|
-
* latest one from the overview's generated frontmatter instead of falling
|
|
42
|
-
* back to the site's package version. The parser writes that value while
|
|
43
|
-
* turning the changelog into the overview and release pages.
|
|
44
|
-
*/
|
|
45
|
-
const changelog = computed(
|
|
46
|
-
() =>
|
|
47
|
-
source.value?.generated?.type === 'changelog' &&
|
|
48
|
-
source.value.generated.versioning === 'global'
|
|
49
|
-
);
|
|
50
|
-
|
|
51
|
-
const { data: changelogPage } = await useAsyncData(
|
|
52
|
-
() => `duxt-changelog-version-${collection.value}-${path.value}`,
|
|
53
|
-
async () => {
|
|
54
|
-
if (!changelog.value) return undefined;
|
|
55
|
-
|
|
56
|
-
return (await queryCollection(collection.value as DuxtCollectionArg)
|
|
57
|
-
.path(path.value)
|
|
58
|
-
.first()) as { release?: string } | null;
|
|
59
|
-
},
|
|
60
|
-
{ watch: [collection, path, changelog] }
|
|
61
|
-
);
|
|
62
|
-
|
|
63
37
|
const current = computed(() =>
|
|
64
38
|
sourceForPath(
|
|
65
39
|
path.value,
|
|
@@ -67,10 +41,6 @@ const current = computed(() =>
|
|
|
67
41
|
)
|
|
68
42
|
);
|
|
69
43
|
|
|
70
|
-
const label = computed(
|
|
71
|
-
() => changelogPage.value?.release ?? current.value?.label ?? duxt.version
|
|
72
|
-
);
|
|
73
|
-
|
|
74
44
|
const { t, te } = useI18n();
|
|
75
45
|
|
|
76
46
|
/**
|
|
@@ -124,7 +94,7 @@ function pathIn(version: { to?: string }) {
|
|
|
124
94
|
|
|
125
95
|
<span class="grid min-w-0 flex-1 leading-tight">
|
|
126
96
|
<span class="truncate font-mono text-sm font-medium">
|
|
127
|
-
{{ label }}
|
|
97
|
+
{{ current?.label ?? duxt.version }}
|
|
128
98
|
</span>
|
|
129
99
|
<span class="truncate text-xs text-muted-foreground">
|
|
130
100
|
{{ (current && caption(current)) ?? $t('duxt.version.label') }}
|
|
@@ -142,7 +112,7 @@ function pathIn(version: { to?: string }) {
|
|
|
142
112
|
variant="secondary"
|
|
143
113
|
class="gap-1 font-mono text-[10px] hover:bg-accent"
|
|
144
114
|
>
|
|
145
|
-
{{ label }}
|
|
115
|
+
{{ current?.label ?? duxt.version }}
|
|
146
116
|
<Icon name="lucide:chevron-down" class="size-3 opacity-60" />
|
|
147
117
|
</UiBadge>
|
|
148
118
|
</button>
|
|
@@ -193,6 +163,6 @@ function pathIn(version: { to?: string }) {
|
|
|
193
163
|
variant="secondary"
|
|
194
164
|
class="font-mono text-[10px]"
|
|
195
165
|
>
|
|
196
|
-
{{
|
|
166
|
+
{{ duxt.version }}
|
|
197
167
|
</UiBadge>
|
|
198
168
|
</template>
|
|
@@ -11,9 +11,8 @@
|
|
|
11
11
|
* mid-sentence.
|
|
12
12
|
*
|
|
13
13
|
* So an absolute internal path is read as relative TO ITS OWN SOURCE, and the
|
|
14
|
-
* prefix is put back here.
|
|
15
|
-
* a link
|
|
16
|
-
* is left alone, so a link written the long way round still works.
|
|
14
|
+
* prefix is put back here. A path that already carries the prefix is left
|
|
15
|
+
* alone, so a link written the long way round still works.
|
|
17
16
|
*/
|
|
18
17
|
const props = defineProps<{
|
|
19
18
|
href?: string;
|
|
@@ -27,26 +26,22 @@ const external = computed(() =>
|
|
|
27
26
|
Boolean(props.href && /^(?:[a-z]+:|\/\/)/i.test(props.href))
|
|
28
27
|
);
|
|
29
28
|
|
|
30
|
-
const internal = computed(() =>
|
|
31
|
-
Boolean(props.href?.startsWith('/') || props.href?.startsWith('~/'))
|
|
32
|
-
);
|
|
33
|
-
|
|
34
29
|
const to = computed(() => {
|
|
35
30
|
const href = props.href ?? '';
|
|
36
|
-
if (external.value || !
|
|
31
|
+
if (external.value || !href.startsWith('/')) return href;
|
|
37
32
|
|
|
38
33
|
const prefix = source.value?.prefix ?? '';
|
|
39
|
-
const [
|
|
40
|
-
const
|
|
41
|
-
|
|
34
|
+
const [path, hash] = href.split('#');
|
|
35
|
+
const resolved =
|
|
36
|
+
prefix && !isInside(path!, prefix) ? `${prefix}${path}` : path!;
|
|
42
37
|
|
|
43
|
-
return `${localeLink(resolved)}${
|
|
38
|
+
return `${localeLink(resolved)}${hash ? `#${hash}` : ''}`;
|
|
44
39
|
});
|
|
45
40
|
</script>
|
|
46
41
|
|
|
47
42
|
<template>
|
|
48
43
|
<a
|
|
49
|
-
v-if="external || !
|
|
44
|
+
v-if="external || !href?.startsWith('/')"
|
|
50
45
|
:href="href"
|
|
51
46
|
:target="target ?? (external ? '_blank' : undefined)"
|
|
52
47
|
:rel="external ? 'noopener' : undefined"
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import type { DuxtResolvedSource } from '../../sources-resolve';
|
|
2
|
-
import { searchSources } from '../utils/search-scope';
|
|
3
1
|
import type { DuxtSearchSection } from '@duxt/composables/useFuzzySearch';
|
|
4
2
|
|
|
5
3
|
/** A hit, with the source it came out of. */
|
|
@@ -24,19 +22,18 @@ export interface DuxtSearchHit extends DuxtSearchSection {
|
|
|
24
22
|
* The source being read goes first in each round, because a reader searching
|
|
25
23
|
* inside a project usually means that project.
|
|
26
24
|
*
|
|
27
|
-
* ONE VERSION PER
|
|
25
|
+
* ONE VERSION PER REPOSITORY. Searching every version returns each page as many
|
|
28
26
|
* times as there are versions, which buries the answer under its own history.
|
|
29
|
-
*
|
|
27
|
+
* So each repository contributes the version the reader is in, or its default.
|
|
30
28
|
*/
|
|
31
29
|
export function useDuxtSearch() {
|
|
32
30
|
const duxt = useDuxtConfig();
|
|
33
31
|
const { source } = useDuxtCollection();
|
|
34
|
-
const { locale, fallbackLocale } = useI18n();
|
|
35
32
|
|
|
36
33
|
const sources = computed(() => duxt.resolvedSources ?? []);
|
|
37
34
|
|
|
38
35
|
/**
|
|
39
|
-
* The
|
|
36
|
+
* The collections to search, one per repository.
|
|
40
37
|
*
|
|
41
38
|
* Fixed at setup, deliberately: each one needs its own `useSearchCollection`,
|
|
42
39
|
* and a composable list that grows with the route is not a thing. The
|
|
@@ -82,54 +79,38 @@ export function useDuxtSearch() {
|
|
|
82
79
|
}).search
|
|
83
80
|
}));
|
|
84
81
|
|
|
85
|
-
/**
|
|
86
|
-
const active = computed(() =>
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
.
|
|
102
|
-
|
|
103
|
-
.catch((error) => {
|
|
104
|
-
pagePaths.delete(collection);
|
|
105
|
-
throw error;
|
|
106
|
-
});
|
|
107
|
-
pagePaths.set(collection, pending);
|
|
82
|
+
/** One entry per repository: the version being read, else that repo's default. */
|
|
83
|
+
const active = computed(() => {
|
|
84
|
+
const current = source.value;
|
|
85
|
+
const byRepo = new Map<string, (typeof searchable)[number]>();
|
|
86
|
+
|
|
87
|
+
for (const entry of searchable) {
|
|
88
|
+
const key = entry.repo ?? '';
|
|
89
|
+
const chosen = byRepo.get(key);
|
|
90
|
+
|
|
91
|
+
if (entry.collection === current?.collection) {
|
|
92
|
+
byRepo.set(key, entry);
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (chosen?.collection === current?.collection) continue;
|
|
97
|
+
if (!chosen || (entry.isDefault && !chosen.isDefault)) {
|
|
98
|
+
byRepo.set(key, entry);
|
|
99
|
+
}
|
|
108
100
|
}
|
|
109
|
-
return pending;
|
|
110
|
-
}
|
|
111
101
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
.filter((other) => other.prefix === entry.prefix);
|
|
120
|
-
return new Set(
|
|
121
|
-
(
|
|
122
|
-
await Promise.all(preferred.map((other) => paths(other.collection)))
|
|
123
|
-
).flatMap((pages) => [...pages])
|
|
124
|
-
);
|
|
125
|
-
})
|
|
102
|
+
// The source being read leads; the rest keep the config's own order.
|
|
103
|
+
return [...byRepo.values()].sort((a, b) =>
|
|
104
|
+
a.collection === current?.collection
|
|
105
|
+
? -1
|
|
106
|
+
: b.collection === current?.collection
|
|
107
|
+
? 1
|
|
108
|
+
: 0
|
|
126
109
|
);
|
|
127
|
-
}
|
|
110
|
+
});
|
|
128
111
|
|
|
129
112
|
/** Only worth a badge when there is more than one thing to tell apart. */
|
|
130
|
-
const labelled = computed(
|
|
131
|
-
() => new Set(active.value.map((entry) => entry.prefix)).size > 1
|
|
132
|
-
);
|
|
113
|
+
const labelled = computed(() => active.value.length > 1);
|
|
133
114
|
|
|
134
115
|
const labelOf = (entry: (typeof searchable)[number]) =>
|
|
135
116
|
[entry.repo, entry.version].filter(Boolean).join(' ') ||
|
|
@@ -138,8 +119,8 @@ export function useDuxtSearch() {
|
|
|
138
119
|
|
|
139
120
|
async function init() {
|
|
140
121
|
await Promise.all(
|
|
141
|
-
|
|
142
|
-
.
|
|
122
|
+
searches
|
|
123
|
+
.filter((search) => active.value.includes(search.entry))
|
|
143
124
|
.filter((search) => search.status.value === 'idle')
|
|
144
125
|
.map((search) => search.init())
|
|
145
126
|
);
|
|
@@ -172,41 +153,17 @@ export function useDuxtSearch() {
|
|
|
172
153
|
term: string,
|
|
173
154
|
limit = 20
|
|
174
155
|
): Promise<{ hits: DuxtSearchHit[]; approximate: boolean }> {
|
|
175
|
-
if (!term.trim()
|
|
156
|
+
if (!term.trim()) return { hits: [], approximate: false };
|
|
176
157
|
|
|
177
158
|
const wanted = active.value;
|
|
178
|
-
const hidden = await visiblePages(wanted);
|
|
179
|
-
async function visible(
|
|
180
|
-
query: (requested: number) => Promise<DuxtSearchSection[]>,
|
|
181
|
-
index: number
|
|
182
|
-
) {
|
|
183
|
-
let requested = limit;
|
|
184
|
-
while (true) {
|
|
185
|
-
const hits = await query(requested);
|
|
186
|
-
const shown = hits.filter(
|
|
187
|
-
(hit) => !hidden[index]!.has(hit.id.split('#')[0]!)
|
|
188
|
-
);
|
|
189
|
-
if (
|
|
190
|
-
shown.length >= limit ||
|
|
191
|
-
hits.length < requested ||
|
|
192
|
-
!hidden[index]!.size
|
|
193
|
-
)
|
|
194
|
-
return shown.slice(0, limit);
|
|
195
|
-
requested *= 2;
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
159
|
|
|
199
160
|
const lists = await Promise.all(
|
|
200
|
-
|
|
201
|
-
.
|
|
202
|
-
.map(async (entry
|
|
203
|
-
const hits = await
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
limit: requested
|
|
207
|
-
})) as DuxtSearchSection[],
|
|
208
|
-
index
|
|
209
|
-
);
|
|
161
|
+
searches
|
|
162
|
+
.filter((entry) => wanted.includes(entry.entry))
|
|
163
|
+
.map(async (entry) => {
|
|
164
|
+
const hits = (await entry.search(term, {
|
|
165
|
+
limit
|
|
166
|
+
})) as DuxtSearchSection[];
|
|
210
167
|
|
|
211
168
|
return hits.map((hit) => ({
|
|
212
169
|
...hit,
|
|
@@ -227,13 +184,10 @@ export function useDuxtSearch() {
|
|
|
227
184
|
// leaves the reader with an empty box. Fuse gets a second look, over the
|
|
228
185
|
// same sources and merged the same way.
|
|
229
186
|
const fuzzy = await Promise.all(
|
|
230
|
-
|
|
231
|
-
.
|
|
232
|
-
.map(async (entry
|
|
233
|
-
const hits = await
|
|
234
|
-
(requested) => entry.search(term, requested),
|
|
235
|
-
index
|
|
236
|
-
);
|
|
187
|
+
approximate
|
|
188
|
+
.filter((entry) => wanted.includes(entry.entry))
|
|
189
|
+
.map(async (entry) => {
|
|
190
|
+
const hits = await entry.search(term, limit);
|
|
237
191
|
|
|
238
192
|
return hits.map((hit) => ({
|
|
239
193
|
...hit,
|
package/app/types/duxt.d.ts
CHANGED
|
@@ -185,13 +185,6 @@ declare global {
|
|
|
185
185
|
interface DuxtSourceInput {
|
|
186
186
|
/** Folder holding the Markdown, relative to the repository root. */
|
|
187
187
|
path?: string;
|
|
188
|
-
/**
|
|
189
|
-
* Whether this source's Markdown is published as documentation pages.
|
|
190
|
-
*
|
|
191
|
-
* Set `false` for a source that only provides generated sections. It can
|
|
192
|
-
* then share the default URL prefix of a versioned documentation source.
|
|
193
|
-
*/
|
|
194
|
-
content?: boolean;
|
|
195
188
|
/** `owner/name` or a full git URL. Omitted means this repository. */
|
|
196
189
|
repo?: string;
|
|
197
190
|
/** Refs to publish as versions. Omitted means the current checkout. */
|
|
@@ -45,8 +45,6 @@ export function versionChoices(
|
|
|
45
45
|
// from the URL segment.
|
|
46
46
|
if (configured?.length) return configured;
|
|
47
47
|
|
|
48
|
-
const editions = new Set<string>();
|
|
49
|
-
|
|
50
48
|
return sources
|
|
51
49
|
.filter(
|
|
52
50
|
(source) =>
|
|
@@ -54,14 +52,6 @@ export function versionChoices(
|
|
|
54
52
|
source.repo === current?.repo &&
|
|
55
53
|
sameArtefact(source, current)
|
|
56
54
|
)
|
|
57
|
-
.filter((source) => {
|
|
58
|
-
// Repository and artefact are scoped above; translations share the
|
|
59
|
-
// edition's version and target, while keeping separate collections.
|
|
60
|
-
const edition = JSON.stringify([source.version, source.prefix || '/']);
|
|
61
|
-
if (editions.has(edition)) return false;
|
|
62
|
-
editions.add(edition);
|
|
63
|
-
return true;
|
|
64
|
-
})
|
|
65
55
|
.map((source) => ({
|
|
66
56
|
label: source.version!,
|
|
67
57
|
to: source.prefix || '/',
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://www.schemastore.org/package.json",
|
|
3
3
|
"name": "@kirchdev/duxt",
|
|
4
|
-
"version": "0.1.1-dev
|
|
4
|
+
"version": "0.1.1-dev.14-636388d",
|
|
5
5
|
"description": "Nuxt documentation layer on Content v3 — multi-repo, versioned docs from one compact source list.",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"content-v3",
|
|
@@ -238,7 +238,7 @@
|
|
|
238
238
|
<p class="hint">The build validator, run against what the site is serving now.</p>
|
|
239
239
|
<div class="stats"><div class="stat "><b>34</b><span>pages checked</span></div><div class="stat "><b>6</b><span>collections</span></div><div class="stat ok"><b>0</b><span>errors</span></div><div class="stat warn"><b>5</b><span>warnings</span></div></div><h2>Warnings</h2><div class="scroll"><table>
|
|
240
240
|
<thead><tr><th scope="col"></th><th scope="col">Warning</th></tr></thead>
|
|
241
|
-
<tbody><tr><td><span class="tag warn">warn</span></td><td>the generated section "Releases" (CHANGELOG.md): the heading "1.3 (Feb 2026)" on line 41 reads like a release but does not parse as one, so it became part of the release above it instead of a page of its own. A release heading is a version, optionally followed by a date in parentheses: `## 1.4.0 (2024-02-01)`.</td></tr><tr><td><span class="tag warn">warn</span></td><td><a class="file" href="/__nuxt_devtools__/open-in-editor?file=%2Fsrv%2Facme-docs%2Fdocs_sdk%2F3.reference%2F2.errors.md" target="_blank" title="/srv/acme-docs/docs_sdk/3.reference/2.errors.md">"docs_sdk/3.reference/2.errors.md"</a> has no title and no description.</td></tr><tr><td><span class="tag warn">warn</span></td><td><a class="file" href="/__nuxt_devtools__/open-in-editor?file=%2Fsrv%2Facme-docs%2Fdocs_de_sdk%2Fde%2F3.reference%2F2.errors.md" target="_blank" title="/srv/acme-docs/docs_de_sdk/de/3.reference/2.errors.md">"docs_de_sdk/de/3.reference/2.errors.md"</a> has no title and no description.</td></tr><tr><td><span class="tag warn">warn</span></td><td><a class="file" href="/__nuxt_devtools__/open-in-editor?file=%2Fsrv%2Facme-docs%2Fdocs_sdk_v1_9%2F3.reference%2F2.errors.md" target="_blank" title="/srv/acme-docs/docs_sdk_v1_9/3.reference/2.errors.md">"docs_sdk_v1_9/3.reference/2.errors.md"</a> has no title and no description.</td></tr><tr><td><span class="tag warn">warn</span></td><td><a class="file" href="/__nuxt_devtools__/open-in-editor?file=%2Fsrv%2Facme-docs%2Fdocs_sdk%2F1.getting-started%2Findex.md" target="_blank" title="/srv/acme-docs/docs_sdk/1.getting-started/index.md">"docs_sdk/1.getting-started/index.md"</a> links to "/
|
|
241
|
+
<tbody><tr><td><span class="tag warn">warn</span></td><td>the generated section "Releases" (CHANGELOG.md): the heading "1.3 (Feb 2026)" on line 41 reads like a release but does not parse as one, so it became part of the release above it instead of a page of its own. A release heading is a version, optionally followed by a date in parentheses: `## 1.4.0 (2024-02-01)`.</td></tr><tr><td><span class="tag warn">warn</span></td><td><a class="file" href="/__nuxt_devtools__/open-in-editor?file=%2Fsrv%2Facme-docs%2Fdocs_sdk%2F3.reference%2F2.errors.md" target="_blank" title="/srv/acme-docs/docs_sdk/3.reference/2.errors.md">"docs_sdk/3.reference/2.errors.md"</a> has no title and no description.</td></tr><tr><td><span class="tag warn">warn</span></td><td><a class="file" href="/__nuxt_devtools__/open-in-editor?file=%2Fsrv%2Facme-docs%2Fdocs_de_sdk%2Fde%2F3.reference%2F2.errors.md" target="_blank" title="/srv/acme-docs/docs_de_sdk/de/3.reference/2.errors.md">"docs_de_sdk/de/3.reference/2.errors.md"</a> has no title and no description.</td></tr><tr><td><span class="tag warn">warn</span></td><td><a class="file" href="/__nuxt_devtools__/open-in-editor?file=%2Fsrv%2Facme-docs%2Fdocs_sdk_v1_9%2F3.reference%2F2.errors.md" target="_blank" title="/srv/acme-docs/docs_sdk_v1_9/3.reference/2.errors.md">"docs_sdk_v1_9/3.reference/2.errors.md"</a> has no title and no description.</td></tr><tr><td><span class="tag warn">warn</span></td><td><a class="file" href="/__nuxt_devtools__/open-in-editor?file=%2Fsrv%2Facme-docs%2Fdocs_sdk%2F1.getting-started%2Findex.md" target="_blank" title="/srv/acme-docs/docs_sdk/1.getting-started/index.md">"docs_sdk/1.getting-started/index.md"</a> links to "/guides/cacheing", which no page serves.</td></tr></tbody>
|
|
242
242
|
</table></div><h2>Translations</h2><ul class="steps"><li><strong>de under "/sdk": 6/8 pages</strong></li><li class="dim" style="margin-left:1.5rem"><a class="file" href="/__nuxt_devtools__/open-in-editor?file=%2Fsrv%2Facme-docs%2Fdocs_sdk%2F2.guides%2F2.caching.md" target="_blank" title="/srv/acme-docs/docs_sdk/2.guides/2.caching.md">"docs_sdk/2.guides/2.caching.md"</a> has no de translation.</li><li class="dim" style="margin-left:1.5rem"><a class="file" href="/__nuxt_devtools__/open-in-editor?file=%2Fsrv%2Facme-docs%2Fdocs_sdk%2F4.changelog.md" target="_blank" title="/srv/acme-docs/docs_sdk/4.changelog.md">"docs_sdk/4.changelog.md"</a> has no de translation.</li><li><strong>de under "/sdk/v1.9": 5/7 pages</strong></li><li class="dim" style="margin-left:1.5rem"><a class="file" href="/__nuxt_devtools__/open-in-editor?file=%2Fsrv%2Facme-docs%2Fdocs_sdk_v1_9%2F3.reference%2F2.errors.md" target="_blank" title="/srv/acme-docs/docs_sdk_v1_9/3.reference/2.errors.md">"docs_sdk_v1_9/3.reference/2.errors.md"</a> has no de translation.</li><li class="dim" style="margin-left:1.5rem"><a class="file" href="/__nuxt_devtools__/open-in-editor?file=%2Fsrv%2Facme-docs%2Fdocs_sdk_v1_9%2F4.changelog.md" target="_blank" title="/srv/acme-docs/docs_sdk_v1_9/4.changelog.md">"docs_sdk_v1_9/4.changelog.md"</a> has no de translation.</li></ul>
|
|
243
243
|
</main>
|
|
244
244
|
<script>
|
|
@@ -353,7 +353,6 @@
|
|
|
353
353
|
<span class="j-key">"repository"</span>: <span class="j-str">"acme/cli"</span>,
|
|
354
354
|
<span class="j-key">"repositoryUrl"</span>: <span class="j-str">"https://github.com/acme/cli"</span>,
|
|
355
355
|
<span class="j-key">"path"</span>: <span class="j-str">"docs"</span>,
|
|
356
|
-
<span class="j-key">"locale"</span>: <span class="j-str">"en"</span>,
|
|
357
356
|
<span class="j-key">"isDefaultLocale"</span>: <span class="j-word">true</span>,
|
|
358
357
|
<span class="j-key">"status"</span>: <span class="j-str">"current"</span>,
|
|
359
358
|
<span class="j-key">"history"</span>: <span class="j-word">false</span>
|
package/sections-changelog.ts
CHANGED
|
@@ -192,14 +192,12 @@ function parseChangelog(
|
|
|
192
192
|
context: DuxtSectionContext
|
|
193
193
|
): DuxtSectionPage[] {
|
|
194
194
|
const lines = artefact.split(/\r?\n/);
|
|
195
|
-
const headings = headingsOf(lines);
|
|
196
|
-
const starts = headings.filter((heading) => release(heading.text));
|
|
197
195
|
|
|
198
|
-
|
|
196
|
+
if (granularityOf(context.options) === 'flat') return [flat(lines, context)];
|
|
199
197
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
198
|
+
const headings = headingsOf(lines);
|
|
199
|
+
|
|
200
|
+
const starts = headings.filter((heading) => release(heading.text));
|
|
203
201
|
|
|
204
202
|
reportNearMisses(headings, starts, context);
|
|
205
203
|
const releases: Release[] = starts.map((heading, index) => {
|
|
@@ -238,15 +236,11 @@ function parseChangelog(
|
|
|
238
236
|
* exactly where the release tool put it — which is what "unchanged" has to
|
|
239
237
|
* mean, or the mode is not the escape hatch it exists to be.
|
|
240
238
|
*/
|
|
241
|
-
function flat(
|
|
242
|
-
lines: string[],
|
|
243
|
-
context: DuxtSectionContext,
|
|
244
|
-
newest?: string
|
|
245
|
-
): DuxtSectionPage {
|
|
239
|
+
function flat(lines: string[], context: DuxtSectionContext): DuxtSectionPage {
|
|
246
240
|
return {
|
|
247
241
|
file: 'index.md',
|
|
248
242
|
body: [
|
|
249
|
-
frontmatter({ title: context.label
|
|
243
|
+
frontmatter({ title: context.label }),
|
|
250
244
|
'',
|
|
251
245
|
...trim(withoutTitle(lines)),
|
|
252
246
|
''
|
|
@@ -443,11 +437,7 @@ function index(
|
|
|
443
437
|
return {
|
|
444
438
|
file: 'index.md',
|
|
445
439
|
body: [
|
|
446
|
-
|
|
447
|
-
// props below. Keep the newest release beside the page title instead:
|
|
448
|
-
// Content exposes frontmatter on the page query that `DuxtVersion` uses,
|
|
449
|
-
// and parsing the file again there would make the two answers drift.
|
|
450
|
-
frontmatter({ title: context.label, release: releases[0]?.version }),
|
|
440
|
+
frontmatter({ title: context.label }),
|
|
451
441
|
'',
|
|
452
442
|
// NO `<h1>` OF ITS OWN. The page draws the docs header — breadcrumb,
|
|
453
443
|
// title, description, the copy control beside it — for exactly the
|
package/sections-resolve.ts
CHANGED
|
@@ -22,7 +22,6 @@ import type {
|
|
|
22
22
|
DuxtSourcesOptions
|
|
23
23
|
} from './sources-resolve';
|
|
24
24
|
import {
|
|
25
|
-
assertCollectionIdentities,
|
|
26
25
|
expandSources,
|
|
27
26
|
identifier,
|
|
28
27
|
resolveSources,
|
|
@@ -368,16 +367,10 @@ export function resolveGeneratedSections(
|
|
|
368
367
|
// Claimed per locale, exactly as `resolveSources` claims its own prefixes:
|
|
369
368
|
// two languages serving one prefix is not a collision, it is the point.
|
|
370
369
|
const taken = new Map<string, string>(
|
|
371
|
-
resolved.
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
[
|
|
376
|
-
`${entry.locale ?? ''}|${entry.prefix}`,
|
|
377
|
-
`the documentation at "${entry.prefix || '/'}"`
|
|
378
|
-
] as [string, string]
|
|
379
|
-
]
|
|
380
|
-
)
|
|
370
|
+
resolved.map((entry) => [
|
|
371
|
+
`${entry.locale ?? ''}|${entry.prefix}`,
|
|
372
|
+
`the documentation at "${entry.prefix || '/'}"`
|
|
373
|
+
])
|
|
381
374
|
);
|
|
382
375
|
|
|
383
376
|
// Counted over the whole list rather than per source, so the identity is
|
|
@@ -546,10 +539,6 @@ export function resolveGeneratedSections(
|
|
|
546
539
|
}
|
|
547
540
|
});
|
|
548
541
|
|
|
549
|
-
assertCollectionIdentities([
|
|
550
|
-
...resolved.filter((_, index) => expanded[index]!.source.content !== false),
|
|
551
|
-
...generated
|
|
552
|
-
]);
|
|
553
542
|
return generated;
|
|
554
543
|
}
|
|
555
544
|
|
|
@@ -715,11 +704,8 @@ export function duxtManifest(
|
|
|
715
704
|
options: DuxtSourcesOptions = {},
|
|
716
705
|
types: DuxtSectionTypes = duxtSectionTypes()
|
|
717
706
|
): DuxtResolvedSource[] {
|
|
718
|
-
const resolved = resolveSources(sources, options);
|
|
719
|
-
const expanded = expandSources(sources, options);
|
|
720
|
-
|
|
721
707
|
return [
|
|
722
|
-
...
|
|
708
|
+
...resolveSources(sources, options),
|
|
723
709
|
...resolveGeneratedSections(sources, options, types)
|
|
724
710
|
];
|
|
725
711
|
}
|
|
@@ -1,7 +1,4 @@
|
|
|
1
1
|
import { queryCollection } from '@nuxt/content/nitro';
|
|
2
|
-
import { defineEventHandler, setHeader } from 'h3';
|
|
3
|
-
import { splitLocalePath } from '../../app/utils/locale-path';
|
|
4
|
-
import { sourcesForRoute } from '../../sources-resolve';
|
|
5
2
|
import { duxtDefaults, mergeDuxtConfig } from '../../app/utils/duxt-config';
|
|
6
3
|
import { stripFrontmatter } from '../utils/duxt-server-text';
|
|
7
4
|
|
|
@@ -24,42 +21,31 @@ export default defineEventHandler(async (event) => {
|
|
|
24
21
|
const appConfig = useAppConfig() as { duxt?: Partial<DuxtConfig> };
|
|
25
22
|
const duxt = mergeDuxtConfig(appConfig.duxt, duxtDefaults);
|
|
26
23
|
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
}
|
|
34
|
-
).i18n;
|
|
35
|
-
const { path: wanted, locale } = splitLocalePath(
|
|
36
|
-
path.slice(0, -'.md'.length),
|
|
37
|
-
(i18n?.locales ?? []).map((entry) =>
|
|
38
|
-
typeof entry === 'string' ? entry : entry.code
|
|
39
|
-
)
|
|
40
|
-
);
|
|
41
|
-
// Nuxt i18n loads the consumer's merged Vue I18n config in its request hook.
|
|
42
|
-
// Browser-language detection's fallback is a different policy.
|
|
43
|
-
const fallbackLocale = event.context.nuxtI18n?.vueI18nOptions
|
|
44
|
-
?.fallbackLocale as string | string[] | undefined;
|
|
45
|
-
const sources = sourcesForRoute(
|
|
46
|
-
wanted,
|
|
47
|
-
locale ?? i18n?.defaultLocale,
|
|
48
|
-
duxt.resolvedSources ?? [],
|
|
49
|
-
fallbackLocale
|
|
24
|
+
const wanted = stripLocale(path.slice(0, -'.md'.length));
|
|
25
|
+
|
|
26
|
+
// Longest prefix first, exactly as the app resolves a route: `/app/v2` beats
|
|
27
|
+
// `/app` on `/app/v2/guide`.
|
|
28
|
+
const sources = [...(duxt.resolvedSources ?? [])].sort(
|
|
29
|
+
(a, b) => b.prefix.length - a.prefix.length
|
|
50
30
|
);
|
|
51
31
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
32
|
+
const source =
|
|
33
|
+
sources.find(
|
|
34
|
+
(entry) =>
|
|
35
|
+
!entry.prefix ||
|
|
36
|
+
wanted === entry.prefix ||
|
|
37
|
+
wanted.startsWith(`${entry.prefix}/`)
|
|
38
|
+
) ?? sources[0];
|
|
39
|
+
|
|
40
|
+
if (!source) return;
|
|
41
|
+
|
|
42
|
+
const page = await queryCollection(
|
|
43
|
+
event,
|
|
44
|
+
source.collection as Parameters<typeof queryCollection>[1]
|
|
45
|
+
)
|
|
46
|
+
.path(wanted)
|
|
47
|
+
.select('title', 'rawbody')
|
|
48
|
+
.first();
|
|
63
49
|
|
|
64
50
|
// No page is not this middleware's error to raise: falling through lets the
|
|
65
51
|
// app answer with its own 404, which knows how to suggest a near miss.
|
|
@@ -71,3 +57,30 @@ export default defineEventHandler(async (event) => {
|
|
|
71
57
|
setHeader(event, 'content-type', 'text/markdown; charset=utf-8');
|
|
72
58
|
return stripFrontmatter(body).trim();
|
|
73
59
|
});
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* The locale segment, off.
|
|
63
|
+
*
|
|
64
|
+
* The browser is on `/de-DE/guide/deploying`; the page is `/guide/deploying`.
|
|
65
|
+
* Same distinction `stripLocalePrefix` makes in the app, made again here
|
|
66
|
+
* because Nitro has no i18n composable to ask.
|
|
67
|
+
*/
|
|
68
|
+
function stripLocale(path: string): string {
|
|
69
|
+
const first = path.split('/')[1];
|
|
70
|
+
if (!first) return path;
|
|
71
|
+
|
|
72
|
+
const locales = (
|
|
73
|
+
useRuntimeConfig().public as {
|
|
74
|
+
i18n?: { locales?: ({ code: string } | string)[] };
|
|
75
|
+
}
|
|
76
|
+
).i18n?.locales;
|
|
77
|
+
|
|
78
|
+
const codes = (locales ?? []).map((locale) =>
|
|
79
|
+
typeof locale === 'string' ? locale : locale.code
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
if (!codes.includes(first)) return path;
|
|
83
|
+
|
|
84
|
+
const rest = path.slice(first.length + 1);
|
|
85
|
+
return rest.startsWith('/') ? rest : rest || '/';
|
|
86
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { queryCollection } from '@nuxt/content/nitro';
|
|
2
2
|
// Imported by path, not auto-import: app/ belongs to the Vue app, and Nitro
|
|
3
3
|
// does not see its utils. The defaults have to come from the same file the
|
|
4
4
|
// pages use, or the two descriptions drift apart.
|
|
@@ -27,7 +27,19 @@ export default defineEventHandler(async (event) => {
|
|
|
27
27
|
mergeDuxtConfig(appConfig.duxt, duxtDefaults)
|
|
28
28
|
);
|
|
29
29
|
|
|
30
|
-
const
|
|
30
|
+
const collections = duxt.resolvedSources?.length
|
|
31
|
+
? duxt.resolvedSources.map((source) => source.collection)
|
|
32
|
+
: ['docs'];
|
|
33
|
+
|
|
34
|
+
const pages = (
|
|
35
|
+
await Promise.all(
|
|
36
|
+
collections.map((name) =>
|
|
37
|
+
queryCollection(event, name as Parameters<typeof queryCollection>[1])
|
|
38
|
+
.select('path', 'title', 'description', 'rawbody')
|
|
39
|
+
.all()
|
|
40
|
+
)
|
|
41
|
+
)
|
|
42
|
+
).flat();
|
|
31
43
|
|
|
32
44
|
const origin = getRequestURL(event).origin;
|
|
33
45
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { queryCollection } from '@nuxt/content/nitro';
|
|
2
2
|
// Imported by path, not auto-import: app/ belongs to the Vue app, and Nitro
|
|
3
3
|
// does not see its utils. The defaults have to come from the same file the
|
|
4
4
|
// pages use, or the two descriptions drift apart.
|
|
@@ -22,7 +22,21 @@ export default defineEventHandler(async (event) => {
|
|
|
22
22
|
|
|
23
23
|
// Every collection the manifest names, not just `docs`: a versioned site
|
|
24
24
|
// has none by that name, and llms.txt is the whole site's index.
|
|
25
|
-
const
|
|
25
|
+
const collections = duxt.resolvedSources?.length
|
|
26
|
+
? duxt.resolvedSources.map((source) => source.collection)
|
|
27
|
+
: ['docs'];
|
|
28
|
+
|
|
29
|
+
const pages = (
|
|
30
|
+
await Promise.all(
|
|
31
|
+
collections.map((name) =>
|
|
32
|
+
// Cast from Content's own signature: the collection name is data
|
|
33
|
+
// from the manifest, and the key union is generated per site.
|
|
34
|
+
queryCollection(event, name as Parameters<typeof queryCollection>[1])
|
|
35
|
+
.select('path', 'title', 'description')
|
|
36
|
+
.all()
|
|
37
|
+
)
|
|
38
|
+
)
|
|
39
|
+
).flat();
|
|
26
40
|
|
|
27
41
|
const origin = getRequestURL(event).origin;
|
|
28
42
|
|
package/sources-git.ts
CHANGED
|
@@ -1,11 +1,6 @@
|
|
|
1
1
|
import { execFileSync } from 'node:child_process';
|
|
2
2
|
import type { DuxtSource } from './sources-resolve';
|
|
3
|
-
import {
|
|
4
|
-
expandSources,
|
|
5
|
-
isLatestRef,
|
|
6
|
-
newestTag,
|
|
7
|
-
repoUrl
|
|
8
|
-
} from './sources-resolve';
|
|
3
|
+
import { isLatestRef, newestTag, repoUrl } from './sources-resolve';
|
|
9
4
|
|
|
10
5
|
/**
|
|
11
6
|
* Resolve the `latest` shorthand into the tag it means.
|
|
@@ -86,8 +81,6 @@ function tagsOf(repo: string | undefined): string[] {
|
|
|
86
81
|
* resolver itself stays pure and testable — it never learns that git exists.
|
|
87
82
|
*/
|
|
88
83
|
export function resolveLatestRefs(sources: DuxtSource[]): DuxtSource[] {
|
|
89
|
-
// Reject local refs before a missing tag can hide the configuration error.
|
|
90
|
-
expandSources(sources);
|
|
91
84
|
return sources.map((source) => {
|
|
92
85
|
if (!source.refs?.some(isLatestRef)) return source;
|
|
93
86
|
|
package/sources-resolve.ts
CHANGED
|
@@ -12,18 +12,10 @@ import type {
|
|
|
12
12
|
export interface DuxtSource {
|
|
13
13
|
/** Folder holding the Markdown, relative to the repository root. */
|
|
14
14
|
path?: string;
|
|
15
|
-
/**
|
|
16
|
-
* Whether this source's Markdown is published as documentation pages.
|
|
17
|
-
*
|
|
18
|
-
* A source can carry generated sections without publishing its own tree. That
|
|
19
|
-
* lets a versioned documentation source and a generated artefact share their
|
|
20
|
-
* default URL prefix without declaring two page collections for it.
|
|
21
|
-
*/
|
|
22
|
-
content?: boolean;
|
|
23
|
-
/** `owner/name` or a full git URL. Omitted reads the local checkout. */
|
|
15
|
+
/** `owner/name` or a full git URL. Omitted means this repository. */
|
|
24
16
|
repo?: string;
|
|
25
17
|
/**
|
|
26
|
-
* Refs to publish as versions.
|
|
18
|
+
* Refs to publish as versions. Omitted means the current checkout.
|
|
27
19
|
*
|
|
28
20
|
* A bare string is a branch. A tag has to say so — git keeps the two in
|
|
29
21
|
* separate namespaces, and asking for a tag under refs/heads fails the
|
|
@@ -41,8 +33,7 @@ export interface DuxtSource {
|
|
|
41
33
|
* The DEFAULT locale is the exception: it is the tree in `path` itself, with
|
|
42
34
|
* no folder, so listing it changes nothing. That is what keeps this key
|
|
43
35
|
* additive — a site that adds `locales` does not move the pages it already
|
|
44
|
-
* serves. Which one is the default comes from `defaultLocale`.
|
|
45
|
-
* `locales` serves that default language too.
|
|
36
|
+
* serves. Which one is the default comes from `defaultLocale`.
|
|
46
37
|
*/
|
|
47
38
|
locales?: DuxtSourceLocale[];
|
|
48
39
|
/**
|
|
@@ -175,7 +166,7 @@ export type DuxtSourceLocale =
|
|
|
175
166
|
path?: string;
|
|
176
167
|
/** A repository of its own — `owner/name` or a git URL. */
|
|
177
168
|
repo?: string;
|
|
178
|
-
/** A ref of its own
|
|
169
|
+
/** A ref of its own, when the translation is versioned separately. */
|
|
179
170
|
ref?: DuxtRef;
|
|
180
171
|
};
|
|
181
172
|
|
|
@@ -497,16 +488,12 @@ export function expandSources(
|
|
|
497
488
|
|
|
498
489
|
return sources.flatMap((source) =>
|
|
499
490
|
(source.refs?.length ? source.refs : [undefined]).flatMap((ref) =>
|
|
500
|
-
localesOf(source, ref).map((locale) => {
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
);
|
|
507
|
-
}
|
|
508
|
-
return { source, ref, locale, effective };
|
|
509
|
-
})
|
|
491
|
+
localesOf(source, ref).map((locale) => ({
|
|
492
|
+
source,
|
|
493
|
+
ref,
|
|
494
|
+
locale,
|
|
495
|
+
effective: localeEntry(source, locale, defaultLocale)
|
|
496
|
+
}))
|
|
510
497
|
)
|
|
511
498
|
);
|
|
512
499
|
}
|
|
@@ -574,7 +561,7 @@ export function resolveSources(
|
|
|
574
561
|
const name = effectiveRef ? refName(effectiveRef) : source.version;
|
|
575
562
|
const label =
|
|
576
563
|
(ref && typeof ref === 'object' ? ref.label : undefined) ?? source.label;
|
|
577
|
-
const code = locale ? localeCode(locale) :
|
|
564
|
+
const code = locale ? localeCode(locale) : undefined;
|
|
578
565
|
const isDefaultLocale = !code || code === defaultLocale;
|
|
579
566
|
const version = name ? slugify(label ?? name) : undefined;
|
|
580
567
|
const isDefault = !name || name === defaultRef;
|
|
@@ -597,23 +584,21 @@ export function resolveSources(
|
|
|
597
584
|
|
|
598
585
|
// Claimed per locale: two languages serving one prefix is not a collision,
|
|
599
586
|
// it is the point.
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
`
|
|
608
|
-
|
|
609
|
-
'Give one of them a `slug` or a `label`.'
|
|
610
|
-
);
|
|
611
|
-
}
|
|
612
|
-
taken.set(
|
|
613
|
-
claim,
|
|
614
|
-
`${source.repo ?? 'this repository'}${name ? `@${name}` : ''}`
|
|
587
|
+
const claim = `${code ?? ''}|${prefix}`;
|
|
588
|
+
const previous = taken.get(claim);
|
|
589
|
+
if (previous) {
|
|
590
|
+
// The one ambiguity the build-time decision leaves: a docs folder named
|
|
591
|
+
// like a repository or a version. Rejected rather than resolved silently.
|
|
592
|
+
throw new Error(
|
|
593
|
+
`duxt: two sources resolve to the same URL prefix "${prefix || '/'}" ` +
|
|
594
|
+
`(${previous} and ${source.repo ?? 'this repository'}${ref ? `@${ref}` : ''}). ` +
|
|
595
|
+
'Give one of them a `slug` or a `label`.'
|
|
615
596
|
);
|
|
616
597
|
}
|
|
598
|
+
taken.set(
|
|
599
|
+
claim,
|
|
600
|
+
`${source.repo ?? 'this repository'}${name ? `@${name}` : ''}`
|
|
601
|
+
);
|
|
617
602
|
|
|
618
603
|
resolved.push({
|
|
619
604
|
collection,
|
|
@@ -646,9 +631,6 @@ export function resolveSources(
|
|
|
646
631
|
});
|
|
647
632
|
}
|
|
648
633
|
|
|
649
|
-
assertCollectionIdentities(
|
|
650
|
-
resolved.filter((_, index) => expanded[index]!.source.content !== false)
|
|
651
|
-
);
|
|
652
634
|
return resolved;
|
|
653
635
|
}
|
|
654
636
|
|
|
@@ -924,41 +906,3 @@ function isInsidePrefix(path: string, prefix: string): boolean {
|
|
|
924
906
|
|
|
925
907
|
return path === prefix || path.startsWith(`${prefix}/`);
|
|
926
908
|
}
|
|
927
|
-
|
|
928
|
-
/** Validate names before any collection map can overwrite a declaration. */
|
|
929
|
-
export function assertCollectionIdentities(
|
|
930
|
-
entries: DuxtResolvedSource[]
|
|
931
|
-
): void {
|
|
932
|
-
const taken = new Map<string, string>();
|
|
933
|
-
const claim = (name: string, declaration: string) => {
|
|
934
|
-
const previous = taken.get(name);
|
|
935
|
-
if (previous !== undefined) {
|
|
936
|
-
throw new Error(
|
|
937
|
-
`duxt: collection "${name}" is claimed by both ${previous} and ${declaration}. ` +
|
|
938
|
-
'Give one declaration a distinct `slug` or `label`.'
|
|
939
|
-
);
|
|
940
|
-
}
|
|
941
|
-
taken.set(name, declaration);
|
|
942
|
-
};
|
|
943
|
-
|
|
944
|
-
for (const entry of entries) {
|
|
945
|
-
const kind = entry.generated
|
|
946
|
-
? `generated section "${entry.generated.label}"`
|
|
947
|
-
: 'documentation';
|
|
948
|
-
claim(
|
|
949
|
-
entry.collection,
|
|
950
|
-
`${kind} "${entry.path}" at "${entry.prefix || '/'}"` +
|
|
951
|
-
(entry.locale ? ` (locale "${entry.locale}")` : '')
|
|
952
|
-
);
|
|
953
|
-
}
|
|
954
|
-
|
|
955
|
-
// Shared partials are one declaration per language, even across sources.
|
|
956
|
-
const locales = new Set(
|
|
957
|
-
entries
|
|
958
|
-
.filter((entry) => !entry.generated)
|
|
959
|
-
.map((entry) => (entry.isDefaultLocale ? undefined : entry.locale))
|
|
960
|
-
);
|
|
961
|
-
for (const locale of locales) {
|
|
962
|
-
claim(partialsCollection(locale), `partials for "${locale ?? 'default'}"`);
|
|
963
|
-
}
|
|
964
|
-
}
|
package/sources.ts
CHANGED
|
@@ -37,8 +37,6 @@ export const pageSchema = z.object({
|
|
|
37
37
|
icon: z.string().optional(),
|
|
38
38
|
/** `landing` renders the page without the docs chrome. */
|
|
39
39
|
layout: z.string().optional(),
|
|
40
|
-
/** The latest release of a generated changelog overview. */
|
|
41
|
-
release: z.string().optional(),
|
|
42
40
|
/** false hides the page from the navigation. */
|
|
43
41
|
navigation: z.boolean().optional(),
|
|
44
42
|
/**
|
|
@@ -155,7 +153,6 @@ export function duxtSources(
|
|
|
155
153
|
|
|
156
154
|
resolved.forEach((entry, index) => {
|
|
157
155
|
const { source, ref, effective } = expanded[index]!;
|
|
158
|
-
if (source.content === false) return;
|
|
159
156
|
|
|
160
157
|
// A locale entry may carry a ref of its own — a translation repository
|
|
161
158
|
// that tags on its own schedule.
|
|
@@ -231,8 +228,7 @@ function partialFolders(
|
|
|
231
228
|
const seen = new Set<string>();
|
|
232
229
|
|
|
233
230
|
resolved.forEach((entry, index) => {
|
|
234
|
-
const {
|
|
235
|
-
if (source.content === false) return;
|
|
231
|
+
const { effective } = expanded[index]!;
|
|
236
232
|
const key = entry.isDefaultLocale ? undefined : entry.locale;
|
|
237
233
|
|
|
238
234
|
// One entry per REPOSITORY AND FOLDER, not per version: a partial is a
|
package/validate-report.ts
CHANGED
|
@@ -10,7 +10,6 @@
|
|
|
10
10
|
*/
|
|
11
11
|
import { reservedSegments } from './sources-resolve';
|
|
12
12
|
import { packageCommandIssues } from './app/utils/package-command';
|
|
13
|
-
import { resolveDocumentationPath } from './app/utils/documentation-link';
|
|
14
13
|
|
|
15
14
|
export interface PageRecord {
|
|
16
15
|
collection: string;
|
|
@@ -204,9 +203,9 @@ export function report(
|
|
|
204
203
|
// verify, and checking it would put the network in the build.
|
|
205
204
|
//
|
|
206
205
|
// Resolved exactly as `ProseA` resolves them at render time: an absolute
|
|
207
|
-
// path written in a page is relative to that page's OWN source
|
|
208
|
-
//
|
|
209
|
-
//
|
|
206
|
+
// path written in a page is relative to that page's OWN source, so it is
|
|
207
|
+
// tried under the source's prefix first and bare second. Checking only the
|
|
208
|
+
// bare form reports every correct link on a prefixed site.
|
|
210
209
|
//
|
|
211
210
|
// PER LANGUAGE, and that is not a refinement. Every language of one source
|
|
212
211
|
// serves IDENTICAL content paths — the locale lives in front of the URL,
|
|
@@ -260,15 +259,9 @@ export function report(
|
|
|
260
259
|
|
|
261
260
|
for (const link of page.links) {
|
|
262
261
|
const { href } = link;
|
|
263
|
-
if (
|
|
264
|
-
!href.startsWith('/') &&
|
|
265
|
-
!href.startsWith('~/') &&
|
|
266
|
-
!href.startsWith('#')
|
|
267
|
-
)
|
|
268
|
-
continue;
|
|
262
|
+
if (!href.startsWith('/') && !href.startsWith('#')) continue;
|
|
269
263
|
|
|
270
|
-
const [
|
|
271
|
-
const [target, query] = targetAndQuery!.split('?');
|
|
264
|
+
const [target, fragment] = href.split('#');
|
|
272
265
|
|
|
273
266
|
// DECODED, because the two halves are written in different alphabets. A
|
|
274
267
|
// heading's id is the text as it stands — `icônes` — while the link
|
|
@@ -276,22 +269,20 @@ export function report(
|
|
|
276
269
|
// accented anchor on the site as missing and told the author to point at
|
|
277
270
|
// a heading that was already there.
|
|
278
271
|
const anchor = fragment && decodeAnchor(fragment);
|
|
279
|
-
const
|
|
280
|
-
?
|
|
281
|
-
:
|
|
282
|
-
const renderedHref = `${renderedTarget}${query ? `?${query}` : ''}${fragment ? `#${fragment}` : ''}`;
|
|
283
|
-
const destination = renderedTarget ? resolve(renderedTarget, page) : page;
|
|
272
|
+
const destination = target
|
|
273
|
+
? (resolve(`${prefix}${target}`, page) ?? resolve(target, page))
|
|
274
|
+
: page;
|
|
284
275
|
|
|
285
|
-
if (
|
|
276
|
+
if (target && !destination) {
|
|
286
277
|
warnings.push(
|
|
287
|
-
`"${page.file}" links to "${
|
|
278
|
+
`"${page.file}" links to "${href}", which no page serves.`
|
|
288
279
|
);
|
|
289
280
|
continue;
|
|
290
281
|
}
|
|
291
282
|
|
|
292
283
|
if (anchor && destination && !destination.anchors.has(anchor)) {
|
|
293
284
|
warnings.push(
|
|
294
|
-
`"${page.file}" links to "${
|
|
285
|
+
`"${page.file}" links to "${href}", but that page has no "${anchor}" heading.`
|
|
295
286
|
);
|
|
296
287
|
}
|
|
297
288
|
}
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
import { isInside } from './version-paths';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Resolve a documentation path as the reader will see it.
|
|
5
|
-
*
|
|
6
|
-
* Markdown paths normally belong to the source that contains them. `~/` is the
|
|
7
|
-
* explicit escape hatch: it starts at the website root so one source can name
|
|
8
|
-
* a page served by another source without hard-coding its own prefix.
|
|
9
|
-
*/
|
|
10
|
-
export function resolveDocumentationPath(path: string, prefix: string): string {
|
|
11
|
-
if (path.startsWith('~/')) return path.slice(1);
|
|
12
|
-
|
|
13
|
-
return prefix && !isInside(path, prefix) ? `${prefix}${path}` : path;
|
|
14
|
-
}
|
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
sourcesForRoute,
|
|
3
|
-
type DuxtResolvedSource
|
|
4
|
-
} from '../../sources-resolve';
|
|
5
|
-
|
|
6
|
-
/** Select the editions participating in site-wide search. */
|
|
7
|
-
export function searchSources(
|
|
8
|
-
sources: DuxtResolvedSource[],
|
|
9
|
-
current: DuxtResolvedSource | undefined,
|
|
10
|
-
locale?: string,
|
|
11
|
-
fallbackLocale?: string | string[]
|
|
12
|
-
): DuxtResolvedSource[] {
|
|
13
|
-
const byArtefact = new Map<string, DuxtResolvedSource>();
|
|
14
|
-
for (const entry of sources) {
|
|
15
|
-
const key = JSON.stringify([entry.repo, entry.generated?.declaration]);
|
|
16
|
-
const chosen = byArtefact.get(key);
|
|
17
|
-
if (entry.collection === current?.collection) {
|
|
18
|
-
byArtefact.set(key, entry);
|
|
19
|
-
continue;
|
|
20
|
-
}
|
|
21
|
-
if (chosen?.collection === current?.collection) continue;
|
|
22
|
-
if (!chosen || (entry.isDefault && !chosen.isDefault))
|
|
23
|
-
byArtefact.set(key, entry);
|
|
24
|
-
}
|
|
25
|
-
return [...byArtefact.values()]
|
|
26
|
-
.sort((a, b) =>
|
|
27
|
-
a.collection === current?.collection
|
|
28
|
-
? -1
|
|
29
|
-
: b.collection === current?.collection
|
|
30
|
-
? 1
|
|
31
|
-
: 0
|
|
32
|
-
)
|
|
33
|
-
.flatMap((edition) =>
|
|
34
|
-
sourcesForRoute(
|
|
35
|
-
edition.prefix,
|
|
36
|
-
locale ?? current?.locale,
|
|
37
|
-
sources.filter((entry) => entry.prefix === edition.prefix),
|
|
38
|
-
fallbackLocale
|
|
39
|
-
)
|
|
40
|
-
);
|
|
41
|
-
}
|
|
@@ -1,87 +0,0 @@
|
|
|
1
|
-
import { queryCollection } from '@nuxt/content/nitro';
|
|
2
|
-
import type { H3Event } from 'h3';
|
|
3
|
-
import {
|
|
4
|
-
sourcesForRoute,
|
|
5
|
-
type DuxtResolvedSource
|
|
6
|
-
} from '../../sources-resolve';
|
|
7
|
-
|
|
8
|
-
/** Enumerate public pages, selecting the same collection chain as HTML. */
|
|
9
|
-
export async function llmsPages(
|
|
10
|
-
event: H3Event,
|
|
11
|
-
sources: DuxtResolvedSource[] = []
|
|
12
|
-
) {
|
|
13
|
-
const i18n = (
|
|
14
|
-
useRuntimeConfig(event).public as unknown as {
|
|
15
|
-
i18n?: {
|
|
16
|
-
locales?: (string | { code: string })[];
|
|
17
|
-
defaultLocale?: string;
|
|
18
|
-
strategy?: string;
|
|
19
|
-
};
|
|
20
|
-
}
|
|
21
|
-
).i18n;
|
|
22
|
-
const fallbackLocale = event.context.nuxtI18n?.vueI18nOptions
|
|
23
|
-
?.fallbackLocale as string | string[] | undefined;
|
|
24
|
-
const codes = (i18n?.locales ?? []).map((locale) =>
|
|
25
|
-
typeof locale === 'string' ? locale : locale.code
|
|
26
|
-
);
|
|
27
|
-
const defaultLocale = i18n?.defaultLocale ?? codes[0];
|
|
28
|
-
const strategy = i18n?.strategy ?? 'prefix_except_default';
|
|
29
|
-
const locales =
|
|
30
|
-
strategy === 'no_prefix' || !codes.length ? [defaultLocale] : codes;
|
|
31
|
-
const collections = sources.length
|
|
32
|
-
? [...new Set(sources.map((source) => source.collection))]
|
|
33
|
-
: ['docs'];
|
|
34
|
-
const entries = await Promise.all(
|
|
35
|
-
collections.map(
|
|
36
|
-
async (name) =>
|
|
37
|
-
[
|
|
38
|
-
name,
|
|
39
|
-
await queryCollection(
|
|
40
|
-
event,
|
|
41
|
-
name as Parameters<typeof queryCollection>[1]
|
|
42
|
-
)
|
|
43
|
-
.select('path', 'title', 'description', 'rawbody')
|
|
44
|
-
.all()
|
|
45
|
-
] as const
|
|
46
|
-
)
|
|
47
|
-
);
|
|
48
|
-
const pagesByCollection = new Map(
|
|
49
|
-
entries.map(([name, pages]) => [
|
|
50
|
-
name,
|
|
51
|
-
new Map(
|
|
52
|
-
pages.filter((page) => page.path).map((page) => [page.path, page])
|
|
53
|
-
)
|
|
54
|
-
])
|
|
55
|
-
);
|
|
56
|
-
const paths = new Set(
|
|
57
|
-
entries.flatMap(([, pages]) =>
|
|
58
|
-
pages.map((page) => page.path).filter(Boolean)
|
|
59
|
-
)
|
|
60
|
-
);
|
|
61
|
-
const result = new Map<string, (typeof entries)[number][1][number]>();
|
|
62
|
-
|
|
63
|
-
for (const locale of locales) {
|
|
64
|
-
for (const path of paths) {
|
|
65
|
-
const chain = sources.length
|
|
66
|
-
? sourcesForRoute(path, locale, sources, fallbackLocale).map(
|
|
67
|
-
(source) => source.collection
|
|
68
|
-
)
|
|
69
|
-
: ['docs'];
|
|
70
|
-
const page = chain
|
|
71
|
-
.map((name) => pagesByCollection.get(name)?.get(path))
|
|
72
|
-
.find(Boolean);
|
|
73
|
-
if (!page) continue;
|
|
74
|
-
const prefixed =
|
|
75
|
-
locale &&
|
|
76
|
-
strategy !== 'no_prefix' &&
|
|
77
|
-
(strategy !== 'prefix_except_default' || locale !== defaultLocale);
|
|
78
|
-
const publicPath = prefixed
|
|
79
|
-
? `/${locale}${path === '/' ? '' : path}`
|
|
80
|
-
: path;
|
|
81
|
-
result.set(publicPath, { ...page, path: publicPath });
|
|
82
|
-
if (strategy === 'prefix_and_default' && locale === defaultLocale)
|
|
83
|
-
result.set(path, { ...page, path });
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
return [...result.values()].sort((a, b) => a.path.localeCompare(b.path));
|
|
87
|
-
}
|