@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.
@@ -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
- <NuxtLink
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="path !== '/' && items.length">
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="text-2xl font-semibold tracking-tight text-balance sm:text-3xl"
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
- ? ['min-w-0 self-start', reverse ? 'lg:order-2' : 'lg:order-1']
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 min-w-0 flex-1 items-center gap-1 overflow-x-auto"
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 shrink-0 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"
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
- {{ label }}
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. `~/` is the explicit website-root escape hatch for
15
- * a link that has to leave that source. A path that already carries the prefix
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 || !internal.value) return href;
31
+ if (external.value || !href.startsWith('/')) return href;
37
32
 
38
33
  const prefix = source.value?.prefix ?? '';
39
- const [pathAndQuery, hash] = href.split('#');
40
- const [path, query] = pathAndQuery!.split('?');
41
- const resolved = resolveDocumentationPath(path!, prefix);
34
+ const [path, hash] = href.split('#');
35
+ const resolved =
36
+ prefix && !isInside(path!, prefix) ? `${prefix}${path}` : path!;
42
37
 
43
- return `${localeLink(resolved)}${query ? `?${query}` : ''}${hash ? `#${hash}` : ''}`;
38
+ return `${localeLink(resolved)}${hash ? `#${hash}` : ''}`;
44
39
  });
45
40
  </script>
46
41
 
47
42
  <template>
48
43
  <a
49
- v-if="external || !internal"
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 ARTEFACT. Searching every version returns each page as many
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
- * Each docs tree and generated declaration contributes its current or default edition.
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 available collections; only selected editions are initialized.
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
- /** Selected editions, ordered by artefact and then preferred language. */
86
- const active = computed(() =>
87
- searchSources(
88
- searchable,
89
- source.value,
90
- locale.value,
91
- fallbackLocale.value as string | string[] | undefined
92
- )
93
- );
94
-
95
- const pagePaths = new Map<string, Promise<Set<string>>>();
96
- function paths(collection: string) {
97
- let pending = pagePaths.get(collection);
98
- if (!pending) {
99
- pending = queryCollection(collection as DuxtCollectionArg)
100
- .select('path')
101
- .all()
102
- .then((pages) => new Set(pages.map((page) => page.path)))
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
- // A fallback may answer only for a PAGE absent from every better language,
113
- // even when a heading or the search term exists only in the original.
114
- async function visiblePages(wanted: DuxtResolvedSource[]) {
115
- return Promise.all(
116
- wanted.map(async (entry, index) => {
117
- const preferred = wanted
118
- .slice(0, index)
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
- active.value
142
- .map((entry) => searches.find((search) => search.entry === entry)!)
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() || limit <= 0) return { hits: [], approximate: false };
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
- wanted
201
- .map((source) => searches.find((entry) => entry.entry === source)!)
202
- .map(async (entry, index) => {
203
- const hits = await visible(
204
- async (requested) =>
205
- (await entry.search(term, {
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
- wanted
231
- .map((source) => approximate.find((entry) => entry.entry === source)!)
232
- .map(async (entry, index) => {
233
- const hits = await visible(
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,
@@ -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-main.30-1e18e2e",
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 &quot;Releases&quot; (CHANGELOG.md): the heading &quot;1.3 (Feb 2026)&quot; 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">&quot;docs_sdk/3.reference/2.errors.md&quot;</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">&quot;docs_de_sdk/de/3.reference/2.errors.md&quot;</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">&quot;docs_sdk_v1_9/3.reference/2.errors.md&quot;</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">&quot;docs_sdk/1.getting-started/index.md&quot;</a> links to &quot;/sdk/guides/cacheing&quot;, which no page serves.</td></tr></tbody>
241
+ <tbody><tr><td><span class="tag warn">warn</span></td><td>the generated section &quot;Releases&quot; (CHANGELOG.md): the heading &quot;1.3 (Feb 2026)&quot; 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">&quot;docs_sdk/3.reference/2.errors.md&quot;</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">&quot;docs_de_sdk/de/3.reference/2.errors.md&quot;</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">&quot;docs_sdk_v1_9/3.reference/2.errors.md&quot;</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">&quot;docs_sdk/1.getting-started/index.md&quot;</a> links to &quot;/guides/cacheing&quot;, which no page serves.</td></tr></tbody>
242
242
  </table></div><h2>Translations</h2><ul class="steps"><li><strong>de under &quot;/sdk&quot;: 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">&quot;docs_sdk/2.guides/2.caching.md&quot;</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">&quot;docs_sdk/4.changelog.md&quot;</a> has no de translation.</li><li><strong>de under &quot;/sdk/v1.9&quot;: 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">&quot;docs_sdk_v1_9/3.reference/2.errors.md&quot;</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">&quot;docs_sdk_v1_9/4.changelog.md&quot;</a> has no de translation.</li></ul>
243
243
  </main>
244
244
  <script>
@@ -353,7 +353,6 @@
353
353
  <span class="j-key">&quot;repository&quot;</span>: <span class="j-str">&quot;acme/cli&quot;</span>,
354
354
  <span class="j-key">&quot;repositoryUrl&quot;</span>: <span class="j-str">&quot;https://github.com/acme/cli&quot;</span>,
355
355
  <span class="j-key">&quot;path&quot;</span>: <span class="j-str">&quot;docs&quot;</span>,
356
- <span class="j-key">&quot;locale&quot;</span>: <span class="j-str">&quot;en&quot;</span>,
357
356
  <span class="j-key">&quot;isDefaultLocale&quot;</span>: <span class="j-word">true</span>,
358
357
  <span class="j-key">&quot;status&quot;</span>: <span class="j-str">&quot;current&quot;</span>,
359
358
  <span class="j-key">&quot;history&quot;</span>: <span class="j-word">false</span>
@@ -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
- const newest = starts[0] ? release(starts[0].text)?.version : undefined;
196
+ if (granularityOf(context.options) === 'flat') return [flat(lines, context)];
199
197
 
200
- if (granularityOf(context.options) === 'flat') {
201
- return [flat(lines, context, newest)];
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, release: newest }),
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
- // The header stays outside this component, so it cannot read the MDC
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
@@ -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.flatMap((entry, index) =>
372
- expanded[index]!.source.content === false
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
- ...resolved.filter((_, index) => expanded[index]!.source.content !== false),
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 i18n = (
28
- useRuntimeConfig(event).public as {
29
- i18n?: {
30
- locales?: ({ code: string } | string)[];
31
- defaultLocale?: string;
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
- let page;
53
- for (const source of sources) {
54
- page = await queryCollection(
55
- event,
56
- source.collection as Parameters<typeof queryCollection>[1]
57
- )
58
- .path(wanted)
59
- .select('title', 'rawbody')
60
- .first();
61
- if (page) break;
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 { llmsPages } from '../utils/llms-pages';
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 pages = await llmsPages(event, duxt.resolvedSources);
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 { llmsPages } from '../utils/llms-pages';
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 pages = await llmsPages(event, duxt.resolvedSources);
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
 
@@ -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. Requires an explicit source or locale repo.
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`. Omitting
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; requires an explicit source or locale repo. */
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
- const effective = localeEntry(source, locale, defaultLocale);
502
- if (!effective.repo && (effective.ref ?? ref)) {
503
- throw new Error(
504
- `duxt: source "${source.path ?? 'docs'}" reads the local checkout and cannot select a ref. ` +
505
- 'Set an explicit repo to publish Git refs, or use version folders without refs.'
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) : defaultLocale;
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
- if (source.content !== false) {
601
- const claim = `${code ?? ''}|${prefix}`;
602
- const previous = taken.get(claim);
603
- if (previous) {
604
- // The one ambiguity the build-time decision leaves: a docs folder named
605
- // like a repository or a version. Rejected rather than resolved silently.
606
- throw new Error(
607
- `duxt: two sources resolve to the same URL prefix "${prefix || '/'}" ` +
608
- `(${previous} and ${source.repo ?? 'this repository'}${ref ? `@${ref}` : ''}). ` +
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 { source, effective } = expanded[index]!;
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
@@ -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. `~/` is
208
- // the one explicit website-root form. Checking a normal path bare as a
209
- // fallback would green-light a rendered 404 from a prefixed source.
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 [targetAndQuery, fragment] = href.split('#');
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 renderedTarget = target
280
- ? resolveDocumentationPath(target, prefix)
281
- : target;
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 (renderedTarget && !destination) {
276
+ if (target && !destination) {
286
277
  warnings.push(
287
- `"${page.file}" links to "${renderedHref}", which no page serves.`
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 "${renderedHref}", but that page has no "${anchor}" heading.`
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
- }