@eqtylab/docs 0.3.5 → 0.5.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/README.md +77 -12
- package/dist/{chunk-VJN3HXKM.js → chunk-LNQV7TVH.js} +18 -6
- package/dist/chunk-LNQV7TVH.js.map +1 -0
- package/dist/chunk-OM7745ZQ.js +77 -0
- package/dist/chunk-OM7745ZQ.js.map +1 -0
- package/dist/{chunk-IWKZQ4CW.js → chunk-VNQZJMDO.js} +4 -2
- package/dist/chunk-VNQZJMDO.js.map +1 -0
- package/dist/{chunk-K7PJRTK3.js → chunk-X4LT7ASQ.js} +31 -4
- package/dist/chunk-X4LT7ASQ.js.map +1 -0
- package/dist/config.d.ts +10 -1
- package/dist/config.js +1 -1
- package/dist/index.js +366 -29
- package/dist/index.js.map +1 -1
- package/dist/internal/extract-versions.d.ts +33 -0
- package/dist/internal/rehype-base-url.d.ts +9 -2
- package/dist/internal/rehype-base-url.js +1 -1
- package/dist/internal/remark-archive-document.d.ts +6 -0
- package/dist/internal/run-plugins.d.ts +22 -0
- package/dist/internal/virtual-config.d.ts +2 -0
- package/dist/loaders.d.ts +5 -5
- package/dist/loaders.js +18 -7
- package/dist/loaders.js.map +1 -1
- package/dist/nav.d.ts +4 -0
- package/dist/nav.js +21 -7
- package/dist/nav.js.map +1 -1
- package/dist/paths.d.ts +25 -3
- package/dist/paths.js +1 -1
- package/dist/runtime/chrome/GlobalSearch.tsx +7 -2
- package/dist/runtime/chrome/Header.astro +8 -3
- package/dist/runtime/chrome/NavDrawer.astro +44 -1
- package/dist/runtime/chrome/NavTree.astro +1 -11
- package/dist/runtime/chrome/Prose.astro +17 -3
- package/dist/runtime/chrome/VersionBanner.astro +19 -0
- package/dist/runtime/chrome/VersionSwitcher.tsx +105 -0
- package/dist/runtime/layouts/DocsPage.astro +36 -4
- package/dist/runtime/layouts/DocsShell.astro +4 -2
- package/dist/runtime/layouts/RedirectPage.astro +19 -0
- package/dist/runtime/lib/mdx-components.ts +4 -1
- package/dist/runtime/lib/nav-data.ts +44 -15
- package/dist/runtime/lib/versions-ui.ts +12 -0
- package/dist/runtime/routes/docs-md.ts +50 -11
- package/dist/runtime/routes/docs.astro +182 -73
- package/dist/runtime/routes/llms-txt.ts +2 -1
- package/dist/runtime/routes/not-found.astro +27 -0
- package/dist/schema.d.ts +1 -0
- package/dist/schema.js +1 -1
- package/dist/types.d.ts +14 -5
- package/dist/versions.d.ts +37 -0
- package/package.json +16 -16
- package/dist/chunk-GSSSQTEV.js +0 -51
- package/dist/chunk-GSSSQTEV.js.map +0 -1
- package/dist/chunk-IWKZQ4CW.js.map +0 -1
- package/dist/chunk-K7PJRTK3.js.map +0 -1
- package/dist/chunk-VJN3HXKM.js.map +0 -1
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
---
|
|
2
|
+
/** A one-line page for a release URL that has no copy of its own. Static hosting has no server redirects. */
|
|
3
|
+
import DocsShell from './DocsShell.astro';
|
|
4
|
+
|
|
5
|
+
interface Props {
|
|
6
|
+
to: string;
|
|
7
|
+
version: string | null;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const { to, version } = Astro.props;
|
|
11
|
+
const label = version ?? 'the latest docs';
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
<DocsShell title="Redirecting" noIndex canonical={to} version={version ?? undefined}>
|
|
15
|
+
<meta slot="head" http-equiv="refresh" content={`0; url=${to}`} />
|
|
16
|
+
<main class="text-text-secondary mx-auto max-w-prose px-6 py-24 text-sm">
|
|
17
|
+
<p>This release's docs live at <a href={to}>{label}</a>.</p>
|
|
18
|
+
</main>
|
|
19
|
+
</DocsShell>
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
TableRow,
|
|
13
13
|
} from '@eqtylab/equality';
|
|
14
14
|
import CONFIG from 'virtual:eqty-docs/config';
|
|
15
|
+
import pluginComponents from 'virtual:eqty-docs/plugin-components';
|
|
15
16
|
|
|
16
17
|
import AlertBridge from '../components/AlertBridge.astro';
|
|
17
18
|
import CodeFenceBridge from '../components/CodeFenceBridge.astro';
|
|
@@ -46,4 +47,6 @@ if (CONFIG.code?.highlighter === 'codeblock') {
|
|
|
46
47
|
base.pre = CodeFenceBridge;
|
|
47
48
|
}
|
|
48
49
|
|
|
49
|
-
|
|
50
|
+
// Plugin-contributed components (`docs({ plugins })`) land last, so a plugin can add a name or
|
|
51
|
+
// replace one of the defaults.
|
|
52
|
+
export const mdxComponents: Record<string, unknown> = { ...base, ...pluginComponents };
|
|
@@ -1,42 +1,64 @@
|
|
|
1
1
|
/** Bridges the content collections into the pure nav builder. Needs `astro:content`, so it lives in the runtime tree. */
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
breadcrumbsFor,
|
|
4
|
+
buildNavTree,
|
|
5
|
+
buildTocTree,
|
|
6
|
+
flattenNav,
|
|
7
|
+
prevNextFor,
|
|
8
|
+
} from '@eqtylab/docs/nav';
|
|
3
9
|
import type { GroupConfig } from '@eqtylab/docs/nav';
|
|
10
|
+
import { docsHref } from '@eqtylab/docs/paths';
|
|
4
11
|
import type { DocsNavEntry, NavNode } from '@eqtylab/docs/types';
|
|
5
12
|
import { getCollection } from 'astro:content';
|
|
6
13
|
import CONFIG from 'virtual:eqty-docs/config';
|
|
7
14
|
|
|
8
15
|
export { buildTocTree, breadcrumbsFor, prevNextFor };
|
|
9
16
|
|
|
10
|
-
/**
|
|
11
|
-
export function pathContext() {
|
|
17
|
+
/** The version is a route segment after the path prefix; latest has none. */
|
|
18
|
+
export function pathContext(versionId?: string) {
|
|
12
19
|
return {
|
|
13
20
|
base: import.meta.env.BASE_URL,
|
|
14
21
|
pathPrefix: CONFIG.pathPrefix,
|
|
22
|
+
versionPrefix: versionId,
|
|
15
23
|
};
|
|
16
24
|
}
|
|
17
25
|
|
|
18
|
-
/**
|
|
19
|
-
export
|
|
20
|
-
|
|
21
|
-
|
|
26
|
+
/** Collection names for a version id, or for latest when undefined. */
|
|
27
|
+
export function collectionsFor(versionId?: string): { docs: string; groups: string } {
|
|
28
|
+
if (!versionId) return { docs: 'docs', groups: 'docsGroups' };
|
|
29
|
+
const entry = CONFIG.versionManifest.find((v) => v.id === versionId);
|
|
30
|
+
if (!entry) throw new Error(`[@eqtylab/docs] unknown version id "${versionId}"`);
|
|
31
|
+
return { docs: `docs_${entry.suffix}`, groups: `docsGroups_${entry.suffix}` };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// `getCollection` is typed against the generated DataEntryMap, which cannot know names built at runtime.
|
|
35
|
+
const anyCollection = getCollection as unknown as (
|
|
36
|
+
name: string,
|
|
37
|
+
filter?: (entry: { data: Record<string, unknown> }) => boolean
|
|
38
|
+
) => Promise<never[]>;
|
|
39
|
+
|
|
40
|
+
/** All entries of a version, with drafts filtered out unless we're in `astro dev`. */
|
|
41
|
+
export async function docsEntries(versionId?: string) {
|
|
42
|
+
return anyCollection(collectionsFor(versionId).docs, ({ data }) =>
|
|
43
|
+
import.meta.env.DEV ? true : !(data as { draft?: boolean }).draft
|
|
22
44
|
);
|
|
23
45
|
}
|
|
24
46
|
|
|
25
|
-
async function groupMap(): Promise<Map<string, GroupConfig>> {
|
|
47
|
+
async function groupMap(versionId?: string): Promise<Map<string, GroupConfig>> {
|
|
26
48
|
const map = new Map<string, GroupConfig>();
|
|
27
49
|
try {
|
|
28
|
-
const groups = await
|
|
29
|
-
for (const group of groups) {
|
|
30
|
-
map.set(group.id, group.data
|
|
50
|
+
const groups = await anyCollection(collectionsFor(versionId).groups);
|
|
51
|
+
for (const group of groups as Array<{ id: string; data: GroupConfig }>) {
|
|
52
|
+
map.set(group.id, group.data);
|
|
31
53
|
}
|
|
32
54
|
} catch {
|
|
33
|
-
//
|
|
55
|
+
// The groups collection is optional; no _group.yaml means alphabetical ordering.
|
|
34
56
|
}
|
|
35
57
|
return map;
|
|
36
58
|
}
|
|
37
59
|
|
|
38
|
-
export async function docsNav(currentPath: string): Promise<NavNode[]> {
|
|
39
|
-
const [entries, groups] = await Promise.all([docsEntries(), groupMap()]);
|
|
60
|
+
export async function docsNav(currentPath: string, versionId?: string): Promise<NavNode[]> {
|
|
61
|
+
const [entries, groups] = await Promise.all([docsEntries(versionId), groupMap(versionId)]);
|
|
40
62
|
|
|
41
63
|
const navEntries: DocsNavEntry[] = entries.map((entry) => ({
|
|
42
64
|
id: entry.id,
|
|
@@ -52,9 +74,10 @@ export async function docsNav(currentPath: string): Promise<NavNode[]> {
|
|
|
52
74
|
entries: navEntries,
|
|
53
75
|
groups,
|
|
54
76
|
currentPath,
|
|
55
|
-
paths: pathContext(),
|
|
77
|
+
paths: pathContext(versionId),
|
|
56
78
|
defaultCollapsed: CONFIG.sidebar.collapsed,
|
|
57
79
|
defaultSort: CONFIG.sidebar.sort,
|
|
80
|
+
defaultIndexLabel: CONFIG.sidebar.indexLabel,
|
|
58
81
|
extra: (CONFIG.sidebar.extra ?? []) as NavNode[],
|
|
59
82
|
onWarn: warnOnce,
|
|
60
83
|
});
|
|
@@ -72,3 +95,9 @@ function deprecationBadge(deprecated: unknown) {
|
|
|
72
95
|
if (!deprecated) return undefined;
|
|
73
96
|
return { text: 'Deprecated', variant: 'warning' as const };
|
|
74
97
|
}
|
|
98
|
+
|
|
99
|
+
/** The first linkable page of a version. Version roots and switcher fallbacks land here. */
|
|
100
|
+
export async function firstNavHref(versionId?: string): Promise<string> {
|
|
101
|
+
const tree = await docsNav('/', versionId);
|
|
102
|
+
return flattenNav(tree)[0]?.href ?? docsHref('', pathContext(versionId));
|
|
103
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/** Shapes the chrome receives. Computed at build in the route; the components hold no logic. */
|
|
2
|
+
export interface SwitcherItem {
|
|
3
|
+
label: string;
|
|
4
|
+
href: string;
|
|
5
|
+
current: boolean;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface SwitcherData {
|
|
9
|
+
latest: SwitcherItem;
|
|
10
|
+
/** One group per major, newest first; items newest first. */
|
|
11
|
+
groups: Array<{ label: string; items: SwitcherItem[] }>;
|
|
12
|
+
}
|
|
@@ -4,30 +4,69 @@ import type { APIRoute } from 'astro';
|
|
|
4
4
|
import { getCollection } from 'astro:content';
|
|
5
5
|
import CONFIG from 'virtual:eqty-docs/config';
|
|
6
6
|
|
|
7
|
+
import { collectionsFor } from '../lib/nav-data.ts';
|
|
8
|
+
|
|
7
9
|
export async function getStaticPaths() {
|
|
8
|
-
const
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
10
|
+
const visible = ({ data }: { data: { draft?: boolean; noIndex?: boolean } }) =>
|
|
11
|
+
!data.draft && !data.noIndex;
|
|
12
|
+
const anyCollection = getCollection as unknown as (
|
|
13
|
+
name: string,
|
|
14
|
+
f: typeof visible
|
|
15
|
+
) => Promise<never[]>;
|
|
16
|
+
const versions: Array<string | undefined> = [
|
|
17
|
+
undefined,
|
|
18
|
+
...CONFIG.versionManifest.map((v) => v.id),
|
|
19
|
+
];
|
|
20
|
+
const out: Array<{ params: { slug: string }; props: { entry: unknown } }> = [];
|
|
21
|
+
for (const versionId of versions) {
|
|
22
|
+
const entries = (await anyCollection(collectionsFor(versionId).docs, visible)) as Array<{
|
|
23
|
+
id: string;
|
|
24
|
+
}>;
|
|
25
|
+
for (const entry of entries) {
|
|
26
|
+
// An empty slug would emit a file literally called ".md".
|
|
27
|
+
const page = idToPath(entry.id) || 'index';
|
|
28
|
+
out.push({ params: { slug: versionId ? `${versionId}/${page}` : page }, props: { entry } });
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return out;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
type Deprecated = boolean | { message?: string; replacedBy?: string } | undefined;
|
|
35
|
+
|
|
36
|
+
/** Agents read the twin, not the rendered banner, so the deprecation has to travel with it. */
|
|
37
|
+
function deprecationNotice(deprecated: Deprecated) {
|
|
38
|
+
if (!deprecated) return [];
|
|
39
|
+
const detail = typeof deprecated === 'object' ? deprecated : {};
|
|
40
|
+
const message = stripHtml(detail.message) ?? 'This page documents a deprecated feature.';
|
|
41
|
+
const replacedBy = detail.replacedBy ? ` Use ${stripHtml(detail.replacedBy)} instead.` : '';
|
|
42
|
+
return [`> **Deprecated** — ${message}${replacedBy}`, ''];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Frontmatter messages may carry authored HTML links, which are noise in plain Markdown. */
|
|
46
|
+
function stripHtml(value?: string) {
|
|
47
|
+
return value?.replace(/<[^>]*>/g, '').trim() || undefined;
|
|
17
48
|
}
|
|
18
49
|
|
|
19
50
|
export const GET: APIRoute = ({ props }) => {
|
|
20
51
|
const { entry } = props as {
|
|
21
|
-
entry: {
|
|
52
|
+
entry: {
|
|
53
|
+
body?: string;
|
|
54
|
+
data: { title: string; description?: string; deprecated?: Deprecated };
|
|
55
|
+
};
|
|
22
56
|
};
|
|
23
57
|
|
|
24
58
|
const frontmatter = ['---', `title: ${JSON.stringify(entry.data.title)}`];
|
|
25
59
|
if (entry.data.description) {
|
|
26
60
|
frontmatter.push(`description: ${JSON.stringify(entry.data.description)}`);
|
|
27
61
|
}
|
|
62
|
+
if (entry.data.deprecated) {
|
|
63
|
+
frontmatter.push('deprecated: true');
|
|
64
|
+
}
|
|
28
65
|
frontmatter.push(`source: ${JSON.stringify(CONFIG.title)}`, '---', '');
|
|
29
66
|
|
|
30
|
-
|
|
67
|
+
const body = [...deprecationNotice(entry.data.deprecated), entry.body ?? ''].join('\n');
|
|
68
|
+
|
|
69
|
+
return new Response(frontmatter.join('\n') + body, {
|
|
31
70
|
headers: { 'Content-Type': 'text/markdown; charset=utf-8' },
|
|
32
71
|
});
|
|
33
72
|
};
|
|
@@ -1,92 +1,201 @@
|
|
|
1
1
|
---
|
|
2
|
-
import {
|
|
2
|
+
import { render } from 'astro:content';
|
|
3
3
|
import CONFIG from 'virtual:eqty-docs/config';
|
|
4
4
|
import { docsHref, idToPath, joinPath, normalizePath } from '@eqtylab/docs/paths';
|
|
5
5
|
import DocsPage from '../layouts/DocsPage.astro';
|
|
6
|
-
import
|
|
6
|
+
import RedirectPage from '../layouts/RedirectPage.astro';
|
|
7
|
+
import {
|
|
8
|
+
breadcrumbsFor,
|
|
9
|
+
buildTocTree,
|
|
10
|
+
docsEntries,
|
|
11
|
+
docsNav,
|
|
12
|
+
firstNavHref,
|
|
13
|
+
pathContext,
|
|
14
|
+
prevNextFor,
|
|
15
|
+
} from '../lib/nav-data.ts';
|
|
7
16
|
import { firstParagraph } from '../lib/summary.ts';
|
|
8
17
|
import { mdxComponents } from '../lib/mdx-components.ts';
|
|
18
|
+
import type { SwitcherData } from '../lib/versions-ui.ts';
|
|
9
19
|
import PageFooter from '../chrome/PageFooter.astro';
|
|
10
20
|
import Prose from '../chrome/Prose.astro';
|
|
11
21
|
|
|
22
|
+
type Entry = Awaited<ReturnType<typeof docsEntries>>[number] & {
|
|
23
|
+
id: string;
|
|
24
|
+
body?: string;
|
|
25
|
+
filePath?: string;
|
|
26
|
+
data: Record<string, any>;
|
|
27
|
+
};
|
|
28
|
+
|
|
12
29
|
export async function getStaticPaths() {
|
|
13
|
-
const
|
|
14
|
-
|
|
30
|
+
const owned = new Set((CONFIG.ownedByConsumer ?? []).map(normalizePath));
|
|
31
|
+
// Astro does not de-duplicate injected routes against consumer pages. Latest only: a consumer cannot own a versioned path.
|
|
32
|
+
const latest = ((await docsEntries()) as Entry[]).filter(
|
|
33
|
+
(entry) => !owned.has(normalizePath(docsHref(entry.id, pathContext())))
|
|
34
|
+
);
|
|
35
|
+
const copies = await Promise.all(
|
|
36
|
+
CONFIG.versionManifest.map(async (v) => ({ v, entries: (await docsEntries(v.id)) as Entry[] }))
|
|
15
37
|
);
|
|
16
38
|
|
|
17
|
-
const
|
|
39
|
+
const idsOf = new Map<string | undefined, Set<string>>([
|
|
40
|
+
[undefined, new Set(latest.map((e) => e.id))],
|
|
41
|
+
]);
|
|
42
|
+
for (const { v, entries } of copies) idsOf.set(v.id, new Set(entries.map((e) => e.id)));
|
|
43
|
+
const roots = new Map<string | undefined, string>();
|
|
44
|
+
for (const v of CONFIG.versionManifest) roots.set(v.id, await firstNavHref(v.id));
|
|
45
|
+
|
|
46
|
+
const latestLabel = CONFIG.currentVersion ? CONFIG.currentVersion.id : 'latest';
|
|
47
|
+
const hrefFor = (versionId: string | undefined, id: string) =>
|
|
48
|
+
idsOf.get(versionId)?.has(id)
|
|
49
|
+
? docsHref(id, pathContext(versionId))
|
|
50
|
+
: versionId
|
|
51
|
+
? docsHref('', pathContext(versionId))
|
|
52
|
+
: docsHref('', pathContext());
|
|
18
53
|
|
|
19
|
-
|
|
20
|
-
//
|
|
21
|
-
|
|
22
|
-
.
|
|
54
|
+
const switcherFor = (versionId: string | undefined, id: string): SwitcherData | undefined => {
|
|
55
|
+
// Nothing to switch to until a release is tagged: a picker offering only the page you are
|
|
56
|
+
// already on is noise, and it displaces the header's control cluster.
|
|
57
|
+
if (!CONFIG.currentVersion || !CONFIG.versionManifest.length) return undefined;
|
|
58
|
+
const byMajor = new Map<string, SwitcherData['groups'][number]>();
|
|
59
|
+
for (const v of CONFIG.versionManifest) {
|
|
60
|
+
const major = `v${v.group.split('.')[0]}`;
|
|
61
|
+
if (!byMajor.has(major)) byMajor.set(major, { label: major, items: [] });
|
|
62
|
+
byMajor
|
|
63
|
+
.get(major)!
|
|
64
|
+
.items.push({ label: v.id, href: hrefFor(v.id, id), current: v.id === versionId });
|
|
65
|
+
}
|
|
66
|
+
return {
|
|
67
|
+
latest: { label: latestLabel, href: hrefFor(undefined, id), current: versionId === undefined },
|
|
68
|
+
groups: [...byMajor.values()],
|
|
69
|
+
};
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
return [
|
|
73
|
+
...latest.map((entry) => ({
|
|
23
74
|
// `undefined` (not '') is how a rest param matches the empty path.
|
|
24
75
|
params: { slug: idToPath(entry.id) || undefined },
|
|
25
|
-
props: { entry },
|
|
26
|
-
}))
|
|
76
|
+
props: { entry, switcher: switcherFor(undefined, entry.id) },
|
|
77
|
+
})),
|
|
78
|
+
...copies.flatMap(({ v, entries }) => [
|
|
79
|
+
...entries.map((entry) => ({
|
|
80
|
+
params: { slug: `${v.id}/${idToPath(entry.id)}`.replace(/\/$/, '') },
|
|
81
|
+
props: {
|
|
82
|
+
entry,
|
|
83
|
+
version: v.id,
|
|
84
|
+
switcher: switcherFor(v.id, entry.id),
|
|
85
|
+
canonical: idsOf.get(undefined)!.has(entry.id)
|
|
86
|
+
? docsHref(entry.id, pathContext())
|
|
87
|
+
: undefined,
|
|
88
|
+
latestHref: hrefFor(undefined, entry.id),
|
|
89
|
+
latestLabel,
|
|
90
|
+
},
|
|
91
|
+
})),
|
|
92
|
+
// 3.9.1 had no index.mdx, so without this the version root and every stub pointing at it would 404.
|
|
93
|
+
...(idsOf.get(v.id)!.has('index')
|
|
94
|
+
? []
|
|
95
|
+
: [{ params: { slug: v.id }, props: { redirectTo: roots.get(v.id)!, version: v.id } }]),
|
|
96
|
+
]),
|
|
97
|
+
// Every target goes through docsHref so base and pathPrefix apply.
|
|
98
|
+
...CONFIG.versionRedirects.map((r) => ({
|
|
99
|
+
params: { slug: r.id },
|
|
100
|
+
props: {
|
|
101
|
+
redirectTo: r.to
|
|
102
|
+
? (roots.get(r.to) ?? docsHref('', pathContext(r.to)))
|
|
103
|
+
: docsHref('', pathContext()),
|
|
104
|
+
version: r.to,
|
|
105
|
+
},
|
|
106
|
+
})),
|
|
107
|
+
];
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
interface Props {
|
|
111
|
+
entry?: Entry;
|
|
112
|
+
version?: string | null;
|
|
113
|
+
switcher?: SwitcherData;
|
|
114
|
+
canonical?: string;
|
|
115
|
+
latestHref?: string;
|
|
116
|
+
latestLabel?: string;
|
|
117
|
+
redirectTo?: string;
|
|
27
118
|
}
|
|
28
119
|
|
|
29
|
-
const { entry } = Astro.props;
|
|
30
|
-
const { Content, headings } = await render(entry);
|
|
31
|
-
|
|
32
|
-
const tocConfig = entry.data.tableOfContents ?? CONFIG.tableOfContents;
|
|
33
|
-
const showToc = tocConfig !== false;
|
|
34
|
-
const toc = showToc
|
|
35
|
-
? buildTocTree(headings, {
|
|
36
|
-
minLevel: (tocConfig as { minLevel?: number })?.minLevel ?? 2,
|
|
37
|
-
maxLevel: (tocConfig as { maxLevel?: number })?.maxLevel ?? 3,
|
|
38
|
-
})
|
|
39
|
-
: [];
|
|
40
|
-
|
|
41
|
-
const nav = await docsNav(Astro.url.pathname);
|
|
42
|
-
const splash = entry.data.template === 'splash';
|
|
43
|
-
|
|
44
|
-
// Trail is [group, ..., page]; a length of one means the page has no section above it.
|
|
45
|
-
const trail = breadcrumbsFor(nav, Astro.url.pathname);
|
|
46
|
-
const crumbs = trail.length > 1 ? trail.map((node) => node.label).join(' › ') : undefined;
|
|
47
|
-
|
|
48
|
-
// Ancestors only. `crumbs` above keeps the page as well, because a search result row
|
|
49
|
-
// needs the page name to identify itself. The two diverge on purpose.
|
|
50
|
-
const renderedTrail = trail.slice(0, -1).map((node) => ({ label: node.label, href: node.href }));
|
|
51
|
-
|
|
52
|
-
// Indexed only, never rendered: a page with no `description` still needs a result summary.
|
|
53
|
-
const fallbackSummary = entry.data.description ? undefined : firstParagraph(entry.body);
|
|
54
|
-
|
|
55
|
-
const { prev, next } = CONFIG.footer.showPrevNext ? prevNextFor(nav, Astro.url.pathname) : {};
|
|
56
|
-
|
|
57
|
-
// `editUrl` is a base; the content path is appended. The slice is taken at the
|
|
58
|
-
// consumer's own content directory, which is configurable.
|
|
59
|
-
const contentPath = entry.filePath?.split(`${CONFIG.contentDir}/`).pop();
|
|
60
|
-
const editHref =
|
|
61
|
-
CONFIG.footer.editUrl && contentPath
|
|
62
|
-
? `${CONFIG.footer.editUrl.replace(/\/$/, '')}/${contentPath}`
|
|
63
|
-
: undefined;
|
|
64
|
-
|
|
65
|
-
// File-shaped (/x/label.md) beside the directory-shaped page; 'index' must match docs-md.ts.
|
|
66
|
-
const markdownHref = CONFIG.routing.markdownTwins
|
|
67
|
-
? `${joinPath(import.meta.env.BASE_URL, CONFIG.pathPrefix, idToPath(entry.id) || 'index')}.md`
|
|
68
|
-
: undefined;
|
|
120
|
+
const { entry, version, switcher, canonical, latestHref, latestLabel, redirectTo } = Astro.props;
|
|
69
121
|
---
|
|
70
122
|
|
|
71
|
-
<
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
123
|
+
{redirectTo && <RedirectPage to={redirectTo} version={version ?? null} />}
|
|
124
|
+
{
|
|
125
|
+
!redirectTo &&
|
|
126
|
+
entry &&
|
|
127
|
+
(async () => {
|
|
128
|
+
const { Content, headings } = await render(entry as never);
|
|
129
|
+
|
|
130
|
+
const tocConfig = entry.data.tableOfContents ?? CONFIG.tableOfContents;
|
|
131
|
+
const showToc = tocConfig !== false;
|
|
132
|
+
const toc = showToc
|
|
133
|
+
? buildTocTree(headings, {
|
|
134
|
+
minLevel: (tocConfig as { minLevel?: number })?.minLevel ?? 2,
|
|
135
|
+
maxLevel: (tocConfig as { maxLevel?: number })?.maxLevel ?? 3,
|
|
136
|
+
})
|
|
137
|
+
: [];
|
|
138
|
+
|
|
139
|
+
const nav = await docsNav(Astro.url.pathname, version ?? undefined);
|
|
140
|
+
const splash = entry.data.template === 'splash';
|
|
141
|
+
|
|
142
|
+
const trail = breadcrumbsFor(nav, Astro.url.pathname);
|
|
143
|
+
const crumbs = trail.length > 1 ? trail.map((node) => node.label).join(' › ') : undefined;
|
|
144
|
+
|
|
145
|
+
// Ancestors only. `crumbs` above keeps the page as well, because a search result row
|
|
146
|
+
// needs the page name to identify itself. The two diverge on purpose.
|
|
147
|
+
const renderedTrail = trail.slice(0, -1).map((node) => ({ label: node.label, href: node.href }));
|
|
148
|
+
|
|
149
|
+
// Indexed only, never rendered: a page with no `description` still needs a result summary.
|
|
150
|
+
const fallbackSummary = entry.data.description ? undefined : firstParagraph(entry.body);
|
|
151
|
+
|
|
152
|
+
const { prev, next } = CONFIG.footer.showPrevNext ? prevNextFor(nav, Astro.url.pathname) : {};
|
|
153
|
+
|
|
154
|
+
// Old versions are read-only and their files live in the cache, so the split below would
|
|
155
|
+
// return the whole cache path and produce a bogus link. Latest only.
|
|
156
|
+
const contentPath = version ? undefined : entry.filePath?.split(`${CONFIG.contentDir}/`).pop();
|
|
157
|
+
const editHref =
|
|
158
|
+
CONFIG.footer.editUrl && contentPath
|
|
159
|
+
? `${CONFIG.footer.editUrl.replace(/\/$/, '')}/${contentPath}`
|
|
160
|
+
: undefined;
|
|
161
|
+
|
|
162
|
+
// File-shaped (/v3.9/x/label.md) beside the directory-shaped page; 'index' must match docs-md.ts.
|
|
163
|
+
const markdownHref = CONFIG.routing.markdownTwins
|
|
164
|
+
? `${joinPath(import.meta.env.BASE_URL, CONFIG.pathPrefix, version ?? undefined, idToPath(entry.id) || 'index')}.md`
|
|
165
|
+
: undefined;
|
|
166
|
+
|
|
167
|
+
return (
|
|
168
|
+
<DocsPage
|
|
169
|
+
title={entry.data.title}
|
|
170
|
+
description={entry.data.description}
|
|
171
|
+
nav={nav}
|
|
172
|
+
toc={toc}
|
|
173
|
+
showToc={showToc}
|
|
174
|
+
splash={splash}
|
|
175
|
+
version={version ?? undefined}
|
|
176
|
+
switcher={switcher}
|
|
177
|
+
noIndex={Boolean(version)}
|
|
178
|
+
canonical={canonical}
|
|
179
|
+
latestHref={latestHref}
|
|
180
|
+
latestLabel={latestLabel}
|
|
181
|
+
>
|
|
182
|
+
<Prose
|
|
183
|
+
title={entry.data.title}
|
|
184
|
+
description={entry.data.description}
|
|
185
|
+
deprecated={entry.data.deprecated}
|
|
186
|
+
markdownHref={markdownHref}
|
|
187
|
+
source={entry.body}
|
|
188
|
+
crumbs={crumbs}
|
|
189
|
+
trail={renderedTrail}
|
|
190
|
+
fallbackSummary={fallbackSummary}
|
|
191
|
+
version={version ?? undefined}
|
|
192
|
+
>
|
|
193
|
+
<Content components={mdxComponents} />
|
|
194
|
+
</Prose>
|
|
195
|
+
{!splash && (
|
|
196
|
+
<PageFooter prev={prev} next={next} editHref={editHref} text={CONFIG.footer.text} />
|
|
197
|
+
)}
|
|
198
|
+
</DocsPage>
|
|
199
|
+
);
|
|
200
|
+
})()
|
|
201
|
+
}
|
|
@@ -26,8 +26,9 @@ export const GET: APIRoute = async (ctx) => {
|
|
|
26
26
|
'',
|
|
27
27
|
...sorted.map((entry) => {
|
|
28
28
|
const href = docsHref(entry.id, paths).replace(/\/$/, '');
|
|
29
|
+
const prefix = entry.data.deprecated ? ' (deprecated)' : '';
|
|
29
30
|
const suffix = entry.data.description ? `: ${entry.data.description}` : '';
|
|
30
|
-
return `- [${entry.data.title}](${origin}${href}.md)${suffix}`;
|
|
31
|
+
return `- [${entry.data.title}](${origin}${href}.md)${prefix}${suffix}`;
|
|
31
32
|
}),
|
|
32
33
|
'',
|
|
33
34
|
];
|
|
@@ -8,9 +8,36 @@ import NotFoundBody from '../chrome/NotFoundBody.tsx';
|
|
|
8
8
|
const homeHref = withBase(CONFIG.pathPrefix ? `/${CONFIG.pathPrefix}/` : '/', {
|
|
9
9
|
base: import.meta.env.BASE_URL,
|
|
10
10
|
});
|
|
11
|
+
|
|
12
|
+
// One static file serves every miss, so the version has to be read from the URL in the browser.
|
|
13
|
+
// The tables ride in a JSON script rather than `define:vars`, which would force `is:inline` and
|
|
14
|
+
// a second copy of the matching logic that nothing tests.
|
|
15
|
+
const fallback = {
|
|
16
|
+
base: import.meta.env.BASE_URL,
|
|
17
|
+
pathPrefix: CONFIG.pathPrefix,
|
|
18
|
+
copyIds: CONFIG.versionManifest.map((v) => v.id),
|
|
19
|
+
redirects: CONFIG.versionRedirects,
|
|
20
|
+
};
|
|
11
21
|
---
|
|
12
22
|
|
|
13
23
|
<DocsShell title="Page not found" noIndex>
|
|
14
24
|
<Header />
|
|
25
|
+
<script
|
|
26
|
+
type="application/json"
|
|
27
|
+
data-eq-docs-fallback
|
|
28
|
+
is:inline
|
|
29
|
+
set:html={JSON.stringify(fallback)}
|
|
30
|
+
/>
|
|
15
31
|
<NotFoundBody homeHref={homeHref} client:load />
|
|
16
32
|
</DocsShell>
|
|
33
|
+
|
|
34
|
+
<script>
|
|
35
|
+
import { resolveVersionedPath } from '@eqtylab/docs/paths';
|
|
36
|
+
|
|
37
|
+
const source = document.querySelector('[data-eq-docs-fallback]')?.textContent;
|
|
38
|
+
if (source) {
|
|
39
|
+
const target = resolveVersionedPath(location.pathname, JSON.parse(source));
|
|
40
|
+
// A reader who typed a real release URL should reach the page, not the 404 body.
|
|
41
|
+
if (target && target !== location.pathname) location.replace(target);
|
|
42
|
+
}
|
|
43
|
+
</script>
|
package/dist/schema.d.ts
CHANGED
|
@@ -69,6 +69,7 @@ export declare function groupSchema(): z.ZodObject<{
|
|
|
69
69
|
label: z.ZodOptional<z.ZodString>;
|
|
70
70
|
icon: z.ZodOptional<z.ZodString>;
|
|
71
71
|
order: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
72
|
+
indexLabel: z.ZodOptional<z.ZodString>;
|
|
72
73
|
sort: z.ZodOptional<z.ZodEnum<{
|
|
73
74
|
alpha: "alpha";
|
|
74
75
|
filename: "filename";
|
package/dist/schema.js
CHANGED
package/dist/types.d.ts
CHANGED
|
@@ -45,16 +45,25 @@ export interface DocsNavEntry {
|
|
|
45
45
|
hidden?: boolean;
|
|
46
46
|
draft?: boolean;
|
|
47
47
|
}
|
|
48
|
-
/**
|
|
48
|
+
/**
|
|
49
|
+
* Extension point for generated sections (OpenAPI reference, changelogs, ...). Passed to
|
|
50
|
+
* `docs({ plugins })`; `setup` runs inside `astro:config:setup`, after the framework has planned
|
|
51
|
+
* its own routes.
|
|
52
|
+
*/
|
|
49
53
|
export interface DocsPlugin {
|
|
50
54
|
name: string;
|
|
51
55
|
setup(ctx: DocsPluginContext): void | Promise<void>;
|
|
52
56
|
}
|
|
53
57
|
export interface DocsPluginContext {
|
|
54
|
-
/** Push a top-level group into the sidebar. Appended after folder-derived nodes. */
|
|
58
|
+
/** Push a top-level group into the sidebar. Appended after folder-derived nodes and `sidebar.extra`. */
|
|
55
59
|
addNavGroup(group: NavNode): void;
|
|
56
|
-
/**
|
|
57
|
-
|
|
58
|
-
|
|
60
|
+
/**
|
|
61
|
+
* Add components to the route-level MDX component map, by name. Each value is a module
|
|
62
|
+
* specifier (a package export or an absolute path) whose default export is the component;
|
|
63
|
+
* the runtime imports it, so a component object cannot be passed here. A plugin's entry
|
|
64
|
+
* replaces a default of the same name.
|
|
65
|
+
*/
|
|
66
|
+
addMdxComponents(map: Record<string, string>): void;
|
|
67
|
+
/** Raw `astro:config:setup` params: config, injectRoute, updateConfig, addWatchFile, logger. */
|
|
59
68
|
astro: unknown;
|
|
60
69
|
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/** Pure. Turns a tag list into the copies to build and the release URLs that redirect to them. */
|
|
2
|
+
export type Granularity = 'major' | 'minor' | 'patch';
|
|
3
|
+
export interface ParsedTag {
|
|
4
|
+
tag: string;
|
|
5
|
+
major: number;
|
|
6
|
+
minor: number;
|
|
7
|
+
patch: number;
|
|
8
|
+
version: string;
|
|
9
|
+
}
|
|
10
|
+
export interface VersionCopy {
|
|
11
|
+
id: string;
|
|
12
|
+
suffix: string;
|
|
13
|
+
group: string;
|
|
14
|
+
tag: string;
|
|
15
|
+
}
|
|
16
|
+
/** `to: null` means latest, i.e. `/`. */
|
|
17
|
+
export interface VersionRedirect {
|
|
18
|
+
id: string;
|
|
19
|
+
to: string | null;
|
|
20
|
+
}
|
|
21
|
+
export interface VersionSelection {
|
|
22
|
+
/** Newest first. */
|
|
23
|
+
copies: VersionCopy[];
|
|
24
|
+
redirects: VersionRedirect[];
|
|
25
|
+
/** Tags that did not parse as MAJOR.MINOR.PATCH, prereleases included. */
|
|
26
|
+
skipped: string[];
|
|
27
|
+
/** Parsed tags above `current`. */
|
|
28
|
+
above: string[];
|
|
29
|
+
}
|
|
30
|
+
export declare function parseTag(tag: string): ParsedTag | null;
|
|
31
|
+
export declare function compareTags(a: ParsedTag, b: ParsedTag): number;
|
|
32
|
+
export declare function groupOf(v: ParsedTag, granularity: Granularity): string;
|
|
33
|
+
export declare function idOf(group: string): string;
|
|
34
|
+
/** Collection names are identifiers, so `v3.9` becomes `v3_9`. */
|
|
35
|
+
export declare function suffixOf(id: string): string;
|
|
36
|
+
export declare function highestTag(tags: string[]): ParsedTag | null;
|
|
37
|
+
export declare function selectVersions(tags: string[], current: string, granularity: Granularity, skippedGroups?: string[]): VersionSelection;
|