@jaamd/jaad 0.0.0-stage → 1.0.0-rc.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.
- package/LICENSE +21 -0
- package/README.md +58 -2
- package/dist/jaad.css +1331 -0
- package/docs/00-overview.md +25 -0
- package/docs/01-getting-started/01-installation.md +216 -0
- package/docs/01-getting-started/02-pages.md +109 -0
- package/docs/01-getting-started/03-deployment.md +100 -0
- package/docs/02-configurations/01-reference.md +215 -0
- package/docs/02-configurations/02-frontmatter.md +105 -0
- package/docs/02-configurations/03-i18n.md +210 -0
- package/docs/02-configurations/04-search.md +58 -0
- package/docs/02-configurations/05-themes.md +104 -0
- package/docs/02-configurations/06-styles.md +156 -0
- package/docs/02-configurations/07-custom-pages.md +119 -0
- package/docs/02-configurations/08-advanced-layout.md +139 -0
- package/docs/03-upgrading.md +208 -0
- package/index.ts +31 -0
- package/package.json +71 -3
- package/site.ts +36 -0
- package/src/config.ts +85 -0
- package/src/docs/advanced.ts +23 -0
- package/src/docs/collection.ts +109 -0
- package/src/docs/components/Breadcrumbs.astro +29 -0
- package/src/docs/components/DocsMobileNav.astro +90 -0
- package/src/docs/components/DocsNavTree.astro +57 -0
- package/src/docs/components/DocsPage.astro +66 -0
- package/src/docs/components/DocsPageNav.astro +53 -0
- package/src/docs/components/DocsSidebarLeft.astro +53 -0
- package/src/docs/components/PageActions.astro +83 -0
- package/src/docs/components/StructuredData.astro +55 -0
- package/src/docs/components/TableOfContents.astro +71 -0
- package/src/docs/config.ts +167 -0
- package/src/docs/content.ts +36 -0
- package/src/docs/endpoints.ts +89 -0
- package/src/docs/integration.ts +249 -0
- package/src/docs/layouts/DefaultDocsFrame.astro +33 -0
- package/src/docs/layouts/Docs.astro +169 -0
- package/src/docs/layouts/Page.astro +18 -0
- package/src/docs/link-plugins.ts +52 -0
- package/src/docs/links.ts +42 -0
- package/src/docs/locale-tree.ts +51 -0
- package/src/docs/model/ids.ts +87 -0
- package/src/docs/model/rendered.ts +185 -0
- package/src/docs/model/tree.ts +124 -0
- package/src/docs/model/validate.ts +75 -0
- package/src/docs/navigation.ts +75 -0
- package/src/docs/routes/404.astro +16 -0
- package/src/docs/routes/docs-index.astro +19 -0
- package/src/docs/routes/docs-slug.astro +54 -0
- package/src/docs/routes/docs-slug.md.ts +35 -0
- package/src/docs/routes/llms-locale.txt.ts +8 -0
- package/src/docs/routes/llms.txt.ts +4 -0
- package/src/docs/routes/search-index-locale.json.ts +8 -0
- package/src/docs/routes/search-index.json.ts +4 -0
- package/src/docs/runtime.ts +17 -0
- package/src/docs/search-index.ts +39 -0
- package/src/docs/styles/components/breadcrumbs.css +26 -0
- package/src/docs/styles/components/docs-mobile-nav.css +47 -0
- package/src/docs/styles/components/docs-page-nav.css +41 -0
- package/src/docs/styles/components/docs-page.css +9 -0
- package/src/docs/styles/components/docs-sidebar.css +59 -0
- package/src/docs/styles/components/page-actions.css +51 -0
- package/src/docs/styles/components/toc.css +38 -0
- package/src/docs/styles/components.css +7 -0
- package/src/docs/styles/docs.css +81 -0
- package/src/docs/styles/jaamd.css +11 -0
- package/src/docs/types.ts +39 -0
- package/src/docs/urls.ts +35 -0
- package/src/env.d.ts +13 -0
- package/src/integration.ts +36 -0
- package/src/site/components/Footer.astro +18 -0
- package/src/site/components/Head.astro +119 -0
- package/src/site/components/Header.astro +50 -0
- package/src/site/components/LocaleSwitcher.astro +111 -0
- package/src/site/components/Nav.astro +22 -0
- package/src/site/components/NavMobile.astro +105 -0
- package/src/site/components/SearchPalette.astro +409 -0
- package/src/site/components/SocialLinks.astro +27 -0
- package/src/site/components/ThemeSwitcher.astro +55 -0
- package/src/site/components/locale-switcher.ts +6 -0
- package/src/site/config.ts +149 -0
- package/src/site/fonts/OFL.txt +94 -0
- package/src/site/fonts/inter.woff2 +0 -0
- package/src/site/fonts/merriweather-italic.woff2 +0 -0
- package/src/site/fonts/merriweather.woff2 +0 -0
- package/src/site/fonts.ts +29 -0
- package/src/site/forges.ts +60 -0
- package/src/site/helpers.ts +4 -0
- package/src/site/infer.ts +75 -0
- package/src/site/integration.ts +59 -0
- package/src/site/layouts/Shell.astro +41 -0
- package/src/site/locales.ts +199 -0
- package/src/site/runtime.ts +19 -0
- package/src/site/search.ts +227 -0
- package/src/site/social.ts +44 -0
- package/src/site/styles/components/footer.css +8 -0
- package/src/site/styles/components/header.css +31 -0
- package/src/site/styles/components/locale-switcher.css +20 -0
- package/src/site/styles/components/nav-mobile.css +15 -0
- package/src/site/styles/components/nav.css +11 -0
- package/src/site/styles/components/not-found.css +11 -0
- package/src/site/styles/components/search.css +118 -0
- package/src/site/styles/components/shell.css +77 -0
- package/src/site/styles/components/social-links.css +8 -0
- package/src/site/styles/components/theme-switcher.css +14 -0
- package/src/site/styles/components.css +10 -0
- package/src/site/styles/global.css +140 -0
- package/src/site/styles/jaamd-forward.css +16 -0
- package/src/site/styles/jaamd-reverse.css +18 -0
- package/src/site/styles/tailwind.css +30 -0
- package/src/site/themes.ts +26 -0
- package/src/site/ui/en.ts +27 -0
- package/src/site/ui/it.ts +28 -0
- package/src/site/ui.ts +49 -0
- package/src/site/urls.ts +14 -0
- package/src/site/virtual.ts +42 -0
- package/src/styles.css +5 -0
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import type { DocsPageData } from '../types.ts'
|
|
2
|
+
import type { DocsUrlOptions } from '../urls.ts'
|
|
3
|
+
import type { RenderedMarkdown } from './rendered.ts'
|
|
4
|
+
import { docsPageHref } from '../urls.ts'
|
|
5
|
+
import { namedSlug, parseDocPath, slugToTitle } from './ids.ts'
|
|
6
|
+
import { renderedTitle } from './rendered.ts'
|
|
7
|
+
|
|
8
|
+
/** A markdown file of one locale: a collection entry, or a file read from disk. */
|
|
9
|
+
export interface DocsSource {
|
|
10
|
+
/** Collection id. */
|
|
11
|
+
id: string
|
|
12
|
+
/** The id without its locale and its section. */
|
|
13
|
+
path: string
|
|
14
|
+
/** Set when the docs are localised. */
|
|
15
|
+
locale?: string
|
|
16
|
+
/** Set when the docs are split into sections. */
|
|
17
|
+
section?: string
|
|
18
|
+
data: Partial<DocsPageData>
|
|
19
|
+
rendered?: RenderedMarkdown
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** A page with everything its file, its frontmatter and its place in the docs decide. */
|
|
23
|
+
export interface DocsPage<S extends DocsSource = DocsSource> {
|
|
24
|
+
/** What the page was built from. */
|
|
25
|
+
source: S
|
|
26
|
+
id: string
|
|
27
|
+
path: string
|
|
28
|
+
locale?: string
|
|
29
|
+
section?: string
|
|
30
|
+
/** The chapter directory, without its order prefix. */
|
|
31
|
+
chapter?: string
|
|
32
|
+
data: S['data']
|
|
33
|
+
/** The named url below the docs mount: its section, then its clean path. */
|
|
34
|
+
slug: string
|
|
35
|
+
/** Where it is served: empty for the opening page, the section for the first
|
|
36
|
+
* page of a later one, `slug` otherwise. */
|
|
37
|
+
canonical: string
|
|
38
|
+
/** The other slugs it answers to, each redirecting to `canonical`. */
|
|
39
|
+
aliases: string[]
|
|
40
|
+
/** The url of `canonical`. */
|
|
41
|
+
href: string
|
|
42
|
+
/** Frontmatter title, then the first h1, then the file name. */
|
|
43
|
+
title: string
|
|
44
|
+
/** The name in navigation: frontmatter `label`, then `title`. */
|
|
45
|
+
label: string
|
|
46
|
+
/** What pairs it with its translations: frontmatter `translationKey`, then `slug`. */
|
|
47
|
+
translationKey: string
|
|
48
|
+
/** Position in reading order. */
|
|
49
|
+
index: number
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** The pages of one locale in reading order. */
|
|
53
|
+
export interface DocsTree<S extends DocsSource = DocsSource> {
|
|
54
|
+
pages: DocsPage<S>[]
|
|
55
|
+
byId: Map<string, DocsPage<S>>
|
|
56
|
+
byKey: Map<string, DocsPage<S>>
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface DocsTreeOptions {
|
|
60
|
+
/** Section keys, in their order. */
|
|
61
|
+
sections?: string[]
|
|
62
|
+
/** For `href`. */
|
|
63
|
+
urls: DocsUrlOptions
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** The order of `sections` first, then chapter order, then file order within
|
|
67
|
+
* the chapter. Frontmatter `order` replaces a file's prefix number; root pages
|
|
68
|
+
* interleave with chapters by their own number. */
|
|
69
|
+
function sortSources<S extends DocsSource>(sources: S[], sections: string[]): S[] {
|
|
70
|
+
return sources
|
|
71
|
+
.map((source) => {
|
|
72
|
+
const parsed = parseDocPath(source.path)
|
|
73
|
+
const order = source.data.order ?? parsed.order
|
|
74
|
+
return {
|
|
75
|
+
source,
|
|
76
|
+
section: sections.indexOf(source.section ?? ''),
|
|
77
|
+
primary: parsed.orderChapter ?? order,
|
|
78
|
+
secondary: order,
|
|
79
|
+
}
|
|
80
|
+
})
|
|
81
|
+
.toSorted((a, b) => a.section - b.section || a.primary - b.primary || a.secondary - b.secondary)
|
|
82
|
+
.map((entry) => entry.source)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Resolves every page of one locale once. */
|
|
86
|
+
export function buildDocsTree<S extends DocsSource>(sources: S[], options: DocsTreeOptions): DocsTree<S> {
|
|
87
|
+
const sorted = sortSources(sources, options.sections ?? [])
|
|
88
|
+
|
|
89
|
+
const pages = sorted.map((source, index): DocsPage<S> => {
|
|
90
|
+
const parsed = parseDocPath(source.path)
|
|
91
|
+
const slug = namedSlug(source.path, source.section)
|
|
92
|
+
const canonical =
|
|
93
|
+
index === 0 ? '' : source.section && sorted[index - 1].section !== source.section ? source.section : slug
|
|
94
|
+
const title = source.data.title ?? renderedTitle(source.rendered) ?? slugToTitle(parsed.slug)
|
|
95
|
+
|
|
96
|
+
return {
|
|
97
|
+
source,
|
|
98
|
+
id: source.id,
|
|
99
|
+
path: source.path,
|
|
100
|
+
locale: source.locale,
|
|
101
|
+
section: source.section,
|
|
102
|
+
chapter: parsed.chapter,
|
|
103
|
+
data: source.data,
|
|
104
|
+
slug,
|
|
105
|
+
canonical,
|
|
106
|
+
aliases: [
|
|
107
|
+
...(slug === canonical ? [] : [slug]),
|
|
108
|
+
// The first section's key leads to the opening page.
|
|
109
|
+
...(index === 0 && source.section ? [source.section] : []),
|
|
110
|
+
],
|
|
111
|
+
href: docsPageHref(canonical, options.urls),
|
|
112
|
+
title,
|
|
113
|
+
label: source.data.label ?? title,
|
|
114
|
+
translationKey: source.data.translationKey ?? slug,
|
|
115
|
+
index,
|
|
116
|
+
}
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
return {
|
|
120
|
+
pages,
|
|
121
|
+
byId: new Map(pages.map((page) => [page.id, page])),
|
|
122
|
+
byKey: new Map(pages.map((page) => [page.translationKey, page])),
|
|
123
|
+
}
|
|
124
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import type { DocsSource } from './tree.ts'
|
|
2
|
+
import { getCleanSlug, namedSlug } from './ids.ts'
|
|
3
|
+
|
|
4
|
+
/** Within a section, like `id`; across them, the section is part of the name. */
|
|
5
|
+
const inSection = (page: DocsSource, id: string) => (page.section ? `${page.section}/${id}` : id)
|
|
6
|
+
|
|
7
|
+
/** Fail before route generation when the file tree cannot produce one clear
|
|
8
|
+
* navigation and URL for every page. */
|
|
9
|
+
export function validateDocsStructure(pages: DocsSource[], locale?: string): void {
|
|
10
|
+
const idsBySlug = new Map<string, string[]>()
|
|
11
|
+
const directoriesByChapter = new Map<string, Set<string>>()
|
|
12
|
+
const issues: string[] = []
|
|
13
|
+
// Messages name the file on disk, locale directory included.
|
|
14
|
+
const named = (id: string) => (locale ? `${locale}/${id}` : id)
|
|
15
|
+
|
|
16
|
+
const byId = pages.map((page) => ({ page, id: inSection(page, page.path) }))
|
|
17
|
+
for (const { page, id } of byId.toSorted((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0))) {
|
|
18
|
+
const parts = page.path.split('/')
|
|
19
|
+
if (parts.length > 2) {
|
|
20
|
+
issues.push(`${named(id)}: only one chapter directory is supported inside docs/`)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const slug = namedSlug(page.path, page.section)
|
|
24
|
+
const ids = idsBySlug.get(slug) ?? []
|
|
25
|
+
ids.push(id)
|
|
26
|
+
idsBySlug.set(slug, ids)
|
|
27
|
+
|
|
28
|
+
if (parts.length === 2) {
|
|
29
|
+
const directory = inSection(page, parts[0])
|
|
30
|
+
const chapter = inSection(page, getCleanSlug(parts[0]))
|
|
31
|
+
const directories = directoriesByChapter.get(chapter) ?? new Set()
|
|
32
|
+
directories.add(directory)
|
|
33
|
+
directoriesByChapter.set(chapter, directories)
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
for (const [slug, ids] of idsBySlug) {
|
|
38
|
+
if (ids.length > 1) {
|
|
39
|
+
issues.push(`${ids.map(named).join(', ')} all resolve to /${slug}`)
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// A page outside the sections cannot take a section's url.
|
|
44
|
+
const keys = new Set(pages.flatMap((page) => page.section ?? []))
|
|
45
|
+
for (const page of pages) {
|
|
46
|
+
const first = namedSlug(page.path, page.section).split('/')[0]
|
|
47
|
+
if (!page.section && keys.has(first)) {
|
|
48
|
+
issues.push(`${named(page.path)} resolves into section /${first}`)
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const byKey = new Map<string, string[]>()
|
|
53
|
+
for (const page of pages) {
|
|
54
|
+
const key = page.data.translationKey
|
|
55
|
+
if (!key) continue
|
|
56
|
+
const ids = byKey.get(key) ?? []
|
|
57
|
+
ids.push(inSection(page, page.path))
|
|
58
|
+
byKey.set(key, ids)
|
|
59
|
+
}
|
|
60
|
+
for (const [key, ids] of byKey) {
|
|
61
|
+
if (ids.length > 1) {
|
|
62
|
+
issues.push(`${ids.map(named).join(', ')} all claim translationKey ${key}`)
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
for (const [chapter, directories] of directoriesByChapter) {
|
|
67
|
+
if (directories.size > 1) {
|
|
68
|
+
issues.push(`${[...directories].map(named).join(', ')} all resolve to chapter /${chapter}`)
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (issues.length > 0) {
|
|
73
|
+
throw new Error(`jaad: invalid documentation structure\n${issues.map((issue) => ` ${issue}`).join('\n')}`)
|
|
74
|
+
}
|
|
75
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import type { DocsTree } from './model/tree.ts'
|
|
2
|
+
import type { DocsNavItem } from './types.ts'
|
|
3
|
+
|
|
4
|
+
/** A page, or a chapter and its pages. */
|
|
5
|
+
export type DocsNavEntry =
|
|
6
|
+
| { type: 'page'; item: DocsNavItem }
|
|
7
|
+
| { type: 'chapter'; chapter: string; items: DocsNavItem[] }
|
|
8
|
+
|
|
9
|
+
/** A top-level entry. With sections, the pages outside them come first, then
|
|
10
|
+
* every section and its entries. */
|
|
11
|
+
export type DocsNavGroup = DocsNavEntry | { type: 'section'; section: string; label: string; entries: DocsNavEntry[] }
|
|
12
|
+
|
|
13
|
+
export interface DocsNavigation {
|
|
14
|
+
/** The whole tree, in reading order. */
|
|
15
|
+
groups: DocsNavGroup[]
|
|
16
|
+
previousPage: DocsNavItem | null
|
|
17
|
+
nextPage: DocsNavItem | null
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Each chapter sits where its first page does. */
|
|
21
|
+
function groupByChapter(items: DocsNavItem[]): DocsNavEntry[] {
|
|
22
|
+
const chapters = new Map<string, DocsNavItem[]>()
|
|
23
|
+
const groups: DocsNavEntry[] = []
|
|
24
|
+
|
|
25
|
+
for (const item of items) {
|
|
26
|
+
if (!item.chapter) {
|
|
27
|
+
groups.push({ type: 'page', item })
|
|
28
|
+
continue
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
let pages = chapters.get(item.chapter)
|
|
32
|
+
if (!pages) {
|
|
33
|
+
pages = []
|
|
34
|
+
chapters.set(item.chapter, pages)
|
|
35
|
+
groups.push({ type: 'chapter', chapter: item.chapter, items: pages })
|
|
36
|
+
}
|
|
37
|
+
pages.push(item)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return groups
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Each section with pages, under its label, in the order of `sections`. */
|
|
44
|
+
function groupBySection(items: DocsNavItem[], sections: { key: string; label: string }[]): DocsNavGroup[] {
|
|
45
|
+
return sections.flatMap(({ key, label }) => {
|
|
46
|
+
const own = items.filter((item) => item.section === key)
|
|
47
|
+
return own.length > 0 ? [{ type: 'section' as const, section: key, label, entries: groupByChapter(own) }] : []
|
|
48
|
+
})
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** The navigation seen from one page of `tree`: every group, the page marked
|
|
52
|
+
* active, and its neighbours. */
|
|
53
|
+
export function buildDocsNavigation(
|
|
54
|
+
tree: DocsTree,
|
|
55
|
+
currentId: string,
|
|
56
|
+
sections: { key: string; label: string }[] = [],
|
|
57
|
+
): DocsNavigation {
|
|
58
|
+
const position = tree.byId.get(currentId)?.index ?? -1
|
|
59
|
+
const items = tree.pages.map((page): DocsNavItem => ({
|
|
60
|
+
title: page.label,
|
|
61
|
+
section: page.section,
|
|
62
|
+
chapter: page.chapter,
|
|
63
|
+
href: page.href,
|
|
64
|
+
isActive: page.index === position,
|
|
65
|
+
}))
|
|
66
|
+
|
|
67
|
+
return {
|
|
68
|
+
groups:
|
|
69
|
+
sections.length > 0
|
|
70
|
+
? [...groupByChapter(items.filter((item) => !item.section)), ...groupBySection(items, sections)]
|
|
71
|
+
: groupByChapter(items),
|
|
72
|
+
previousPage: position > 0 ? items[position - 1] : null,
|
|
73
|
+
nextPage: position >= 0 && position < items.length - 1 ? items[position + 1] : null,
|
|
74
|
+
}
|
|
75
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { withDeploymentBase } from '../../site/urls.ts'
|
|
3
|
+
import Layout from '../layouts/Page.astro'
|
|
4
|
+
|
|
5
|
+
const homeHref = withDeploymentBase('/', import.meta.env.BASE_URL)
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
<Layout title="404" description="The page you're looking for doesn't exist.">
|
|
9
|
+
<div class="jaad-not-found">
|
|
10
|
+
<h1 class="jaad-not-found-title">404</h1>
|
|
11
|
+
<p class="jaad-not-found-text">
|
|
12
|
+
<i>Oops!</i> The page you're looking for doesn't exist.
|
|
13
|
+
</p>
|
|
14
|
+
<a href={homeHref}>Go back to the homepage</a>
|
|
15
|
+
</div>
|
|
16
|
+
</Layout>
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { render } from 'astro:content'
|
|
3
|
+
import { withDeploymentBase } from '../../site/urls.ts'
|
|
4
|
+
import { getDocsTree } from '../collection.ts'
|
|
5
|
+
import DocsPage from '../components/DocsPage.astro'
|
|
6
|
+
|
|
7
|
+
const tree = await getDocsTree()
|
|
8
|
+
|
|
9
|
+
const page = tree.pages[0]
|
|
10
|
+
if (!page) {
|
|
11
|
+
return Astro.redirect(withDeploymentBase('/404', import.meta.env.BASE_URL))
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const { Content } = await render(page.source)
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
<DocsPage {page} {tree}>
|
|
18
|
+
<Content />
|
|
19
|
+
</DocsPage>
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { render } from 'astro:content'
|
|
3
|
+
import { buildLocales, localePrefix } from '../../site/runtime.ts'
|
|
4
|
+
import { withDeploymentBase } from '../../site/urls.ts'
|
|
5
|
+
import { getDocsTree } from '../collection.ts'
|
|
6
|
+
import DocsPage from '../components/DocsPage.astro'
|
|
7
|
+
import { docsUrls } from '../runtime.ts'
|
|
8
|
+
import { docsPageHref } from '../urls.ts'
|
|
9
|
+
|
|
10
|
+
interface DocsPath {
|
|
11
|
+
params: { slug: string }
|
|
12
|
+
/** `redirect` is the canonical slug of a page reached by another name. */
|
|
13
|
+
props: { id: string; locale?: string; redirect?: string }
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export async function getStaticPaths() {
|
|
17
|
+
const paths: DocsPath[] = []
|
|
18
|
+
|
|
19
|
+
for (const locale of buildLocales()) {
|
|
20
|
+
const { pages } = await getDocsTree(locale)
|
|
21
|
+
const prefix = localePrefix(locale)
|
|
22
|
+
|
|
23
|
+
for (const page of pages) {
|
|
24
|
+
const add = (slug: string, redirect?: string) =>
|
|
25
|
+
paths.push({ params: { slug }, props: { id: page.id, locale, redirect } })
|
|
26
|
+
|
|
27
|
+
// Empty is the index route's; a prefixed locale has none, so /it is this page.
|
|
28
|
+
const own = `${prefix}${page.canonical}`.replace(/\/$/, '')
|
|
29
|
+
if (own) add(own)
|
|
30
|
+
for (const alias of page.aliases) add(`${prefix}${alias}`, page.canonical)
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return paths
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const { id, locale, redirect } = Astro.props
|
|
38
|
+
|
|
39
|
+
if (redirect !== undefined) {
|
|
40
|
+
return Astro.redirect(docsPageHref(redirect, docsUrls(locale)), 301)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const tree = await getDocsTree(locale)
|
|
44
|
+
const page = tree.byId.get(id)
|
|
45
|
+
if (!page) {
|
|
46
|
+
return Astro.redirect(withDeploymentBase('/404', import.meta.env.BASE_URL))
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const { Content } = await render(page.source)
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
<DocsPage {page} {tree} headTitle={page.title}>
|
|
53
|
+
<Content />
|
|
54
|
+
</DocsPage>
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { APIRoute, GetStaticPaths } from 'astro'
|
|
2
|
+
import { buildLocales, localePrefix } from '../../site/runtime.ts'
|
|
3
|
+
import { getDocsTree } from '../collection.ts'
|
|
4
|
+
|
|
5
|
+
/** Raw markdown at the page's own url plus `.md`, for readers and for LLMs. */
|
|
6
|
+
export const getStaticPaths: GetStaticPaths = async () => {
|
|
7
|
+
const paths: {
|
|
8
|
+
params: { slug: string }
|
|
9
|
+
props: { id: string; locale?: string }
|
|
10
|
+
}[] = []
|
|
11
|
+
|
|
12
|
+
for (const locale of buildLocales()) {
|
|
13
|
+
const { pages } = await getDocsTree(locale)
|
|
14
|
+
const prefix = localePrefix(locale)
|
|
15
|
+
|
|
16
|
+
for (const page of pages) {
|
|
17
|
+
paths.push({
|
|
18
|
+
params: { slug: `${prefix}${page.slug}` },
|
|
19
|
+
props: { id: page.id, locale },
|
|
20
|
+
})
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return paths
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export const GET: APIRoute = async ({ props }) => {
|
|
28
|
+
const { id, locale } = props as { id: string; locale?: string }
|
|
29
|
+
const page = (await getDocsTree(locale)).byId.get(id)
|
|
30
|
+
if (!page) return new Response('Not found', { status: 404 })
|
|
31
|
+
|
|
32
|
+
return new Response(page.source.body ?? '', {
|
|
33
|
+
headers: { 'Content-Type': 'text/markdown; charset=utf-8' },
|
|
34
|
+
})
|
|
35
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { APIRoute, GetStaticPaths } from 'astro'
|
|
2
|
+
import { getServedLocales } from '../collection.ts'
|
|
3
|
+
import { llmsResponse } from '../endpoints.ts'
|
|
4
|
+
|
|
5
|
+
export const getStaticPaths: GetStaticPaths = async () =>
|
|
6
|
+
(await getServedLocales()).map((locale) => ({ params: { locale } }))
|
|
7
|
+
|
|
8
|
+
export const GET: APIRoute = ({ site, params }) => llmsResponse(site, params.locale)
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { APIRoute, GetStaticPaths } from 'astro'
|
|
2
|
+
import { getServedLocales } from '../collection.ts'
|
|
3
|
+
import { searchIndexResponse } from '../endpoints.ts'
|
|
4
|
+
|
|
5
|
+
export const getStaticPaths: GetStaticPaths = async () =>
|
|
6
|
+
(await getServedLocales()).map((locale) => ({ params: { locale } }))
|
|
7
|
+
|
|
8
|
+
export const GET: APIRoute = ({ params }) => searchIndexResponse(params.locale)
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { DocsSection } from './config.ts'
|
|
2
|
+
import type { DocsUrlOptions } from './urls.ts'
|
|
3
|
+
import config from 'virtual:jaad/config'
|
|
4
|
+
import { DEFAULT_LOCALE } from '../site/runtime.ts'
|
|
5
|
+
|
|
6
|
+
/** The virtual config, read once. */
|
|
7
|
+
export const SECTIONS: DocsSection[] = config.docs.sections
|
|
8
|
+
|
|
9
|
+
/** The four fields every href builder takes, in one place. */
|
|
10
|
+
export function docsUrls(locale?: string): DocsUrlOptions {
|
|
11
|
+
return {
|
|
12
|
+
docsBase: config.docs.base,
|
|
13
|
+
deploymentBase: import.meta.env.BASE_URL,
|
|
14
|
+
locale,
|
|
15
|
+
defaultLocale: DEFAULT_LOCALE,
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { SearchIndex } from '../site/search.ts'
|
|
2
|
+
import type { DocsSection } from './config.ts'
|
|
3
|
+
import type { DocsTree } from './model/tree.ts'
|
|
4
|
+
import { formatChapterTitle } from './model/ids.ts'
|
|
5
|
+
import { pageChunks } from './model/rendered.ts'
|
|
6
|
+
|
|
7
|
+
/** One entry per h2 and h3 of every page, plus one for the text before them. */
|
|
8
|
+
export function buildSearchIndex(tree: DocsTree, sections: Pick<DocsSection, 'key' | 'label'>[]): SearchIndex {
|
|
9
|
+
const index: SearchIndex = {
|
|
10
|
+
sections: sections
|
|
11
|
+
.filter(({ key }) => tree.pages.some((page) => page.section === key))
|
|
12
|
+
.map(({ key, label }) => ({ key, label })),
|
|
13
|
+
pages: [],
|
|
14
|
+
entries: [],
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
for (const page of tree.pages) {
|
|
18
|
+
const chapter = formatChapterTitle(page.chapter)
|
|
19
|
+
const keywords = page.data.keywords ?? []
|
|
20
|
+
index.pages.push({
|
|
21
|
+
title: page.title,
|
|
22
|
+
href: page.href,
|
|
23
|
+
...(page.section ? { section: page.section } : {}),
|
|
24
|
+
...(chapter ? { chapter } : {}),
|
|
25
|
+
...(keywords.length > 0 ? { keywords } : {}),
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
for (const { heading, text } of pageChunks(page.source.rendered)) {
|
|
29
|
+
if (!heading && !text) continue
|
|
30
|
+
index.entries.push({
|
|
31
|
+
page: page.index,
|
|
32
|
+
...(heading ? { heading: heading.text, anchor: heading.slug } : {}),
|
|
33
|
+
text,
|
|
34
|
+
})
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return index
|
|
39
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
@layer components {
|
|
2
|
+
.docs-breadcrumbs {
|
|
3
|
+
display: flex;
|
|
4
|
+
flex-wrap: wrap;
|
|
5
|
+
gap: 0.375rem;
|
|
6
|
+
margin-bottom: 0.5rem;
|
|
7
|
+
font-size: 0.8125rem;
|
|
8
|
+
color: var(--color-foreground-muted);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
.docs-breadcrumbs-link {
|
|
12
|
+
color: inherit;
|
|
13
|
+
text-decoration: none;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
.docs-breadcrumbs-link:hover {
|
|
17
|
+
color: var(--color-foreground-bright);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/* Narrow, the navigation menu above already names them. */
|
|
21
|
+
@media (width < 64rem) {
|
|
22
|
+
[data-jaad-default-frame] .docs-breadcrumbs {
|
|
23
|
+
display: none;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
@layer components {
|
|
2
|
+
.docs-mobile-nav {
|
|
3
|
+
margin-bottom: 1.5rem;
|
|
4
|
+
border-top: 1px solid var(--color-border);
|
|
5
|
+
font-size: 0.875rem;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
.docs-mobile-menu {
|
|
9
|
+
border-bottom: 1px solid var(--color-border);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
.docs-mobile-summary {
|
|
13
|
+
display: flex;
|
|
14
|
+
flex-wrap: wrap;
|
|
15
|
+
align-items: center;
|
|
16
|
+
gap: 0.375rem;
|
|
17
|
+
min-height: 2.75rem;
|
|
18
|
+
color: var(--color-foreground-muted);
|
|
19
|
+
cursor: pointer;
|
|
20
|
+
list-style: none;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
.docs-mobile-summary::-webkit-details-marker {
|
|
24
|
+
display: none;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
.docs-mobile-icon {
|
|
28
|
+
width: 1rem;
|
|
29
|
+
height: 1rem;
|
|
30
|
+
margin-right: 0.25rem;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
.docs-mobile-current {
|
|
34
|
+
color: var(--color-foreground-bright);
|
|
35
|
+
font-weight: 500;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
.docs-mobile-tree {
|
|
39
|
+
padding: 0.25rem 0 1rem 0.75rem;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
@media (min-width: 64rem) {
|
|
43
|
+
.docs-mobile-nav {
|
|
44
|
+
display: none;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
@layer components {
|
|
2
|
+
.docs-page-nav {
|
|
3
|
+
margin-top: 3rem;
|
|
4
|
+
padding-top: 0.5rem;
|
|
5
|
+
border-top: 1px solid var(--color-border);
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
.docs-page-nav-row {
|
|
9
|
+
display: flex;
|
|
10
|
+
justify-content: space-between;
|
|
11
|
+
gap: 1rem;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
.docs-page-nav-link {
|
|
15
|
+
display: flex;
|
|
16
|
+
align-items: center;
|
|
17
|
+
gap: 0.5rem;
|
|
18
|
+
min-width: 0;
|
|
19
|
+
max-width: calc(50% - 0.5rem);
|
|
20
|
+
min-height: 2.75rem;
|
|
21
|
+
color: var(--color-foreground-bright);
|
|
22
|
+
font-size: 0.875rem;
|
|
23
|
+
text-decoration: none;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
.docs-page-nav-link:hover {
|
|
27
|
+
color: var(--color-foreground-muted);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
.docs-page-nav-icon {
|
|
31
|
+
flex: none;
|
|
32
|
+
width: 1rem;
|
|
33
|
+
height: 1rem;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
.docs-page-nav-label {
|
|
37
|
+
overflow: hidden;
|
|
38
|
+
text-overflow: ellipsis;
|
|
39
|
+
white-space: nowrap;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
@layer components {
|
|
2
|
+
.docs-sidebar-left nav {
|
|
3
|
+
font-size: 0.875rem;
|
|
4
|
+
line-height: 1.5;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
.docs-sidebar-list {
|
|
8
|
+
margin: 0;
|
|
9
|
+
padding: 0;
|
|
10
|
+
list-style: none;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/* The bar of the current page sits in the gutter, so every label lines up. */
|
|
14
|
+
.docs-sidebar-link {
|
|
15
|
+
display: block;
|
|
16
|
+
margin-left: -0.75rem;
|
|
17
|
+
padding: 0.25rem 0 0.25rem calc(0.75rem - 2px);
|
|
18
|
+
border-left: 2px solid transparent;
|
|
19
|
+
color: var(--color-foreground);
|
|
20
|
+
text-decoration: none;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
.docs-sidebar-link:hover {
|
|
24
|
+
color: var(--color-foreground-bright);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
.docs-sidebar-link[aria-current='page'] {
|
|
28
|
+
border-color: var(--color-foreground-bright);
|
|
29
|
+
color: var(--color-foreground-bright);
|
|
30
|
+
font-weight: 500;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
.docs-sidebar-chapter {
|
|
34
|
+
margin-top: 1rem;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
.docs-sidebar-chapter + li:not(.docs-sidebar-chapter) {
|
|
38
|
+
margin-top: 1rem;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
.docs-sidebar-chapter-title {
|
|
42
|
+
margin: 0 0 0.25rem;
|
|
43
|
+
font-size: 0.75rem;
|
|
44
|
+
font-weight: 500;
|
|
45
|
+
color: var(--color-foreground-muted);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
.docs-sidebar-section {
|
|
49
|
+
margin-top: 1rem;
|
|
50
|
+
padding-top: 0.75rem;
|
|
51
|
+
border-top: 1px solid var(--color-border);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
.docs-sidebar-section-title {
|
|
55
|
+
margin: 0 0 0.25rem;
|
|
56
|
+
color: var(--color-foreground-bright);
|
|
57
|
+
font-weight: 600;
|
|
58
|
+
}
|
|
59
|
+
}
|