@eqtylab/docs 0.3.0 → 0.3.1

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.
Files changed (45) hide show
  1. package/LICENSE +190 -0
  2. package/README.md +52 -10
  3. package/dist/config.js +1 -1
  4. package/dist/index.js +16 -5
  5. package/dist/index.js.map +1 -1
  6. package/package.json +2 -2
  7. package/dist/runtime/chrome/GlobalSearch.module.css +0 -26
  8. package/dist/runtime/chrome/GlobalSearch.tsx +0 -422
  9. package/dist/runtime/chrome/Header.astro +0 -105
  10. package/dist/runtime/chrome/LinkIcon.astro +0 -35
  11. package/dist/runtime/chrome/NavDrawer.astro +0 -60
  12. package/dist/runtime/chrome/NavTree.astro +0 -112
  13. package/dist/runtime/chrome/NotFoundBody.tsx +0 -12
  14. package/dist/runtime/chrome/PageFooter.astro +0 -80
  15. package/dist/runtime/chrome/Prose.astro +0 -169
  16. package/dist/runtime/chrome/Sidebar.astro +0 -21
  17. package/dist/runtime/chrome/TableOfContents.astro +0 -86
  18. package/dist/runtime/chrome/ThemeToggle.tsx +0 -85
  19. package/dist/runtime/chrome/TocElbow.astro +0 -30
  20. package/dist/runtime/chrome/TocList.astro +0 -43
  21. package/dist/runtime/components/AlertBridge.astro +0 -16
  22. package/dist/runtime/components/CodeFence.astro +0 -38
  23. package/dist/runtime/components/CodeFenceBridge.astro +0 -20
  24. package/dist/runtime/components/Link.astro +0 -21
  25. package/dist/runtime/components/TableBridge.astro +0 -18
  26. package/dist/runtime/components/index.ts +0 -2
  27. package/dist/runtime/layouts/DocsPage.astro +0 -50
  28. package/dist/runtime/layouts/DocsShell.astro +0 -92
  29. package/dist/runtime/lib/mdx-components.ts +0 -43
  30. package/dist/runtime/lib/nav-data.ts +0 -74
  31. package/dist/runtime/lib/summary.ts +0 -57
  32. package/dist/runtime/lib/theme.ts +0 -84
  33. package/dist/runtime/routes/docs-md.ts +0 -33
  34. package/dist/runtime/routes/docs.astro +0 -92
  35. package/dist/runtime/routes/llms-txt.ts +0 -38
  36. package/dist/runtime/routes/not-found.astro +0 -16
  37. package/dist/runtime/scripts/eq-copy.ts +0 -27
  38. package/dist/runtime/scripts/eq-highlight.ts +0 -24
  39. package/dist/runtime/scripts/eq-nav-drawer.ts +0 -31
  40. package/dist/runtime/scripts/eq-nav-group.ts +0 -52
  41. package/dist/runtime/scripts/eq-toc.ts +0 -166
  42. package/dist/runtime/styles/chrome.css +0 -30
  43. package/dist/runtime/styles/prose.css +0 -102
  44. package/dist/runtime/styles/theme.css +0 -2
  45. package/dist/runtime/styles/utilities.css +0 -38
@@ -1,38 +0,0 @@
1
- ---
2
- /**
3
- * Fenced code through Equality's CodeBlock with no client React. eq-copy drives
4
- * the copy button and eq-highlight starts the highlight pass CodeBlock's own
5
- * useEffect never runs here. The highlighter owns language aliasing.
6
- */
7
- import { CodeBlock } from '@eqtylab/equality';
8
-
9
- interface Props {
10
- code: string;
11
- lang?: string;
12
- title?: string;
13
- }
14
-
15
- const { code, lang = 'text', title } = Astro.props;
16
-
17
- const language = lang.toLowerCase();
18
- ---
19
-
20
- {/*
21
- `block` is load-bearing: the fence is a custom element, which is inline by default.
22
-
23
- The descendant rule undoes CodeBlock's max-h-64, which is wrong for a docs sample.
24
- `!` is load-bearing too: Equality ships unlayered CSS, and an unlayered declaration
25
- beats anything in `@layer utilities` whatever its specificity, so a plain utility
26
- loses to `.code-block` and the sample silently goes back to scrolling at 16rem.
27
- */}
28
- <eq-highlight
29
- class="mb-4 block [&_[class*=code-block]]:max-h-none!"
30
- data-eq-chrome
31
- data-eq-copy={code}
32
- >
33
- <CodeBlock code={code} language={language} title={title} />
34
- </eq-highlight>
35
- <script>
36
- import '@eqtylab/docs/scripts/eq-copy.ts';
37
- import '@eqtylab/docs/scripts/eq-highlight.ts';
38
- </script>
@@ -1,20 +0,0 @@
1
- ---
2
- /**
3
- * Renders a fence from the props `rehypeCodeFence` lifted onto the `<pre>`.
4
- * Do not re-parse the slot: its HTML is escaped in dev and raw in build.
5
- */
6
- import CodeFence from './CodeFence.astro';
7
-
8
- interface Props {
9
- 'data-code'?: string;
10
- 'data-language'?: string;
11
- 'data-title'?: string;
12
- }
13
-
14
- const props = Astro.props as Props;
15
- const code = props['data-code'] ?? '';
16
- const lang = props['data-language'] ?? 'text';
17
- const title = props['data-title'];
18
- ---
19
-
20
- <CodeFence code={code} lang={lang} title={title} />
@@ -1,21 +0,0 @@
1
- ---
2
- import { isExternalHref } from '@eqtylab/docs/paths';
3
-
4
- interface Props {
5
- href?: string;
6
- [key: string]: unknown;
7
- }
8
-
9
- const { href = '', ...rest } = Astro.props;
10
- const external = isExternalHref(href);
11
- ---
12
-
13
- <a
14
- href={href}
15
- class="text-inherit"
16
- target={external ? '_blank' : undefined}
17
- rel={external ? 'noopener noreferrer' : undefined}
18
- {...rest}
19
- >
20
- <slot />
21
- </a>
@@ -1,18 +0,0 @@
1
- ---
2
- /** Bridges a markdown table to Equality's `Table`; the column count comes from `rehypeTableColumns`. */
3
- import { TableContainer } from '@eqtylab/equality';
4
-
5
- interface Props {
6
- 'data-column-count'?: string;
7
- }
8
-
9
- const count = Number(Astro.props['data-column-count'] ?? 0);
10
-
11
- // minmax(0, auto): without a zero minimum, one long unbroken cell blows the grid past its container.
12
- const columns = count > 0 ? `repeat(${count}, minmax(0, auto))` : undefined;
13
- ---
14
-
15
- {/* Flow spacing only; every visual property comes from Equality's Table. */}
16
- <TableContainer columns={columns} border className="mb-4" data-eq-chrome>
17
- <slot />
18
- </TableContainer>
@@ -1,2 +0,0 @@
1
- /** Public components for MDX authors. `.astro` only: a React component here pulls React into every page. */
2
- export { default as Link } from './Link.astro';
@@ -1,50 +0,0 @@
1
- ---
2
- /** Header + sidebar | prose | TOC. The `doc` template. */
3
- import type { NavNode, TocNode } from '@eqtylab/docs/types';
4
- import CONFIG from 'virtual:eqty-docs/config';
5
- import DocsShell from './DocsShell.astro';
6
- import Header from '../chrome/Header.astro';
7
- import NavDrawer from '../chrome/NavDrawer.astro';
8
- import Sidebar from '../chrome/Sidebar.astro';
9
- import TableOfContents from '../chrome/TableOfContents.astro';
10
-
11
- interface Props {
12
- title: string;
13
- description?: string;
14
- nav: NavNode[];
15
- toc: TocNode[];
16
- showToc?: boolean;
17
- splash?: boolean;
18
- }
19
-
20
- const { title, description, nav, toc, showToc = true, splash = false } = Astro.props;
21
-
22
- // A section's own index page if it has one, else its first child. Never the sidebar repeated.
23
- const suggested = nav
24
- .map((node) => ({ label: node.label, href: node.href ?? node.children?.[0]?.href }))
25
- .filter((item): item is { label: string; href: string } => !!item.href)
26
- .slice(0, 4);
27
- ---
28
-
29
- <DocsShell title={title} description={description}>
30
- {/* A prop, not slot fallback: Astro registers a named slot at compile time, so a forwarded
31
- slot always counts as filled and the fallback never runs. */}
32
- <Header showSearch={!Astro.slots.has('search')} suggested={suggested}>
33
- <slot name="search" slot="search" />
34
- <slot name="versions" slot="versions" />
35
- </Header>
36
- <NavDrawer nodes={nav} />
37
- <div class="flex items-start">
38
- {!splash && <Sidebar nodes={nav} />}
39
- {/* 128px of runway, so a page stops deliberately rather than at its last line. */}
40
- <main
41
- class="mx-auto flex w-full min-w-0 max-w-[calc(var(--eq-docs-content-max)+var(--eq-docs-toc-width)+8rem)] items-start gap-12 px-6 pb-32 pt-10 data-[splash]:max-w-none xl:gap-16"
42
- data-splash={splash ? '' : undefined}
43
- >
44
- <div class="min-w-0 flex-1 [&>.eq-prose]:max-w-[var(--eq-docs-content-max)]">
45
- <slot />
46
- </div>
47
- {!splash && showToc && toc.length > 0 && <TableOfContents items={toc} />}
48
- </main>
49
- </div>
50
- </DocsShell>
@@ -1,92 +0,0 @@
1
- ---
2
- /** The <html> document: head, theme bootstrap, and a slot for the page body. */
3
- import CONFIG from 'virtual:eqty-docs/config';
4
- import { withBase } from '@eqtylab/docs/paths';
5
-
6
- import '../styles/docs.css';
7
-
8
- interface Props {
9
- title?: string;
10
- description?: string;
11
- /** Emit robots noindex. Set for every pinned version build. */
12
- noIndex?: boolean;
13
- /** Canonical URL, when it differs from the current page. */
14
- canonical?: string;
15
- }
16
-
17
- const { title, description, noIndex, canonical } = Astro.props;
18
-
19
- const paths = {
20
- base: import.meta.env.BASE_URL,
21
- pathPrefix: CONFIG.pathPrefix,
22
- };
23
-
24
- const pageTitle = title && title !== CONFIG.title ? `${title} · ${CONFIG.title}` : CONFIG.title;
25
- const pageDescription = description ?? CONFIG.description;
26
-
27
- // Pinned version builds are noindex: latest today is not latest tomorrow.
28
- const robots = noIndex ?? !CONFIG.env.isLatest;
29
-
30
- /**
31
- * Blocking inline theme bootstrap, so the attribute is set before first paint.
32
- * Do not also call Equality's `initializeTheme`: it knows only light and dark
33
- * and its stored value would override this. Never write storage here, or
34
- * 'system' becomes a resolved value permanently. Constants must match
35
- * `runtime/lib/theme.ts`.
36
- */
37
- const themeBootstrap = `
38
- (() => {
39
- const KEY = 'eqty-docs-theme';
40
- const EVENT = 'eqty-docs-theme-change';
41
- const PERSIST = ${JSON.stringify(CONFIG.theme.persist)};
42
- const query = window.matchMedia('(prefers-color-scheme: dark)');
43
-
44
- const read = () => {
45
- if (!PERSIST) return window.__eqtyDocsTheme ?? null;
46
- try {
47
- return window.localStorage.getItem(KEY);
48
- } catch {
49
- return null;
50
- }
51
- };
52
-
53
- const apply = () => {
54
- const stored = read();
55
- const preference =
56
- stored === 'dark' || stored === 'light' || stored === 'system' ? stored : 'system';
57
- const resolved = preference === 'system' ? (query.matches ? 'dark' : 'light') : preference;
58
- const root = document.documentElement;
59
- root.setAttribute('data-equality-theme', resolved);
60
- root.setAttribute('data-eq-theme-pref', preference);
61
- // Native UI follows this, not the attribute above.
62
- root.style.colorScheme = resolved;
63
- };
64
-
65
- apply();
66
- query.addEventListener('change', apply);
67
- window.addEventListener(EVENT, apply);
68
- // The React subscription never touches the attribute, so other tabs repaint here.
69
- window.addEventListener('storage', (event) => {
70
- if (event.key === KEY) apply();
71
- });
72
- })();
73
- `;
74
- ---
75
-
76
- <!doctype html>
77
- <html lang="en">
78
- <head>
79
- <meta charset="utf-8" />
80
- <meta name="viewport" content="width=device-width, initial-scale=1" />
81
- <title>{pageTitle}</title>
82
- {pageDescription && <meta name="description" content={pageDescription} />}
83
- {robots && <meta name="robots" content="noindex, follow" />}
84
- {canonical && <link rel="canonical" href={canonical} />}
85
- <link rel="icon" href={withBase(CONFIG.favicon, paths)} />
86
- <script is:inline set:html={themeBootstrap} />
87
- <slot name="head" />
88
- </head>
89
- <body>
90
- <slot />
91
- </body>
92
- </html>
@@ -1,43 +0,0 @@
1
- /**
2
- * Route-level MDX component map. Entries here OVERRIDE a page's own
3
- * `export const components`, so keep it small.
4
- */
5
- import {
6
- TableBody,
7
- TableCaption,
8
- TableCell,
9
- TableFooter,
10
- TableHead,
11
- TableHeader,
12
- TableRow,
13
- } from '@eqtylab/equality';
14
- import CONFIG from 'virtual:eqty-docs/config';
15
-
16
- import AlertBridge from '../components/AlertBridge.astro';
17
- import CodeFenceBridge from '../components/CodeFenceBridge.astro';
18
- import Link from '../components/Link.astro';
19
- import TableBridge from '../components/TableBridge.astro';
20
-
21
- const base: Record<string, unknown> = {
22
- a: Link,
23
-
24
- // An explicit import of Alert from @eqtylab/equality still wins.
25
- Alert: AlertBridge,
26
-
27
- // All hook-free, so they render statically with no client directive.
28
- table: TableBridge,
29
- thead: TableHeader,
30
- tbody: TableBody,
31
- tfoot: TableFooter,
32
- tr: TableRow,
33
- th: TableHead,
34
- td: TableCell,
35
- caption: TableCaption,
36
- };
37
-
38
- // With 'shiki', Astro has already rendered the block.
39
- if (CONFIG.code?.highlighter === 'codeblock') {
40
- base.pre = CodeFenceBridge;
41
- }
42
-
43
- export const mdxComponents = base;
@@ -1,74 +0,0 @@
1
- /** Bridges the content collections into the pure nav builder. Needs `astro:content`, so it lives in the runtime tree. */
2
- import { breadcrumbsFor, buildNavTree, buildTocTree, prevNextFor } from '@eqtylab/docs/nav';
3
- import type { GroupConfig } from '@eqtylab/docs/nav';
4
- import type { DocsNavEntry, NavNode } from '@eqtylab/docs/types';
5
- import { getCollection } from 'astro:content';
6
- import CONFIG from 'virtual:eqty-docs/config';
7
-
8
- export { buildTocTree, breadcrumbsFor, prevNextFor };
9
-
10
- /** No `versionPrefix`: the version already lives in `base`, and adding it here doubles the segment. */
11
- export function pathContext() {
12
- return {
13
- base: import.meta.env.BASE_URL,
14
- pathPrefix: CONFIG.pathPrefix,
15
- };
16
- }
17
-
18
- /** All docs entries, with drafts filtered out unless we're in `astro dev`. */
19
- export async function docsEntries() {
20
- return getCollection('docs', ({ data }: { data: { draft?: boolean } }) =>
21
- import.meta.env.DEV ? true : !data.draft
22
- );
23
- }
24
-
25
- async function groupMap(): Promise<Map<string, GroupConfig>> {
26
- const map = new Map<string, GroupConfig>();
27
- try {
28
- const groups = await getCollection('docsGroups');
29
- for (const group of groups) {
30
- map.set(group.id, group.data as GroupConfig);
31
- }
32
- } catch {
33
- // docsGroups is optional; no _group.yaml means alphabetical ordering.
34
- }
35
- return map;
36
- }
37
-
38
- export async function docsNav(currentPath: string): Promise<NavNode[]> {
39
- const [entries, groups] = await Promise.all([docsEntries(), groupMap()]);
40
-
41
- const navEntries: DocsNavEntry[] = entries.map((entry) => ({
42
- id: entry.id,
43
- filePath: entry.filePath,
44
- label: entry.data.navLabel ?? entry.data.title,
45
- icon: entry.data.icon,
46
- badge: entry.data.badge ?? deprecationBadge(entry.data.deprecated),
47
- hidden: entry.data.hidden,
48
- draft: entry.data.draft,
49
- }));
50
-
51
- return buildNavTree({
52
- entries: navEntries,
53
- groups,
54
- currentPath,
55
- paths: pathContext(),
56
- defaultCollapsed: CONFIG.sidebar.collapsed,
57
- defaultSort: CONFIG.sidebar.sort,
58
- extra: (CONFIG.sidebar.extra ?? []) as NavNode[],
59
- onWarn: warnOnce,
60
- });
61
- }
62
-
63
- // The nav is rebuilt per page; report each config problem once per build.
64
- const warned = new Set<string>();
65
- function warnOnce(message: string) {
66
- if (warned.has(message)) return;
67
- warned.add(message);
68
- console.warn(`[@eqtylab/docs] ${message}`);
69
- }
70
-
71
- function deprecationBadge(deprecated: unknown) {
72
- if (!deprecated) return undefined;
73
- return { text: 'Deprecated', variant: 'warning' as const };
74
- }
@@ -1,57 +0,0 @@
1
- /** Fallback page summary for pages with no `description` frontmatter. Build time only. */
2
-
3
- const MAX_LENGTH = 140;
4
-
5
- function isSkippable(line: string): boolean {
6
- return (
7
- line === '' ||
8
- line.startsWith('#') ||
9
- line.startsWith('import ') ||
10
- line.startsWith('export ') ||
11
- line.startsWith('<') ||
12
- line.startsWith('|') ||
13
- line.startsWith('>') ||
14
- line.startsWith('- ') ||
15
- line.startsWith('* ') ||
16
- /^\d+\.\s/.test(line)
17
- );
18
- }
19
-
20
- /** Enough markdown to make one sentence readable; anything richer is not a summary anyway. */
21
- function stripMarkdown(text: string): string {
22
- return text
23
- .replace(/!\[[^\]]*\]\([^)]*\)/g, '')
24
- .replace(/\[([^\]]*)\]\([^)]*\)/g, '$1')
25
- .replace(/[`*_]/g, '')
26
- .replace(/<[^>]+>/g, '')
27
- .replace(/\s+/g, ' ')
28
- .trim();
29
- }
30
-
31
- function truncate(text: string): string {
32
- if (text.length <= MAX_LENGTH) return text;
33
- const cut = text.slice(0, MAX_LENGTH);
34
- const lastSpace = cut.lastIndexOf(' ');
35
- return `${cut.slice(0, lastSpace > 0 ? lastSpace : MAX_LENGTH).trimEnd()}…`;
36
- }
37
-
38
- /** First paragraph of prose. Frontmatter is already stripped by the content loader. */
39
- export function firstParagraph(body: string | undefined): string | undefined {
40
- if (!body) return undefined;
41
-
42
- let inFence = false;
43
- for (const raw of body.split('\n')) {
44
- const line = raw.trim();
45
-
46
- if (line.startsWith('```')) {
47
- inFence = !inFence;
48
- continue;
49
- }
50
- if (inFence || isSkippable(line)) continue;
51
-
52
- const text = stripMarkdown(line);
53
- // A line that was nothing but a link or a tag strips to nothing; keep looking.
54
- if (text) return truncate(text);
55
- }
56
- return undefined;
57
- }
@@ -1,84 +0,0 @@
1
- import CONFIG from 'virtual:eqty-docs/config';
2
-
3
- /** These four are repeated in DocsShell.astro's inline script. Change both. */
4
- export const STORAGE_KEY = 'eqty-docs-theme';
5
- export const UPDATE_EVENT = 'eqty-docs-theme-change';
6
- /** Equality's palette keys on this. */
7
- export const THEME_ATTRIBUTE = 'data-equality-theme';
8
- /** ThemeToggle.module.css reads this, so the trigger is correct before React loads. */
9
- export const PREFERENCE_ATTRIBUTE = 'data-eq-theme-pref';
10
-
11
- export type ThemePreference = 'light' | 'dark' | 'system';
12
-
13
- export type ResolvedTheme = 'light' | 'dark';
14
-
15
- const PREFERENCES: readonly ThemePreference[] = ['dark', 'light', 'system'];
16
-
17
- function isPreference(value: unknown): value is ThemePreference {
18
- return typeof value === 'string' && (PREFERENCES as readonly string[]).includes(value);
19
- }
20
-
21
- function readStored(): string | null {
22
- if (!CONFIG.theme.persist) return window.__eqtyDocsTheme ?? null;
23
- try {
24
- return window.localStorage.getItem(STORAGE_KEY);
25
- } catch {
26
- return null;
27
- }
28
- }
29
-
30
- function writeStored(preference: ThemePreference): void {
31
- if (!CONFIG.theme.persist) {
32
- window.__eqtyDocsTheme = preference;
33
- return;
34
- }
35
- try {
36
- window.localStorage.setItem(STORAGE_KEY, preference);
37
- } catch {
38
- // Ignore a storage failure; the theme still applies for this page.
39
- }
40
- }
41
-
42
- export function getSystemTheme(): ResolvedTheme {
43
- return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
44
- }
45
-
46
- export function getThemePreference(): ThemePreference {
47
- if (typeof window === 'undefined') return 'system';
48
- const stored = readStored();
49
- return isPreference(stored) ? stored : 'system';
50
- }
51
-
52
- export function resolveTheme(preference: ThemePreference): ResolvedTheme {
53
- return preference === 'system' ? getSystemTheme() : preference;
54
- }
55
-
56
- export function applyTheme(preference: ThemePreference): void {
57
- const resolved = resolveTheme(preference);
58
- const root = document.documentElement;
59
- root.setAttribute(THEME_ATTRIBUTE, resolved);
60
- root.setAttribute(PREFERENCE_ATTRIBUTE, preference);
61
- // Without this, scrollbars and form controls stay light on a dark page.
62
- root.style.colorScheme = resolved;
63
- }
64
-
65
- export function setThemePreference(preference: ThemePreference): void {
66
- writeStored(preference);
67
- applyTheme(preference);
68
- window.dispatchEvent(new CustomEvent(UPDATE_EVENT, { detail: preference }));
69
- }
70
-
71
- export function subscribeToThemePreference(listener: () => void): () => void {
72
- if (typeof window === 'undefined') return () => {};
73
-
74
- const onStorage = (event: StorageEvent) => {
75
- if (event.key === STORAGE_KEY) listener();
76
- };
77
-
78
- window.addEventListener(UPDATE_EVENT, listener);
79
- window.addEventListener('storage', onStorage);
80
- return () => {
81
- window.removeEventListener(UPDATE_EVENT, listener);
82
- window.removeEventListener('storage', onStorage);
83
- };
84
- }
@@ -1,33 +0,0 @@
1
- /** Markdown twin of every page, for LLM and agent consumption. */
2
- import { idToPath } from '@eqtylab/docs/paths';
3
- import type { APIRoute } from 'astro';
4
- import { getCollection } from 'astro:content';
5
- import CONFIG from 'virtual:eqty-docs/config';
6
-
7
- export async function getStaticPaths() {
8
- const entries = await getCollection(
9
- 'docs',
10
- ({ data }: { data: { draft?: boolean; noIndex?: boolean } }) => !data.draft && !data.noIndex
11
- );
12
- return entries.map((entry) => ({
13
- // An empty slug would emit a file literally called ".md".
14
- params: { slug: idToPath(entry.id) || 'index' },
15
- props: { entry },
16
- }));
17
- }
18
-
19
- export const GET: APIRoute = ({ props }) => {
20
- const { entry } = props as {
21
- entry: { body?: string; data: { title: string; description?: string } };
22
- };
23
-
24
- const frontmatter = ['---', `title: ${JSON.stringify(entry.data.title)}`];
25
- if (entry.data.description) {
26
- frontmatter.push(`description: ${JSON.stringify(entry.data.description)}`);
27
- }
28
- frontmatter.push(`source: ${JSON.stringify(CONFIG.title)}`, '---', '');
29
-
30
- return new Response(frontmatter.join('\n') + (entry.body ?? ''), {
31
- headers: { 'Content-Type': 'text/markdown; charset=utf-8' },
32
- });
33
- };
@@ -1,92 +0,0 @@
1
- ---
2
- import { getCollection, render } from 'astro:content';
3
- import CONFIG from 'virtual:eqty-docs/config';
4
- import { docsHref, idToPath, joinPath, normalizePath } from '@eqtylab/docs/paths';
5
- import DocsPage from '../layouts/DocsPage.astro';
6
- import { breadcrumbsFor, buildTocTree, docsNav, pathContext, prevNextFor } from '../lib/nav-data.ts';
7
- import { firstParagraph } from '../lib/summary.ts';
8
- import { mdxComponents } from '../lib/mdx-components.ts';
9
- import PageFooter from '../chrome/PageFooter.astro';
10
- import Prose from '../chrome/Prose.astro';
11
-
12
- export async function getStaticPaths() {
13
- const entries = await getCollection('docs', ({ data }: { data: { draft?: boolean } }) =>
14
- import.meta.env.DEV ? true : !data.draft
15
- );
16
-
17
- const owned = new Set((CONFIG.ownedByConsumer ?? []).map(normalizePath));
18
-
19
- return entries
20
- // Astro does not de-duplicate injected routes against consumer pages.
21
- .filter((entry) => !owned.has(normalizePath(docsHref(entry.id, pathContext()))))
22
- .map((entry) => ({
23
- // `undefined` (not '') is how a rest param matches the empty path.
24
- params: { slug: idToPath(entry.id) || undefined },
25
- props: { entry },
26
- }));
27
- }
28
-
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;
69
- ---
70
-
71
- <DocsPage
72
- title={entry.data.title}
73
- description={entry.data.description}
74
- nav={nav}
75
- toc={toc}
76
- showToc={showToc}
77
- splash={splash}
78
- >
79
- <Prose
80
- title={entry.data.title}
81
- description={entry.data.description}
82
- deprecated={entry.data.deprecated}
83
- markdownHref={markdownHref}
84
- source={entry.body}
85
- crumbs={crumbs}
86
- trail={renderedTrail}
87
- fallbackSummary={fallbackSummary}
88
- >
89
- <Content components={mdxComponents} />
90
- </Prose>
91
- {!splash && <PageFooter prev={prev} next={next} editHref={editHref} text={CONFIG.footer.text} />}
92
- </DocsPage>
@@ -1,38 +0,0 @@
1
- /** An index of every page, in the llms.txt convention. */
2
- import { docsHref } from '@eqtylab/docs/paths';
3
- import type { APIRoute } from 'astro';
4
- import { getCollection } from 'astro:content';
5
- import CONFIG from 'virtual:eqty-docs/config';
6
-
7
- export const GET: APIRoute = async (ctx) => {
8
- const entries = await getCollection(
9
- 'docs',
10
- ({ data }: { data: { draft?: boolean; noIndex?: boolean } }) => !data.draft && !data.noIndex
11
- );
12
-
13
- const paths = {
14
- base: import.meta.env.BASE_URL,
15
- pathPrefix: CONFIG.pathPrefix,
16
- };
17
- const origin = (ctx.site ?? new URL(ctx.request.url)).origin;
18
-
19
- const sorted = [...entries].sort((a, b) => a.data.title.localeCompare(b.data.title));
20
-
21
- const lines = [
22
- `# ${CONFIG.title}`,
23
- '',
24
- ...(CONFIG.description ? [`> ${CONFIG.description}`, ''] : []),
25
- '## Pages',
26
- '',
27
- ...sorted.map((entry) => {
28
- const href = docsHref(entry.id, paths).replace(/\/$/, '');
29
- const suffix = entry.data.description ? `: ${entry.data.description}` : '';
30
- return `- [${entry.data.title}](${origin}${href}.md)${suffix}`;
31
- }),
32
- '',
33
- ];
34
-
35
- return new Response(lines.join('\n'), {
36
- headers: { 'Content-Type': 'text/plain; charset=utf-8' },
37
- });
38
- };