svelte-docsmith 0.9.0 → 0.10.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/bin/svelte-docsmith.mjs +161 -0
- package/dist/buildtime/archive.d.ts +31 -0
- package/dist/buildtime/archive.js +50 -0
- package/dist/buildtime/markdown-source.d.ts +31 -0
- package/dist/buildtime/markdown-source.js +90 -0
- package/dist/buildtime/vite/collect.d.ts +4 -3
- package/dist/buildtime/vite/collect.js +31 -12
- package/dist/buildtime/vite/extract.js +9 -35
- package/dist/buildtime/vite/frontmatter.js +4 -3
- package/dist/buildtime/vite/index.d.ts +9 -0
- package/dist/buildtime/vite/index.js +11 -4
- package/dist/components/chrome/copy-button.svelte +3 -2
- package/dist/components/chrome/search.svelte +16 -4
- package/dist/components/chrome/search.svelte.d.ts +4 -1
- package/dist/components/chrome/version-banner.svelte +82 -0
- package/dist/components/chrome/version-banner.svelte.d.ts +10 -0
- package/dist/components/chrome/version-switcher.svelte +63 -0
- package/dist/components/chrome/version-switcher.svelte.d.ts +14 -0
- package/dist/components/layouts/docs-header.svelte +26 -3
- package/dist/components/layouts/docs-header.svelte.d.ts +9 -1
- package/dist/components/layouts/docs-mobile-header.svelte +25 -2
- package/dist/components/layouts/docs-mobile-header.svelte.d.ts +9 -1
- package/dist/components/layouts/docs-shell.svelte +84 -66
- package/dist/components/layouts/docs-shell.svelte.d.ts +11 -2
- package/dist/components/layouts/error-page.svelte +23 -3
- package/dist/components/layouts/error-page.svelte.d.ts +8 -2
- package/dist/components/layouts/seo-head.svelte +8 -1
- package/dist/components/layouts/seo-head.svelte.d.ts +2 -0
- package/dist/core/content.d.ts +9 -0
- package/dist/core/docs-page.d.ts +70 -0
- package/dist/core/docs-page.js +55 -0
- package/dist/core/index.d.ts +2 -0
- package/dist/core/index.js +2 -0
- package/dist/core/version.d.ts +80 -0
- package/dist/core/version.js +85 -0
- package/dist/fallbacks/content.d.ts +2 -0
- package/dist/fallbacks/content.js +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +5 -0
- package/dist/search/context.svelte.d.ts +2 -0
- package/dist/search/context.svelte.js +2 -0
- package/dist/search/create-search.d.ts +8 -2
- package/dist/search/create-search.js +11 -3
- package/dist/theme.css +21 -0
- package/package.json +7 -2
|
@@ -2,16 +2,14 @@
|
|
|
2
2
|
import { page } from '$app/state';
|
|
3
3
|
import { afterNavigate } from '$app/navigation';
|
|
4
4
|
import {
|
|
5
|
-
|
|
6
|
-
flattenNav,
|
|
7
|
-
navTrail,
|
|
5
|
+
resolveDocsPage,
|
|
8
6
|
type DocsContentItem,
|
|
9
7
|
type DocsmithConfig,
|
|
10
|
-
type SearchDoc
|
|
8
|
+
type SearchDoc,
|
|
9
|
+
type ResolvedVersion
|
|
11
10
|
} from '../../core/index.js';
|
|
12
11
|
import { createToc } from '../../toc/index.js';
|
|
13
12
|
import { createSearchState } from '../../search/context.svelte.js';
|
|
14
|
-
import { normalizePath } from '../../utils/normalize-path.js';
|
|
15
13
|
import Search from '../chrome/search.svelte';
|
|
16
14
|
import BackgroundPattern from '../chrome/background-pattern.svelte';
|
|
17
15
|
import ThemeProvider from '../chrome/theme-provider.svelte';
|
|
@@ -21,10 +19,11 @@
|
|
|
21
19
|
import DocsMobileHeader from './docs-mobile-header.svelte';
|
|
22
20
|
import DocsSidebar from './docs-sidebar.svelte';
|
|
23
21
|
import PrevNextNav from './prev-next-nav.svelte';
|
|
24
|
-
import Breadcrumbs
|
|
22
|
+
import Breadcrumbs from './breadcrumbs.svelte';
|
|
25
23
|
import CopyPageMenu from './copy-page-menu.svelte';
|
|
26
24
|
import PageFeedback from './page-feedback.svelte';
|
|
27
25
|
import SeoHead from './seo-head.svelte';
|
|
26
|
+
import VersionBanner from '../chrome/version-banner.svelte';
|
|
28
27
|
import SquarePen from '@lucide/svelte/icons/square-pen';
|
|
29
28
|
import TableOfContents from '../chrome/table-of-contents.svelte';
|
|
30
29
|
import type { Snippet } from 'svelte';
|
|
@@ -32,6 +31,7 @@
|
|
|
32
31
|
const {
|
|
33
32
|
config,
|
|
34
33
|
content = [],
|
|
34
|
+
versions = [],
|
|
35
35
|
children,
|
|
36
36
|
logo,
|
|
37
37
|
actions,
|
|
@@ -46,6 +46,14 @@
|
|
|
46
46
|
}: {
|
|
47
47
|
config: DocsmithConfig;
|
|
48
48
|
content?: DocsContentItem[];
|
|
49
|
+
/**
|
|
50
|
+
* The resolved version manifest from `svelte-docsmith/content` (its
|
|
51
|
+
* `versions` export). Provide it to enable versioned docs — the sidebar,
|
|
52
|
+
* search, prev/next, and breadcrumbs scope to the version being read, and
|
|
53
|
+
* once an archived version exists a header switcher and an archived-version
|
|
54
|
+
* banner appear. Omit for a single tree.
|
|
55
|
+
*/
|
|
56
|
+
versions?: ResolvedVersion[];
|
|
49
57
|
children: Snippet;
|
|
50
58
|
/**
|
|
51
59
|
* Override the head tags for this page. Doc pages get their `<title>` and
|
|
@@ -57,8 +65,9 @@
|
|
|
57
65
|
* Enable the ⌘K search palette by lazily providing the generated index,
|
|
58
66
|
* e.g. `search={() => import('svelte-docsmith/search').then((m) => m.docs)}`.
|
|
59
67
|
* Omit to hide search. The index is fetched only when search first opens.
|
|
68
|
+
* Receives the active version id on a versioned site.
|
|
60
69
|
*/
|
|
61
|
-
search?: () => Promise<SearchDoc[]>;
|
|
70
|
+
search?: (versionId?: string) => Promise<SearchDoc[]>;
|
|
62
71
|
/** Custom logo mark for the header and mobile menu. */
|
|
63
72
|
logo?: Snippet;
|
|
64
73
|
/** Extra header controls (desktop and mobile), before the theme toggle. */
|
|
@@ -98,55 +107,43 @@
|
|
|
98
107
|
// `search` loader is static for the shell's lifetime, so reading it once at
|
|
99
108
|
// init is intentional.
|
|
100
109
|
// svelte-ignore state_referenced_locally
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
const nav = $derived(navFromContent(content));
|
|
110
|
+
const searchState = search ? createSearchState() : undefined;
|
|
104
111
|
|
|
105
|
-
//
|
|
106
|
-
//
|
|
107
|
-
|
|
112
|
+
// Every rule about the page being read is resolved in one pure module
|
|
113
|
+
// (`core/docs-page.ts`): the active version and the content scoped to it, the
|
|
114
|
+
// sidebar, the entry, prev/next, breadcrumbs, the server-rendered TOC. Reading
|
|
115
|
+
// them off one view keeps the parts from disagreeing about which page or
|
|
116
|
+
// version is active. What stays here is presentation: locale formatting, the
|
|
117
|
+
// "min read" copy, and the live TOC.
|
|
118
|
+
const view = $derived(
|
|
119
|
+
resolveDocsPage({
|
|
120
|
+
content,
|
|
121
|
+
versions,
|
|
122
|
+
pathname: page.url.pathname,
|
|
123
|
+
editUrl: config.editUrl,
|
|
124
|
+
siteTitle: config.title
|
|
125
|
+
})
|
|
126
|
+
);
|
|
108
127
|
|
|
109
|
-
//
|
|
110
|
-
|
|
111
|
-
|
|
128
|
+
// Keep the search palette scoped to the version currently being read.
|
|
129
|
+
$effect(() => {
|
|
130
|
+
if (searchState) searchState.version = view.activeVersionId;
|
|
131
|
+
});
|
|
112
132
|
|
|
113
|
-
const
|
|
114
|
-
|
|
115
|
-
?
|
|
133
|
+
const lastUpdatedLabel = $derived.by(() =>
|
|
134
|
+
view.lastUpdated
|
|
135
|
+
? new Intl.DateTimeFormat('en', { year: 'numeric', month: 'short', day: 'numeric' }).format(
|
|
136
|
+
view.lastUpdated
|
|
137
|
+
)
|
|
116
138
|
: undefined
|
|
117
139
|
);
|
|
118
|
-
const
|
|
119
|
-
const iso = currentEntry?.lastUpdated;
|
|
120
|
-
if (!iso) return undefined;
|
|
121
|
-
const d = new Date(iso);
|
|
122
|
-
return Number.isNaN(d.getTime())
|
|
123
|
-
? undefined
|
|
124
|
-
: new Intl.DateTimeFormat('en', { year: 'numeric', month: 'short', day: 'numeric' }).format(
|
|
125
|
-
d
|
|
126
|
-
);
|
|
127
|
-
});
|
|
128
|
-
const showFooterMeta = $derived(Boolean(editHref || lastUpdatedLabel));
|
|
140
|
+
const showFooterMeta = $derived(Boolean(view.editHref || lastUpdatedLabel));
|
|
129
141
|
|
|
130
142
|
const readingTimeLabel = $derived(
|
|
131
|
-
readingTime &&
|
|
143
|
+
readingTime && view.readingMinutes ? `${view.readingMinutes} min read` : undefined
|
|
132
144
|
);
|
|
133
145
|
|
|
134
|
-
|
|
135
|
-
const flatNav = $derived(flattenNav(nav));
|
|
136
|
-
const pageIndex = $derived(flatNav.findIndex((item) => item.url === pathname));
|
|
137
|
-
const prev = $derived(pageIndex > 0 ? flatNav[pageIndex - 1] : undefined);
|
|
138
|
-
const next = $derived(
|
|
139
|
-
pageIndex >= 0 && pageIndex < flatNav.length - 1 ? flatNav[pageIndex + 1] : undefined
|
|
140
|
-
);
|
|
141
|
-
const currentTitle = $derived(pageIndex >= 0 ? flatNav[pageIndex].title : config.title);
|
|
142
|
-
|
|
143
|
-
// Breadcrumb trail: the current page's sidebar group path, then the page
|
|
144
|
-
// itself (so a nested page reads e.g. Guides > Advanced > Middleware).
|
|
145
|
-
const breadcrumbs = $derived.by(() => {
|
|
146
|
-
const crumbs: Crumb[] = (navTrail(nav, pathname) ?? []).map((title) => ({ title }));
|
|
147
|
-
if (pageIndex >= 0) crumbs.push({ title: currentTitle });
|
|
148
|
-
return crumbs;
|
|
149
|
-
});
|
|
146
|
+
const breadcrumbs = $derived(view.breadcrumbs.map((title) => ({ title })));
|
|
150
147
|
|
|
151
148
|
// In-page TOC, scanned from the rendered content and re-scanned after every
|
|
152
149
|
// navigation (client-side included) so it never goes stale.
|
|
@@ -156,17 +153,17 @@
|
|
|
156
153
|
toc.refresh();
|
|
157
154
|
});
|
|
158
155
|
|
|
159
|
-
//
|
|
160
|
-
//
|
|
161
|
-
//
|
|
162
|
-
const
|
|
163
|
-
const tocItems = $derived(toc.items.length ? toc.items : pageToc);
|
|
156
|
+
// The build-time TOC from the content index is present on first paint; once
|
|
157
|
+
// the client engine has scanned the DOM, its items take over (scroll-spy +
|
|
158
|
+
// edge-case accuracy).
|
|
159
|
+
const tocItems = $derived(toc.items.length ? toc.items : view.toc);
|
|
164
160
|
</script>
|
|
165
161
|
|
|
166
162
|
<SeoHead
|
|
167
163
|
{config}
|
|
168
|
-
title={seo?.title ??
|
|
169
|
-
description={seo?.description ??
|
|
164
|
+
title={seo?.title ?? view.entry?.title}
|
|
165
|
+
description={seo?.description ?? view.entry?.description}
|
|
166
|
+
noindex={view.activeVersion?.noindex}
|
|
170
167
|
/>
|
|
171
168
|
|
|
172
169
|
<div class="relative isolate flex min-h-screen flex-col">
|
|
@@ -197,7 +194,15 @@
|
|
|
197
194
|
|
|
198
195
|
<!-- One header system everywhere: DocsHeader on desktop, DocsMobileHeader
|
|
199
196
|
below lg. The `page` layout just omits the sidebar nav and in-page TOC. -->
|
|
200
|
-
<DocsHeader
|
|
197
|
+
<DocsHeader
|
|
198
|
+
{config}
|
|
199
|
+
{logo}
|
|
200
|
+
{actions}
|
|
201
|
+
{versions}
|
|
202
|
+
active={view.activeVersion}
|
|
203
|
+
{content}
|
|
204
|
+
pathname={view.pathname}
|
|
205
|
+
/>
|
|
201
206
|
|
|
202
207
|
{#if layout === 'page'}
|
|
203
208
|
<DocsMobileHeader {config} {logo} {actions} />
|
|
@@ -208,16 +213,20 @@
|
|
|
208
213
|
{:else}
|
|
209
214
|
<DocsMobileHeader
|
|
210
215
|
{config}
|
|
211
|
-
{nav}
|
|
212
|
-
title={
|
|
216
|
+
nav={view.nav}
|
|
217
|
+
title={view.title}
|
|
213
218
|
{tocItems}
|
|
214
219
|
tocActiveId={toc.activeId}
|
|
215
220
|
{logo}
|
|
216
221
|
{actions}
|
|
222
|
+
{versions}
|
|
223
|
+
active={view.activeVersion}
|
|
224
|
+
{content}
|
|
225
|
+
pathname={view.pathname}
|
|
217
226
|
/>
|
|
218
227
|
|
|
219
228
|
<div class="mx-auto flex w-full max-w-7xl flex-1 gap-12 px-4 md:px-6 lg:px-8 lg:pt-10">
|
|
220
|
-
<DocsSidebar {nav} />
|
|
229
|
+
<DocsSidebar nav={view.nav} />
|
|
221
230
|
|
|
222
231
|
<main
|
|
223
232
|
bind:this={contentEl}
|
|
@@ -225,6 +234,15 @@
|
|
|
225
234
|
tabindex="-1"
|
|
226
235
|
class="min-w-0 flex-1 py-6 lg:py-0"
|
|
227
236
|
>
|
|
237
|
+
{#if view.isArchived && view.activeVersion && view.currentVersion}
|
|
238
|
+
<VersionBanner
|
|
239
|
+
active={view.activeVersion}
|
|
240
|
+
current={view.currentVersion}
|
|
241
|
+
pathname={view.pathname}
|
|
242
|
+
{content}
|
|
243
|
+
/>
|
|
244
|
+
{/if}
|
|
245
|
+
|
|
228
246
|
<div class="flex items-start justify-between gap-4">
|
|
229
247
|
<Breadcrumbs items={breadcrumbs} />
|
|
230
248
|
<div class="flex shrink-0 items-center gap-3">
|
|
@@ -233,8 +251,8 @@
|
|
|
233
251
|
{readingTimeLabel}
|
|
234
252
|
</span>
|
|
235
253
|
{/if}
|
|
236
|
-
{#if copyPage &&
|
|
237
|
-
<CopyPageMenu path={pathname} origin={config.url ?? ''} />
|
|
254
|
+
{#if copyPage && view.entry}
|
|
255
|
+
<CopyPageMenu path={view.pathname} origin={config.url ?? ''} />
|
|
238
256
|
{/if}
|
|
239
257
|
</div>
|
|
240
258
|
</div>
|
|
@@ -244,9 +262,9 @@
|
|
|
244
262
|
<div
|
|
245
263
|
class="text-muted-foreground border-border mt-10 flex flex-wrap items-center gap-3 border-t pt-6 text-sm"
|
|
246
264
|
>
|
|
247
|
-
{#if editHref}
|
|
265
|
+
{#if view.editHref}
|
|
248
266
|
<a
|
|
249
|
-
href={editHref}
|
|
267
|
+
href={view.editHref}
|
|
250
268
|
target="_blank"
|
|
251
269
|
rel="noopener noreferrer"
|
|
252
270
|
class="hover:text-foreground inline-flex items-center gap-1.5 transition-colors"
|
|
@@ -261,18 +279,18 @@
|
|
|
261
279
|
</div>
|
|
262
280
|
{/if}
|
|
263
281
|
|
|
264
|
-
{#if feedback &&
|
|
265
|
-
{#key pathname}
|
|
282
|
+
{#if feedback && view.entry}
|
|
283
|
+
{#key view.pathname}
|
|
266
284
|
<div class="mt-8">
|
|
267
285
|
<PageFeedback
|
|
268
|
-
path={pathname}
|
|
286
|
+
path={view.pathname}
|
|
269
287
|
onfeedback={typeof feedback === 'function' ? feedback : undefined}
|
|
270
288
|
/>
|
|
271
289
|
</div>
|
|
272
290
|
{/key}
|
|
273
291
|
{/if}
|
|
274
292
|
|
|
275
|
-
<PrevNextNav {prev} {next} bordered={!showFooterMeta} />
|
|
293
|
+
<PrevNextNav prev={view.prev} next={view.next} bordered={!showFooterMeta} />
|
|
276
294
|
</main>
|
|
277
295
|
|
|
278
296
|
<!-- Reserve the TOC column so its content filling in after hydration
|
|
@@ -1,8 +1,16 @@
|
|
|
1
|
-
import { type DocsContentItem, type DocsmithConfig, type SearchDoc } from '../../core/index.js';
|
|
1
|
+
import { type DocsContentItem, type DocsmithConfig, type SearchDoc, type ResolvedVersion } from '../../core/index.js';
|
|
2
2
|
import type { Snippet } from 'svelte';
|
|
3
3
|
type $$ComponentProps = {
|
|
4
4
|
config: DocsmithConfig;
|
|
5
5
|
content?: DocsContentItem[];
|
|
6
|
+
/**
|
|
7
|
+
* The resolved version manifest from `svelte-docsmith/content` (its
|
|
8
|
+
* `versions` export). Provide it to enable versioned docs — the sidebar,
|
|
9
|
+
* search, prev/next, and breadcrumbs scope to the version being read, and
|
|
10
|
+
* once an archived version exists a header switcher and an archived-version
|
|
11
|
+
* banner appear. Omit for a single tree.
|
|
12
|
+
*/
|
|
13
|
+
versions?: ResolvedVersion[];
|
|
6
14
|
children: Snippet;
|
|
7
15
|
/**
|
|
8
16
|
* Override the head tags for this page. Doc pages get their `<title>` and
|
|
@@ -17,8 +25,9 @@ type $$ComponentProps = {
|
|
|
17
25
|
* Enable the ⌘K search palette by lazily providing the generated index,
|
|
18
26
|
* e.g. `search={() => import('svelte-docsmith/search').then((m) => m.docs)}`.
|
|
19
27
|
* Omit to hide search. The index is fetched only when search first opens.
|
|
28
|
+
* Receives the active version id on a versioned site.
|
|
20
29
|
*/
|
|
21
|
-
search?: () => Promise<SearchDoc[]>;
|
|
30
|
+
search?: (versionId?: string) => Promise<SearchDoc[]>;
|
|
22
31
|
/** Custom logo mark for the header and mobile menu. */
|
|
23
32
|
logo?: Snippet;
|
|
24
33
|
/** Extra header controls (desktop and mobile), before the theme toggle. */
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { page } from '$app/state';
|
|
3
|
-
import type {
|
|
3
|
+
import type {
|
|
4
|
+
DocsContentItem,
|
|
5
|
+
DocsmithConfig,
|
|
6
|
+
ResolvedVersion,
|
|
7
|
+
SearchDoc
|
|
8
|
+
} from '../../core/index.js';
|
|
4
9
|
import DocsShell from './docs-shell.svelte';
|
|
5
10
|
import ArrowLeft from '@lucide/svelte/icons/arrow-left';
|
|
6
11
|
import type { Snippet } from 'svelte';
|
|
@@ -8,6 +13,7 @@
|
|
|
8
13
|
const {
|
|
9
14
|
config,
|
|
10
15
|
content = [],
|
|
16
|
+
versions = [],
|
|
11
17
|
search,
|
|
12
18
|
status,
|
|
13
19
|
title,
|
|
@@ -20,8 +26,14 @@
|
|
|
20
26
|
config: DocsmithConfig;
|
|
21
27
|
/** Content index, so the error page keeps the same header/footer as the site. */
|
|
22
28
|
content?: DocsContentItem[];
|
|
29
|
+
/**
|
|
30
|
+
* The resolved version manifest (same as `DocsShell`). Pass it on a
|
|
31
|
+
* versioned site so an error under an archived prefix keeps that version's
|
|
32
|
+
* search scope, switcher, and `noindex`.
|
|
33
|
+
*/
|
|
34
|
+
versions?: ResolvedVersion[];
|
|
23
35
|
/** Enable the ⌘K search palette on the error page (same loader as DocsShell). */
|
|
24
|
-
search?: () => Promise<SearchDoc[]>;
|
|
36
|
+
search?: (versionId?: string) => Promise<SearchDoc[]>;
|
|
25
37
|
/** HTTP status; defaults to the current `page.status`. */
|
|
26
38
|
status?: number;
|
|
27
39
|
/** Heading; defaults to a message keyed off the status. */
|
|
@@ -49,7 +61,15 @@
|
|
|
49
61
|
);
|
|
50
62
|
</script>
|
|
51
63
|
|
|
52
|
-
<DocsShell
|
|
64
|
+
<DocsShell
|
|
65
|
+
{config}
|
|
66
|
+
{content}
|
|
67
|
+
{versions}
|
|
68
|
+
{search}
|
|
69
|
+
{pattern}
|
|
70
|
+
layout="page"
|
|
71
|
+
seo={{ title: resolvedTitle }}
|
|
72
|
+
>
|
|
53
73
|
<section
|
|
54
74
|
class="mx-auto flex min-h-[60vh] max-w-2xl flex-col items-center justify-center px-4 py-20 text-center md:px-6"
|
|
55
75
|
>
|
|
@@ -1,11 +1,17 @@
|
|
|
1
|
-
import type { DocsContentItem, DocsmithConfig, SearchDoc } from '../../core/index.js';
|
|
1
|
+
import type { DocsContentItem, DocsmithConfig, ResolvedVersion, SearchDoc } from '../../core/index.js';
|
|
2
2
|
import type { Snippet } from 'svelte';
|
|
3
3
|
type $$ComponentProps = {
|
|
4
4
|
config: DocsmithConfig;
|
|
5
5
|
/** Content index, so the error page keeps the same header/footer as the site. */
|
|
6
6
|
content?: DocsContentItem[];
|
|
7
|
+
/**
|
|
8
|
+
* The resolved version manifest (same as `DocsShell`). Pass it on a
|
|
9
|
+
* versioned site so an error under an archived prefix keeps that version's
|
|
10
|
+
* search scope, switcher, and `noindex`.
|
|
11
|
+
*/
|
|
12
|
+
versions?: ResolvedVersion[];
|
|
7
13
|
/** Enable the ⌘K search palette on the error page (same loader as DocsShell). */
|
|
8
|
-
search?: () => Promise<SearchDoc[]>;
|
|
14
|
+
search?: (versionId?: string) => Promise<SearchDoc[]>;
|
|
9
15
|
/** HTTP status; defaults to the current `page.status`. */
|
|
10
16
|
status?: number;
|
|
11
17
|
/** Heading; defaults to a message keyed off the status. */
|
|
@@ -6,13 +6,16 @@
|
|
|
6
6
|
const {
|
|
7
7
|
config,
|
|
8
8
|
title,
|
|
9
|
-
description
|
|
9
|
+
description,
|
|
10
|
+
noindex = false
|
|
10
11
|
}: {
|
|
11
12
|
config: DocsmithConfig;
|
|
12
13
|
/** The page's own title (frontmatter or override); omit for the site root. */
|
|
13
14
|
title?: string;
|
|
14
15
|
/** The page's own description; falls back to `config.description`. */
|
|
15
16
|
description?: string;
|
|
17
|
+
/** Emit `robots: noindex`, for a doc version explicitly marked hidden. */
|
|
18
|
+
noindex?: boolean;
|
|
16
19
|
} = $props();
|
|
17
20
|
|
|
18
21
|
// "Page · Site", or just the site title when they would duplicate (the home page).
|
|
@@ -35,6 +38,10 @@
|
|
|
35
38
|
|
|
36
39
|
<svelte:head>
|
|
37
40
|
<title>{fullTitle}</title>
|
|
41
|
+
{#if noindex}
|
|
42
|
+
<!-- Keep this version out of search results; still follow its links. -->
|
|
43
|
+
<meta name="robots" content="noindex, follow" />
|
|
44
|
+
{/if}
|
|
38
45
|
{#if metaDescription}
|
|
39
46
|
<meta name="description" content={metaDescription} />
|
|
40
47
|
{/if}
|
|
@@ -5,6 +5,8 @@ type $$ComponentProps = {
|
|
|
5
5
|
title?: string;
|
|
6
6
|
/** The page's own description; falls back to `config.description`. */
|
|
7
7
|
description?: string;
|
|
8
|
+
/** Emit `robots: noindex`, for a doc version explicitly marked hidden. */
|
|
9
|
+
noindex?: boolean;
|
|
8
10
|
};
|
|
9
11
|
declare const SeoHead: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
10
12
|
type SeoHead = ReturnType<typeof SeoHead>;
|
package/dist/core/content.d.ts
CHANGED
|
@@ -37,6 +37,11 @@ export type DocsContentItem = {
|
|
|
37
37
|
title: string;
|
|
38
38
|
depth: 2 | 3;
|
|
39
39
|
}[];
|
|
40
|
+
/**
|
|
41
|
+
* The id of the version this page belongs to, when the site declares
|
|
42
|
+
* versions (`docsmith({ versions })`). `undefined` on unversioned sites.
|
|
43
|
+
*/
|
|
44
|
+
version?: string;
|
|
40
45
|
};
|
|
41
46
|
/**
|
|
42
47
|
* One page's searchable record, emitted at build time by the `docsmith()` vite
|
|
@@ -54,6 +59,8 @@ export type SearchDoc = {
|
|
|
54
59
|
headings: string[];
|
|
55
60
|
/** Plain-text body: prose and headings, code and markup removed. */
|
|
56
61
|
text: string;
|
|
62
|
+
/** The page's version id when the site declares versions; else `undefined`. */
|
|
63
|
+
version?: string;
|
|
57
64
|
};
|
|
58
65
|
/**
|
|
59
66
|
* One page's record for LLM-facing output, emitted at build time by the
|
|
@@ -70,4 +77,6 @@ export type LlmsDoc = {
|
|
|
70
77
|
/** The page's markdown body (frontmatter and `<script>`/`<style>` removed,
|
|
71
78
|
* headings and code kept), with the title prepended as an `h1`. */
|
|
72
79
|
content: string;
|
|
80
|
+
/** The page's version id when the site declares versions; else `undefined`. */
|
|
81
|
+
version?: string;
|
|
73
82
|
};
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Docs page resolution: everything `DocsShell` needs to know about the page
|
|
3
|
+
* being read, derived in one place from the content index, the version manifest
|
|
4
|
+
* and the URL.
|
|
5
|
+
*
|
|
6
|
+
* Pure and framework-agnostic, so each rule is a field with a test rather than
|
|
7
|
+
* a derivation whose only test surface is a rendered tree. The view returns
|
|
8
|
+
* data, never formatted strings: locale formatting and copy stay in the
|
|
9
|
+
* component that renders them.
|
|
10
|
+
*/
|
|
11
|
+
import type { DocsContentItem } from './content.js';
|
|
12
|
+
import { type NavGroup, type NavItem } from './nav.js';
|
|
13
|
+
import { type ResolvedVersion } from './version.js';
|
|
14
|
+
/** The page's build-time headings, as extracted into the content index. */
|
|
15
|
+
type PageToc = NonNullable<DocsContentItem['toc']>;
|
|
16
|
+
/** Everything `DocsShell` renders about the current page. */
|
|
17
|
+
export type DocsPageView = {
|
|
18
|
+
/** The current path, trailing slash stripped. Match content against this. */
|
|
19
|
+
pathname: string;
|
|
20
|
+
/**
|
|
21
|
+
* The version owning the page, falling back to the current version off the
|
|
22
|
+
* docs tree (e.g. a `page` layout). `undefined` on an unversioned site.
|
|
23
|
+
*/
|
|
24
|
+
activeVersion: ResolvedVersion | undefined;
|
|
25
|
+
/** {@link activeVersion}'s id, for scoping search. */
|
|
26
|
+
activeVersionId: string | undefined;
|
|
27
|
+
/** The current version, for the archived-version banner's "latest" link. */
|
|
28
|
+
currentVersion: ResolvedVersion | undefined;
|
|
29
|
+
/** Whether the page belongs to an archived (frozen) version. */
|
|
30
|
+
isArchived: boolean;
|
|
31
|
+
/** The content index scoped to {@link activeVersion}. */
|
|
32
|
+
scopedContent: DocsContentItem[];
|
|
33
|
+
/** Sidebar nav built from {@link scopedContent}. */
|
|
34
|
+
nav: NavGroup[];
|
|
35
|
+
/** The scoped content entry for this route, if the page is a doc page. */
|
|
36
|
+
entry: DocsContentItem | undefined;
|
|
37
|
+
/** "Edit this page" target. Absent without an edit base, or when archived. */
|
|
38
|
+
editHref: string | undefined;
|
|
39
|
+
/** The page's last commit date, unformatted. Absent if missing or unparseable. */
|
|
40
|
+
lastUpdated?: Date;
|
|
41
|
+
/** Estimated reading time in whole minutes, uncopied. */
|
|
42
|
+
readingMinutes?: number;
|
|
43
|
+
/** Previous page in sidebar reading order. */
|
|
44
|
+
prev: NavItem | undefined;
|
|
45
|
+
/** Next page in sidebar reading order. */
|
|
46
|
+
next: NavItem | undefined;
|
|
47
|
+
/** The page's nav title, falling back to the site title off the nav tree. */
|
|
48
|
+
title: string;
|
|
49
|
+
/**
|
|
50
|
+
* Breadcrumb titles: the page's group trail, then the page itself. Titles
|
|
51
|
+
* only, so this layer never depends on a component's `Crumb` type.
|
|
52
|
+
*/
|
|
53
|
+
breadcrumbs: string[];
|
|
54
|
+
/** The page's server-rendered TOC, from {@link entry}. */
|
|
55
|
+
toc: PageToc;
|
|
56
|
+
};
|
|
57
|
+
/**
|
|
58
|
+
* Resolve the page being read. `pathname` is taken raw (normalized here, so the
|
|
59
|
+
* caller has one fewer place to remember) and every field is derived from the
|
|
60
|
+
* same normalized path and the same scoped content, so the sidebar, prev/next,
|
|
61
|
+
* breadcrumbs and TOC can never disagree about which page or version is active.
|
|
62
|
+
*/
|
|
63
|
+
export declare function resolveDocsPage(input: {
|
|
64
|
+
content: DocsContentItem[];
|
|
65
|
+
versions: ResolvedVersion[];
|
|
66
|
+
pathname: string;
|
|
67
|
+
editUrl?: string;
|
|
68
|
+
siteTitle: string;
|
|
69
|
+
}): DocsPageView;
|
|
70
|
+
export {};
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { navFromContent, flattenNav, navTrail } from './nav.js';
|
|
2
|
+
import { activeVersion, currentVersion, scopeContent } from './version.js';
|
|
3
|
+
import { normalizePath } from '../utils/normalize-path.js';
|
|
4
|
+
/**
|
|
5
|
+
* Resolve the page being read. `pathname` is taken raw (normalized here, so the
|
|
6
|
+
* caller has one fewer place to remember) and every field is derived from the
|
|
7
|
+
* same normalized path and the same scoped content, so the sidebar, prev/next,
|
|
8
|
+
* breadcrumbs and TOC can never disagree about which page or version is active.
|
|
9
|
+
*/
|
|
10
|
+
export function resolveDocsPage(input) {
|
|
11
|
+
const { content, versions, editUrl, siteTitle } = input;
|
|
12
|
+
const pathname = normalizePath(input.pathname);
|
|
13
|
+
// Off the docs tree there is no owning version, so fall back to the current
|
|
14
|
+
// one: a `page` layout still gets the current sidebar and search scope.
|
|
15
|
+
const active = activeVersion(versions, pathname) ?? (versions.length ? currentVersion(versions) : undefined);
|
|
16
|
+
const current = currentVersion(versions);
|
|
17
|
+
// An archived version is frozen, so don't invite edits to it.
|
|
18
|
+
const isArchived = Boolean(active && !active.current);
|
|
19
|
+
const scopedContent = scopeContent(content, active?.id);
|
|
20
|
+
const nav = navFromContent(scopedContent);
|
|
21
|
+
const entry = scopedContent.find((item) => item.path === pathname);
|
|
22
|
+
const flatNav = flattenNav(nav);
|
|
23
|
+
const pageIndex = flatNav.findIndex((item) => item.url === pathname);
|
|
24
|
+
const title = pageIndex >= 0 ? flatNav[pageIndex].title : siteTitle;
|
|
25
|
+
const breadcrumbs = navTrail(nav, pathname) ?? [];
|
|
26
|
+
if (pageIndex >= 0)
|
|
27
|
+
breadcrumbs.push(title);
|
|
28
|
+
return {
|
|
29
|
+
pathname,
|
|
30
|
+
activeVersion: active,
|
|
31
|
+
activeVersionId: active?.id,
|
|
32
|
+
currentVersion: current,
|
|
33
|
+
isArchived,
|
|
34
|
+
scopedContent,
|
|
35
|
+
nav,
|
|
36
|
+
entry,
|
|
37
|
+
editHref: editUrl && entry?.sourcePath && !isArchived
|
|
38
|
+
? editUrl.replace(/\/$/, '') + '/' + entry.sourcePath
|
|
39
|
+
: undefined,
|
|
40
|
+
lastUpdated: parseDate(entry?.lastUpdated),
|
|
41
|
+
readingMinutes: entry?.readingTime,
|
|
42
|
+
prev: pageIndex > 0 ? flatNav[pageIndex - 1] : undefined,
|
|
43
|
+
next: pageIndex >= 0 && pageIndex < flatNav.length - 1 ? flatNav[pageIndex + 1] : undefined,
|
|
44
|
+
title,
|
|
45
|
+
breadcrumbs,
|
|
46
|
+
toc: entry?.toc ?? []
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
/** An ISO stamp as a Date, or `undefined` when absent or unparseable. */
|
|
50
|
+
function parseDate(iso) {
|
|
51
|
+
if (!iso)
|
|
52
|
+
return undefined;
|
|
53
|
+
const date = new Date(iso);
|
|
54
|
+
return Number.isNaN(date.getTime()) ? undefined : date;
|
|
55
|
+
}
|
package/dist/core/index.d.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
1
|
export { defineConfig, type DocsmithConfig, type DocsmithLink, type DocsmithFooterColumn, type DocsmithAnnouncement } from './config.js';
|
|
2
2
|
export type { DocsContentItem, SearchDoc, LlmsDoc } from './content.js';
|
|
3
|
+
export { resolveVersions, activeVersion, currentVersion, currentOnly, scopeContent, mapPathToVersion, type DocsVersion, type DocsVersions, type ResolvedVersion } from './version.js';
|
|
3
4
|
export { navFromContent, flattenNav, navTrail, isNavGroup, type NavItem, type NavGroup, type NavNode } from './nav.js';
|
|
5
|
+
export { resolveDocsPage, type DocsPageView } from './docs-page.js';
|
package/dist/core/index.js
CHANGED
|
@@ -3,4 +3,6 @@
|
|
|
3
3
|
// several of these (e.g. the shell) import from here; the public surface is
|
|
4
4
|
// re-exported through `src/lib/index.ts`.
|
|
5
5
|
export { defineConfig } from './config.js';
|
|
6
|
+
export { resolveVersions, activeVersion, currentVersion, currentOnly, scopeContent, mapPathToVersion } from './version.js';
|
|
6
7
|
export { navFromContent, flattenNav, navTrail, isNavGroup } from './nav.js';
|
|
8
|
+
export { resolveDocsPage } from './docs-page.js';
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Versioned-docs domain logic. Pure and framework-agnostic: the vite plugin
|
|
3
|
+
* resolves the author's versions against the docs URL base at build time and
|
|
4
|
+
* emits the manifest; `DocsShell` and the switcher read it at runtime.
|
|
5
|
+
*
|
|
6
|
+
* The current version is served unprefixed at the docs root and is the only one
|
|
7
|
+
* anyone edits; superseded releases are frozen copies under their own prefix.
|
|
8
|
+
* See `docs/adr/0001-unprefixed-current-docs.md`. All of it is a no-op when a
|
|
9
|
+
* site declares no versions.
|
|
10
|
+
*/
|
|
11
|
+
import type { DocsContentItem } from './content.js';
|
|
12
|
+
/** One documentation version, declared in the `docsmith()` vite plugin. */
|
|
13
|
+
export type DocsVersion = {
|
|
14
|
+
/** Stable id. Also the URL/directory segment for an archived version. */
|
|
15
|
+
id: string;
|
|
16
|
+
/** Label shown in the switcher and banner, e.g. `'v2'`. */
|
|
17
|
+
label: string;
|
|
18
|
+
/** Keep this version out of search engines. Defaults to `false`. */
|
|
19
|
+
noindex?: boolean;
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* A site's versions. `current` is the docs for the latest release: it lives at
|
|
23
|
+
* the docs root, is served unprefixed, and is the folder you edit. `archived`
|
|
24
|
+
* holds frozen copies of superseded releases, each in `<docs root>/<id>/`, in
|
|
25
|
+
* the order they should appear in the switcher (newest first).
|
|
26
|
+
*/
|
|
27
|
+
export type DocsVersions = {
|
|
28
|
+
current: DocsVersion;
|
|
29
|
+
archived?: DocsVersion[];
|
|
30
|
+
};
|
|
31
|
+
/** A {@link DocsVersion} with the URL fields the runtime needs, computed at build time. */
|
|
32
|
+
export type ResolvedVersion = DocsVersion & {
|
|
33
|
+
/** Absolute URL base: the docs root for `current`, `/docs/<id>` for an archive. */
|
|
34
|
+
basePath: string;
|
|
35
|
+
/** The version's first page in sidebar order, for the switcher's fallback. */
|
|
36
|
+
landing: string;
|
|
37
|
+
/** Whether this is the current version (as opposed to an archived one). */
|
|
38
|
+
current: boolean;
|
|
39
|
+
/** Whether search engines should skip this version. */
|
|
40
|
+
noindex: boolean;
|
|
41
|
+
};
|
|
42
|
+
/**
|
|
43
|
+
* Resolve author versions against the docs URL base (e.g. `/docs`) and the
|
|
44
|
+
* collected content: compute each version's `basePath` and its `landing` (first
|
|
45
|
+
* page in sidebar reading order). Emitted as the `versions` manifest in
|
|
46
|
+
* `svelte-docsmith/content`, current first, then archives in declared order.
|
|
47
|
+
*
|
|
48
|
+
* Returns an empty manifest for an unversioned site, which makes every
|
|
49
|
+
* downstream scoping, switcher and banner step a no-op.
|
|
50
|
+
*/
|
|
51
|
+
export declare function resolveVersions(versions: DocsVersions | undefined, docsBase: string, content: DocsContentItem[]): ResolvedVersion[];
|
|
52
|
+
/**
|
|
53
|
+
* The resolved version owning `pathname`, by longest matching `basePath`. An
|
|
54
|
+
* archive's base is longer than the current version's, so `/docs/v1/x` picks
|
|
55
|
+
* `v1` while `/docs/x` falls to the current version. `undefined` off the docs
|
|
56
|
+
* tree entirely, or on an unversioned site.
|
|
57
|
+
*/
|
|
58
|
+
export declare function activeVersion(versions: ResolvedVersion[], pathname: string): ResolvedVersion | undefined;
|
|
59
|
+
/** The current version: the docs for the latest release. */
|
|
60
|
+
export declare function currentVersion(versions: ResolvedVersion[]): ResolvedVersion | undefined;
|
|
61
|
+
/**
|
|
62
|
+
* Keep only the current version's pages, for `sitemap.xml` and `llms.txt`, so
|
|
63
|
+
* search engines and LLMs index one canonical set. A no-op when the site
|
|
64
|
+
* declares no versions. Works over any record carrying `version`.
|
|
65
|
+
*/
|
|
66
|
+
export declare function currentOnly<T extends {
|
|
67
|
+
version?: string;
|
|
68
|
+
}>(items: T[], versions: ResolvedVersion[]): T[];
|
|
69
|
+
/**
|
|
70
|
+
* Content scoped to one version. With no version id (unversioned site) the
|
|
71
|
+
* content passes through unchanged, so every caller stays a no-op off the
|
|
72
|
+
* versioned path.
|
|
73
|
+
*/
|
|
74
|
+
export declare function scopeContent(content: DocsContentItem[], versionId: string | undefined): DocsContentItem[];
|
|
75
|
+
/**
|
|
76
|
+
* Map the current page to its equivalent under `to`: swap the version base, and
|
|
77
|
+
* fall back to the target's landing page when that doc doesn't exist there.
|
|
78
|
+
* `targetPaths` is the set of the target version's page URLs.
|
|
79
|
+
*/
|
|
80
|
+
export declare function mapPathToVersion(pathname: string, from: ResolvedVersion, to: ResolvedVersion, targetPaths: Iterable<string>): string;
|