@kirchdev/duxt 0.1.1-dev.29-765cae3 → 0.2.0
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/DuxtLandingProse.vue +1 -2
- package/app/components/DuxtVersion.vue +33 -3
- package/app/components/content/ProseA.vue +13 -8
- package/app/types/duxt.d.ts +7 -0
- package/app/utils/documentation-link.ts +14 -0
- package/package.json +1 -1
- package/public/devtools/checks.html +1 -1
- package/sections-changelog.ts +17 -7
- package/sections-resolve.ts +18 -6
- package/server/middleware/raw-markdown.ts +1 -0
- package/sources-resolve.ts +26 -14
- package/sources.ts +5 -1
- package/validate-report.ts +20 -11
|
@@ -42,8 +42,7 @@ const localeLink = useDuxtLink();
|
|
|
42
42
|
<component
|
|
43
43
|
:is="`h${level}`"
|
|
44
44
|
:id="headingId"
|
|
45
|
-
class="font-semibold tracking-tight text-balance"
|
|
46
|
-
:class="level === 2 ? 'text-2xl sm:text-3xl' : 'text-xl sm:text-2xl'"
|
|
45
|
+
class="text-2xl font-semibold tracking-tight text-balance sm:text-3xl"
|
|
47
46
|
>
|
|
48
47
|
{{ prose.title }}
|
|
49
48
|
</component>
|
|
@@ -24,6 +24,7 @@ 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();
|
|
27
28
|
|
|
28
29
|
/**
|
|
29
30
|
* Versions come from the resolved source manifest, so the control can only
|
|
@@ -34,6 +35,31 @@ const localeLink = useDuxtLink();
|
|
|
34
35
|
*/
|
|
35
36
|
const { choices: versions } = useDuxtVersion();
|
|
36
37
|
|
|
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
|
+
|
|
37
63
|
const current = computed(() =>
|
|
38
64
|
sourceForPath(
|
|
39
65
|
path.value,
|
|
@@ -41,6 +67,10 @@ const current = computed(() =>
|
|
|
41
67
|
)
|
|
42
68
|
);
|
|
43
69
|
|
|
70
|
+
const label = computed(
|
|
71
|
+
() => changelogPage.value?.release ?? current.value?.label ?? duxt.version
|
|
72
|
+
);
|
|
73
|
+
|
|
44
74
|
const { t, te } = useI18n();
|
|
45
75
|
|
|
46
76
|
/**
|
|
@@ -94,7 +124,7 @@ function pathIn(version: { to?: string }) {
|
|
|
94
124
|
|
|
95
125
|
<span class="grid min-w-0 flex-1 leading-tight">
|
|
96
126
|
<span class="truncate font-mono text-sm font-medium">
|
|
97
|
-
{{
|
|
127
|
+
{{ label }}
|
|
98
128
|
</span>
|
|
99
129
|
<span class="truncate text-xs text-muted-foreground">
|
|
100
130
|
{{ (current && caption(current)) ?? $t('duxt.version.label') }}
|
|
@@ -112,7 +142,7 @@ function pathIn(version: { to?: string }) {
|
|
|
112
142
|
variant="secondary"
|
|
113
143
|
class="gap-1 font-mono text-[10px] hover:bg-accent"
|
|
114
144
|
>
|
|
115
|
-
{{
|
|
145
|
+
{{ label }}
|
|
116
146
|
<Icon name="lucide:chevron-down" class="size-3 opacity-60" />
|
|
117
147
|
</UiBadge>
|
|
118
148
|
</button>
|
|
@@ -163,6 +193,6 @@ function pathIn(version: { to?: string }) {
|
|
|
163
193
|
variant="secondary"
|
|
164
194
|
class="font-mono text-[10px]"
|
|
165
195
|
>
|
|
166
|
-
{{
|
|
196
|
+
{{ label }}
|
|
167
197
|
</UiBadge>
|
|
168
198
|
</template>
|
|
@@ -11,8 +11,9 @@
|
|
|
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
|
-
*
|
|
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.
|
|
16
17
|
*/
|
|
17
18
|
const props = defineProps<{
|
|
18
19
|
href?: string;
|
|
@@ -26,22 +27,26 @@ const external = computed(() =>
|
|
|
26
27
|
Boolean(props.href && /^(?:[a-z]+:|\/\/)/i.test(props.href))
|
|
27
28
|
);
|
|
28
29
|
|
|
30
|
+
const internal = computed(() =>
|
|
31
|
+
Boolean(props.href?.startsWith('/') || props.href?.startsWith('~/'))
|
|
32
|
+
);
|
|
33
|
+
|
|
29
34
|
const to = computed(() => {
|
|
30
35
|
const href = props.href ?? '';
|
|
31
|
-
if (external.value || !
|
|
36
|
+
if (external.value || !internal.value) return href;
|
|
32
37
|
|
|
33
38
|
const prefix = source.value?.prefix ?? '';
|
|
34
|
-
const [
|
|
35
|
-
const
|
|
36
|
-
|
|
39
|
+
const [pathAndQuery, hash] = href.split('#');
|
|
40
|
+
const [path, query] = pathAndQuery!.split('?');
|
|
41
|
+
const resolved = resolveDocumentationPath(path!, prefix);
|
|
37
42
|
|
|
38
|
-
return `${localeLink(resolved)}${hash ? `#${hash}` : ''}`;
|
|
43
|
+
return `${localeLink(resolved)}${query ? `?${query}` : ''}${hash ? `#${hash}` : ''}`;
|
|
39
44
|
});
|
|
40
45
|
</script>
|
|
41
46
|
|
|
42
47
|
<template>
|
|
43
48
|
<a
|
|
44
|
-
v-if="external || !
|
|
49
|
+
v-if="external || !internal"
|
|
45
50
|
:href="href"
|
|
46
51
|
:target="target ?? (external ? '_blank' : undefined)"
|
|
47
52
|
:rel="external ? 'noopener' : undefined"
|
package/app/types/duxt.d.ts
CHANGED
|
@@ -185,6 +185,13 @@ 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;
|
|
188
195
|
/** `owner/name` or a full git URL. Omitted means this repository. */
|
|
189
196
|
repo?: string;
|
|
190
197
|
/** Refs to publish as versions. Omitted means the current checkout. */
|
|
@@ -0,0 +1,14 @@
|
|
|
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
|
+
}
|
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.
|
|
4
|
+
"version": "0.2.0",
|
|
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 "/guides/cacheing", which no page serves.</td></tr></tbody>
|
|
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 "/sdk/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>
|
package/sections-changelog.ts
CHANGED
|
@@ -192,13 +192,15 @@ function parseChangelog(
|
|
|
192
192
|
context: DuxtSectionContext
|
|
193
193
|
): DuxtSectionPage[] {
|
|
194
194
|
const lines = artefact.split(/\r?\n/);
|
|
195
|
-
|
|
196
|
-
if (granularityOf(context.options) === 'flat') return [flat(lines, context)];
|
|
197
|
-
|
|
198
195
|
const headings = headingsOf(lines);
|
|
199
|
-
|
|
200
196
|
const starts = headings.filter((heading) => release(heading.text));
|
|
201
197
|
|
|
198
|
+
const newest = starts[0] ? release(starts[0].text)?.version : undefined;
|
|
199
|
+
|
|
200
|
+
if (granularityOf(context.options) === 'flat') {
|
|
201
|
+
return [flat(lines, context, newest)];
|
|
202
|
+
}
|
|
203
|
+
|
|
202
204
|
reportNearMisses(headings, starts, context);
|
|
203
205
|
const releases: Release[] = starts.map((heading, index) => {
|
|
204
206
|
const parsed = release(heading.text)!;
|
|
@@ -236,11 +238,15 @@ function parseChangelog(
|
|
|
236
238
|
* exactly where the release tool put it — which is what "unchanged" has to
|
|
237
239
|
* mean, or the mode is not the escape hatch it exists to be.
|
|
238
240
|
*/
|
|
239
|
-
function flat(
|
|
241
|
+
function flat(
|
|
242
|
+
lines: string[],
|
|
243
|
+
context: DuxtSectionContext,
|
|
244
|
+
newest?: string
|
|
245
|
+
): DuxtSectionPage {
|
|
240
246
|
return {
|
|
241
247
|
file: 'index.md',
|
|
242
248
|
body: [
|
|
243
|
-
frontmatter({ title: context.label }),
|
|
249
|
+
frontmatter({ title: context.label, release: newest }),
|
|
244
250
|
'',
|
|
245
251
|
...trim(withoutTitle(lines)),
|
|
246
252
|
''
|
|
@@ -437,7 +443,11 @@ function index(
|
|
|
437
443
|
return {
|
|
438
444
|
file: 'index.md',
|
|
439
445
|
body: [
|
|
440
|
-
|
|
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 }),
|
|
441
451
|
'',
|
|
442
452
|
// NO `<h1>` OF ITS OWN. The page draws the docs header — breadcrumb,
|
|
443
453
|
// title, description, the copy control beside it — for exactly the
|
package/sections-resolve.ts
CHANGED
|
@@ -368,10 +368,16 @@ export function resolveGeneratedSections(
|
|
|
368
368
|
// Claimed per locale, exactly as `resolveSources` claims its own prefixes:
|
|
369
369
|
// two languages serving one prefix is not a collision, it is the point.
|
|
370
370
|
const taken = new Map<string, string>(
|
|
371
|
-
resolved.
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
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
|
+
)
|
|
375
381
|
);
|
|
376
382
|
|
|
377
383
|
// Counted over the whole list rather than per source, so the identity is
|
|
@@ -540,7 +546,10 @@ export function resolveGeneratedSections(
|
|
|
540
546
|
}
|
|
541
547
|
});
|
|
542
548
|
|
|
543
|
-
assertCollectionIdentities([
|
|
549
|
+
assertCollectionIdentities([
|
|
550
|
+
...resolved.filter((_, index) => expanded[index]!.source.content !== false),
|
|
551
|
+
...generated
|
|
552
|
+
]);
|
|
544
553
|
return generated;
|
|
545
554
|
}
|
|
546
555
|
|
|
@@ -706,8 +715,11 @@ export function duxtManifest(
|
|
|
706
715
|
options: DuxtSourcesOptions = {},
|
|
707
716
|
types: DuxtSectionTypes = duxtSectionTypes()
|
|
708
717
|
): DuxtResolvedSource[] {
|
|
718
|
+
const resolved = resolveSources(sources, options);
|
|
719
|
+
const expanded = expandSources(sources, options);
|
|
720
|
+
|
|
709
721
|
return [
|
|
710
|
-
...
|
|
722
|
+
...resolved.filter((_, index) => expanded[index]!.source.content !== false),
|
|
711
723
|
...resolveGeneratedSections(sources, options, types)
|
|
712
724
|
];
|
|
713
725
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { queryCollection } from '@nuxt/content/nitro';
|
|
2
|
+
import { defineEventHandler, setHeader } from 'h3';
|
|
2
3
|
import { splitLocalePath } from '../../app/utils/locale-path';
|
|
3
4
|
import { sourcesForRoute } from '../../sources-resolve';
|
|
4
5
|
import { duxtDefaults, mergeDuxtConfig } from '../../app/utils/duxt-config';
|
package/sources-resolve.ts
CHANGED
|
@@ -12,6 +12,14 @@ 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;
|
|
15
23
|
/** `owner/name` or a full git URL. Omitted reads the local checkout. */
|
|
16
24
|
repo?: string;
|
|
17
25
|
/**
|
|
@@ -589,21 +597,23 @@ export function resolveSources(
|
|
|
589
597
|
|
|
590
598
|
// Claimed per locale: two languages serving one prefix is not a collision,
|
|
591
599
|
// it is the point.
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
`
|
|
600
|
-
|
|
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}` : ''}`
|
|
601
615
|
);
|
|
602
616
|
}
|
|
603
|
-
taken.set(
|
|
604
|
-
claim,
|
|
605
|
-
`${source.repo ?? 'this repository'}${name ? `@${name}` : ''}`
|
|
606
|
-
);
|
|
607
617
|
|
|
608
618
|
resolved.push({
|
|
609
619
|
collection,
|
|
@@ -636,7 +646,9 @@ export function resolveSources(
|
|
|
636
646
|
});
|
|
637
647
|
}
|
|
638
648
|
|
|
639
|
-
assertCollectionIdentities(
|
|
649
|
+
assertCollectionIdentities(
|
|
650
|
+
resolved.filter((_, index) => expanded[index]!.source.content !== false)
|
|
651
|
+
);
|
|
640
652
|
return resolved;
|
|
641
653
|
}
|
|
642
654
|
|
package/sources.ts
CHANGED
|
@@ -37,6 +37,8 @@ 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(),
|
|
40
42
|
/** false hides the page from the navigation. */
|
|
41
43
|
navigation: z.boolean().optional(),
|
|
42
44
|
/**
|
|
@@ -153,6 +155,7 @@ export function duxtSources(
|
|
|
153
155
|
|
|
154
156
|
resolved.forEach((entry, index) => {
|
|
155
157
|
const { source, ref, effective } = expanded[index]!;
|
|
158
|
+
if (source.content === false) return;
|
|
156
159
|
|
|
157
160
|
// A locale entry may carry a ref of its own — a translation repository
|
|
158
161
|
// that tags on its own schedule.
|
|
@@ -228,7 +231,8 @@ function partialFolders(
|
|
|
228
231
|
const seen = new Set<string>();
|
|
229
232
|
|
|
230
233
|
resolved.forEach((entry, index) => {
|
|
231
|
-
const { effective } = expanded[index]!;
|
|
234
|
+
const { source, effective } = expanded[index]!;
|
|
235
|
+
if (source.content === false) return;
|
|
232
236
|
const key = entry.isDefaultLocale ? undefined : entry.locale;
|
|
233
237
|
|
|
234
238
|
// One entry per REPOSITORY AND FOLDER, not per version: a partial is a
|
package/validate-report.ts
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
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';
|
|
13
14
|
|
|
14
15
|
export interface PageRecord {
|
|
15
16
|
collection: string;
|
|
@@ -203,9 +204,9 @@ export function report(
|
|
|
203
204
|
// verify, and checking it would put the network in the build.
|
|
204
205
|
//
|
|
205
206
|
// Resolved exactly as `ProseA` resolves them at render time: an absolute
|
|
206
|
-
// path written in a page is relative to that page's OWN source
|
|
207
|
-
//
|
|
208
|
-
//
|
|
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.
|
|
209
210
|
//
|
|
210
211
|
// PER LANGUAGE, and that is not a refinement. Every language of one source
|
|
211
212
|
// serves IDENTICAL content paths — the locale lives in front of the URL,
|
|
@@ -259,9 +260,15 @@ export function report(
|
|
|
259
260
|
|
|
260
261
|
for (const link of page.links) {
|
|
261
262
|
const { href } = link;
|
|
262
|
-
if (
|
|
263
|
+
if (
|
|
264
|
+
!href.startsWith('/') &&
|
|
265
|
+
!href.startsWith('~/') &&
|
|
266
|
+
!href.startsWith('#')
|
|
267
|
+
)
|
|
268
|
+
continue;
|
|
263
269
|
|
|
264
|
-
const [
|
|
270
|
+
const [targetAndQuery, fragment] = href.split('#');
|
|
271
|
+
const [target, query] = targetAndQuery!.split('?');
|
|
265
272
|
|
|
266
273
|
// DECODED, because the two halves are written in different alphabets. A
|
|
267
274
|
// heading's id is the text as it stands — `icônes` — while the link
|
|
@@ -269,20 +276,22 @@ export function report(
|
|
|
269
276
|
// accented anchor on the site as missing and told the author to point at
|
|
270
277
|
// a heading that was already there.
|
|
271
278
|
const anchor = fragment && decodeAnchor(fragment);
|
|
272
|
-
const
|
|
273
|
-
? (
|
|
274
|
-
:
|
|
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;
|
|
275
284
|
|
|
276
|
-
if (
|
|
285
|
+
if (renderedTarget && !destination) {
|
|
277
286
|
warnings.push(
|
|
278
|
-
`"${page.file}" links to "${
|
|
287
|
+
`"${page.file}" links to "${renderedHref}", which no page serves.`
|
|
279
288
|
);
|
|
280
289
|
continue;
|
|
281
290
|
}
|
|
282
291
|
|
|
283
292
|
if (anchor && destination && !destination.anchors.has(anchor)) {
|
|
284
293
|
warnings.push(
|
|
285
|
-
`"${page.file}" links to "${
|
|
294
|
+
`"${page.file}" links to "${renderedHref}", but that page has no "${anchor}" heading.`
|
|
286
295
|
);
|
|
287
296
|
}
|
|
288
297
|
}
|