@dogsbay/format-astro 0.2.0-beta.115 → 0.2.0-beta.117
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/dist/api-markdown.d.ts +8 -0
- package/dist/api-markdown.d.ts.map +1 -0
- package/dist/api-markdown.js +41 -0
- package/dist/api-markdown.js.map +1 -0
- package/dist/blog.d.ts.map +1 -1
- package/dist/blog.js +5 -1
- package/dist/blog.js.map +1 -1
- package/dist/browser.d.ts +14 -0
- package/dist/browser.d.ts.map +1 -0
- package/dist/browser.js +14 -0
- package/dist/browser.js.map +1 -0
- package/dist/canonical.d.ts +34 -0
- package/dist/canonical.d.ts.map +1 -0
- package/dist/canonical.js +107 -0
- package/dist/canonical.js.map +1 -0
- package/dist/head-metadata.d.ts +26 -0
- package/dist/head-metadata.d.ts.map +1 -0
- package/dist/head-metadata.js +56 -0
- package/dist/head-metadata.js.map +1 -0
- package/dist/index.d.ts +4 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -3
- package/dist/index.js.map +1 -1
- package/dist/lead.d.ts +1 -57
- package/dist/lead.d.ts.map +1 -1
- package/dist/lead.js +3 -132
- package/dist/lead.js.map +1 -1
- package/dist/llms-txt.d.ts +6 -10
- package/dist/llms-txt.d.ts.map +1 -1
- package/dist/llms-txt.js +48 -302
- package/dist/llms-txt.js.map +1 -1
- package/dist/plugins.d.ts.map +1 -1
- package/dist/plugins.js +3 -1
- package/dist/plugins.js.map +1 -1
- package/dist/project.d.ts +36 -1
- package/dist/project.d.ts.map +1 -1
- package/dist/project.js +276 -841
- package/dist/project.js.map +1 -1
- package/dist/redirects.d.ts +22 -0
- package/dist/redirects.d.ts.map +1 -1
- package/dist/redirects.js +70 -123
- package/dist/redirects.js.map +1 -1
- package/dist/serialize.d.ts +4 -13
- package/dist/serialize.d.ts.map +1 -1
- package/dist/serialize.js +304 -102
- package/dist/serialize.js.map +1 -1
- package/dist/sitemap.d.ts +44 -17
- package/dist/sitemap.d.ts.map +1 -1
- package/dist/sitemap.js +31 -104
- package/dist/sitemap.js.map +1 -1
- package/dist/taxonomy.d.ts.map +1 -1
- package/dist/taxonomy.js +7 -1
- package/dist/taxonomy.js.map +1 -1
- package/dist/theme-css.d.ts +26 -0
- package/dist/theme-css.d.ts.map +1 -0
- package/dist/theme-css.js +642 -0
- package/dist/theme-css.js.map +1 -0
- package/dist/tone-classes.d.ts +14 -0
- package/dist/tone-classes.d.ts.map +1 -0
- package/dist/tone-classes.js +30 -0
- package/dist/tone-classes.js.map +1 -0
- package/dist/write-if-changed.d.ts +21 -0
- package/dist/write-if-changed.d.ts.map +1 -0
- package/dist/write-if-changed.js +58 -0
- package/dist/write-if-changed.js.map +1 -0
- package/package.json +13 -5
package/dist/project.js
CHANGED
|
@@ -4,14 +4,31 @@
|
|
|
4
4
|
* Takes ExportPage[] + NavItem[] and generates a complete Astro project
|
|
5
5
|
* with static .astro pages using real Dogsbay components.
|
|
6
6
|
*/
|
|
7
|
-
import { existsSync, mkdirSync,
|
|
7
|
+
import { existsSync, mkdirSync, readFileSync, cpSync, readdirSync, statSync, rmSync, rmdirSync, openSync, readSync, closeSync, } from "node:fs";
|
|
8
|
+
// Unchanged files are not rewritten, so `site dev` reloads only what changed
|
|
9
|
+
import { writeFileSync, copyFileSync } from "./write-if-changed.js";
|
|
8
10
|
import { join, dirname, relative, resolve } from "node:path";
|
|
9
11
|
import { fileURLToPath } from "node:url";
|
|
10
12
|
import { treeToDogsbayMd } from "@dogsbay/format-dogsbay-md";
|
|
11
|
-
import { treeToAstro
|
|
13
|
+
import { treeToAstro } from "./serialize.js";
|
|
14
|
+
import { missingTranslations } from "@dogsbay/site-files";
|
|
15
|
+
import { sanitizeTree } from "@dogsbay/render-html/sanitize";
|
|
16
|
+
import { generateGlobalCss, themeCss } from "./theme-css.js";
|
|
17
|
+
/**
|
|
18
|
+
* A value as a JavaScript literal in a generated page. `<` becomes \u003c: Astro reads
|
|
19
|
+
* a page's frontmatter and template looking for markup, so a `<script>` inside a string
|
|
20
|
+
* (a page's own Markdown, a code block showing one) would be taken for a real script tag
|
|
21
|
+
* and its text compiled as JavaScript, which fails Vite's dependency scan.
|
|
22
|
+
*/
|
|
23
|
+
function pageLiteral(value, indent) {
|
|
24
|
+
return (JSON.stringify(value, null, indent) ?? "undefined").replace(/</g, "\\u003c");
|
|
25
|
+
}
|
|
12
26
|
import { emitPluginRuntime } from "./plugins.js";
|
|
13
27
|
import { buildLlmsTxt, buildSectionLlmsTxt, buildLlmsFullTxt } from "./llms-txt.js";
|
|
14
|
-
import {
|
|
28
|
+
import { buildSitemapFiles, buildSitemapIndex } from "./sitemap.js";
|
|
29
|
+
import { canonicalPlan } from "./canonical.js";
|
|
30
|
+
import { headMetadata } from "./head-metadata.js";
|
|
31
|
+
import { buildHeadersFile, buildRobotsTxt as buildRobotsTxtFile } from "@dogsbay/site-files";
|
|
15
32
|
import { assembleBook, estimateTreeBytes } from "./granularity.js";
|
|
16
33
|
/**
|
|
17
34
|
* The per-topic "Read as single page" affordance (granularity book view).
|
|
@@ -571,6 +588,8 @@ function buildSiteConfig(siteName, options, outputDir) {
|
|
|
571
588
|
cfg.editUri = options.editUri;
|
|
572
589
|
if (options.copyright)
|
|
573
590
|
cfg.copyright = options.copyright;
|
|
591
|
+
if (options.tagline)
|
|
592
|
+
cfg.tagline = options.tagline;
|
|
574
593
|
// Written explicitly, both ways: the default is ON, so an ABSENT key in
|
|
575
594
|
// site.json would be indistinguishable from "not yet regenerated" and the
|
|
576
595
|
// page template would have to guess. `false` is what the opt-out looks like.
|
|
@@ -591,6 +610,9 @@ function buildSiteConfig(siteName, options, outputDir) {
|
|
|
591
610
|
if (options.tagLabels && Object.keys(options.tagLabels).length > 0) {
|
|
592
611
|
cfg.tagLabels = options.tagLabels;
|
|
593
612
|
}
|
|
613
|
+
if (options.tagAutoColor) {
|
|
614
|
+
cfg.tagAutoColor = true;
|
|
615
|
+
}
|
|
594
616
|
// Link-icon glyphs — only emitted when configured, so sites without
|
|
595
617
|
// the feature keep byte-identical site.json.
|
|
596
618
|
if (options.linkIcons && (options.linkIcons.external || options.linkIcons.internal)) {
|
|
@@ -1406,15 +1428,12 @@ export default defineConfig({
|
|
|
1406
1428
|
else {
|
|
1407
1429
|
scaffoldFilesSkipped += 2; // global.css + theme.css
|
|
1408
1430
|
}
|
|
1409
|
-
// UI components
|
|
1410
|
-
//
|
|
1411
|
-
//
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
else {
|
|
1416
|
-
scaffoldFilesSkipped++;
|
|
1417
|
-
}
|
|
1431
|
+
// UI components. Per shadcn pattern, copies are owned by the project and
|
|
1432
|
+
// meant to be edited, so an existing component folder is never touched.
|
|
1433
|
+
// Missing ones are added on every build: the serializer starts emitting a
|
|
1434
|
+
// new component (Kbd, FileTree) in a release, and a site scaffolded before
|
|
1435
|
+
// it would otherwise fail with "failed to resolve @ui/kbd/Kbd.astro".
|
|
1436
|
+
copyComponents(outputDir);
|
|
1418
1437
|
return scaffoldFilesSkipped;
|
|
1419
1438
|
}
|
|
1420
1439
|
// ─── Tier 1: content ────────────────────────────────────────────────────
|
|
@@ -1473,6 +1492,7 @@ const favicon = (siteConfigData as { favicon?: string }).favicon;
|
|
|
1473
1492
|
|
|
1474
1493
|
<DocsLayout
|
|
1475
1494
|
siteName={siteConfig.siteName}
|
|
1495
|
+
siteTagline={siteConfig.tagline}
|
|
1476
1496
|
title="Page not found"
|
|
1477
1497
|
description="That page does not exist. Here is where to look instead."
|
|
1478
1498
|
nav={navData as never}
|
|
@@ -1498,7 +1518,30 @@ const favicon = (siteConfigData as { favicon?: string }).favicon;
|
|
|
1498
1518
|
</DocsLayout>
|
|
1499
1519
|
`);
|
|
1500
1520
|
}
|
|
1521
|
+
/**
|
|
1522
|
+
* Nav entries come from a project's own TOC files (toctrees, `_toc.yml`,
|
|
1523
|
+
* `myst.yml`, nav.yml), i.e. from content. An href with a scheme other than
|
|
1524
|
+
* http(s)/mailto/tel — `javascript:` above all — is dropped (the entry keeps
|
|
1525
|
+
* its label and children), so it cannot reach the sidebar, nav.json or a
|
|
1526
|
+
* page's previous/next link.
|
|
1527
|
+
*/
|
|
1528
|
+
export function dropUnsafeNavHrefs(items) {
|
|
1529
|
+
return items.map((item) => {
|
|
1530
|
+
const out = { ...item };
|
|
1531
|
+
if (typeof out.href === "string") {
|
|
1532
|
+
// Browsers strip tabs, newlines and other C0 controls from a URL
|
|
1533
|
+
// (`java\tscript:` IS javascript:), so the scheme is tested without them.
|
|
1534
|
+
const probe = out.href.replace(/[\u0000-\u0020]/g, "");
|
|
1535
|
+
if (/^[a-z][a-z0-9+.-]*:/i.test(probe) && !/^(?:https?|mailto|tel):/i.test(probe))
|
|
1536
|
+
delete out.href;
|
|
1537
|
+
}
|
|
1538
|
+
if (out.children)
|
|
1539
|
+
out.children = dropUnsafeNavHrefs(out.children);
|
|
1540
|
+
return out;
|
|
1541
|
+
});
|
|
1542
|
+
}
|
|
1501
1543
|
export async function emitAstroPages(pages, nav, outputDir, options) {
|
|
1544
|
+
nav = dropUnsafeNavHrefs(nav);
|
|
1502
1545
|
const siteName = options.siteName || "Documentation";
|
|
1503
1546
|
// basePath = filesystem layout prefix (where pages live under
|
|
1504
1547
|
// src/pages/...). combined = the URL prefix HTML hrefs need
|
|
@@ -1718,6 +1761,33 @@ export async function emitAstroPages(pages, nav, outputDir, options) {
|
|
|
1718
1761
|
: undefined;
|
|
1719
1762
|
const blogAdj = blogData ? blogAdjacency(blogData) : undefined;
|
|
1720
1763
|
const blogMinutes = new Map(blogData ? blogData.posts.map((post) => [post.slug, post.readingMinutes]) : []);
|
|
1764
|
+
// Older versions' pages name the default version's page as canonical (src/canonical.ts).
|
|
1765
|
+
const isFragmentPage = (p) => p.fragment === true || p.frontmatter?._fragment === true;
|
|
1766
|
+
// The version served at the root (`path: ""`) and the other versions'
|
|
1767
|
+
// segments: the nav filter keeps the root version's bucket by excluding them.
|
|
1768
|
+
const rootVersion = rootVersionOf(options);
|
|
1769
|
+
const otherVersionSegments = rootVersion
|
|
1770
|
+
? pageVersions(pages).filter((v) => v !== rootVersion)
|
|
1771
|
+
: [];
|
|
1772
|
+
const canonical = canonicalPlan(pages, {
|
|
1773
|
+
isEmitted: (p) => !isFragmentPage(p) && !isExcludedSlug(p.slug),
|
|
1774
|
+
siteUrl: options.siteUrl,
|
|
1775
|
+
basePath: combined,
|
|
1776
|
+
versions: options.versions,
|
|
1777
|
+
defaultVersion: options.defaultVersion,
|
|
1778
|
+
redirects: options.redirects,
|
|
1779
|
+
});
|
|
1780
|
+
// hreflang alternates, WebSite on home pages, BreadcrumbList from the nav (src/head-metadata.ts).
|
|
1781
|
+
const head = headMetadata(pages, outputNav, {
|
|
1782
|
+
siteUrl: options.siteUrl,
|
|
1783
|
+
basePath: combined,
|
|
1784
|
+
defaultLocale: options.defaultLocale,
|
|
1785
|
+
isEmitted: (p) => !isFragmentPage(p) && !isExcludedSlug(p.slug),
|
|
1786
|
+
indexable: (p) => {
|
|
1787
|
+
const c = canonical.get(p.slug);
|
|
1788
|
+
return !options.noindex && !c?.url && !c?.noindex && p.frontmatter?.noindex !== true;
|
|
1789
|
+
},
|
|
1790
|
+
});
|
|
1721
1791
|
for (const page of emitPages) {
|
|
1722
1792
|
try {
|
|
1723
1793
|
// Skip excluded pages before any expensive work (tree rewrite,
|
|
@@ -1741,6 +1811,14 @@ export async function emitAstroPages(pages, nav, outputDir, options) {
|
|
|
1741
1811
|
if (typeof page.redirect === "string" && page.redirect.trim()) {
|
|
1742
1812
|
continue;
|
|
1743
1813
|
}
|
|
1814
|
+
// content.html: sanitize filters raw HTML and directive attributes, as dogsbay-ssg
|
|
1815
|
+
// does (script links are refused by the serializer either way: isSafeHref).
|
|
1816
|
+
if (options.html === "sanitize") {
|
|
1817
|
+
const removed = new Set();
|
|
1818
|
+
page.tree = sanitizeTree(page.tree, (_code, message) => removed.add(message));
|
|
1819
|
+
for (const m of removed)
|
|
1820
|
+
console.warn(`[html] ${page.slug || "/"}: ${m}`);
|
|
1821
|
+
}
|
|
1744
1822
|
// Rewrite internal hrefs to match the output URL structure
|
|
1745
1823
|
rewriteTreeHrefs(page.tree, hrefPrefix);
|
|
1746
1824
|
// Same for raw image srcs — Astro doesn't auto-prefix
|
|
@@ -1751,6 +1829,7 @@ export async function emitAstroPages(pages, nav, outputDir, options) {
|
|
|
1751
1829
|
const result = treeToAstro(page.tree, {
|
|
1752
1830
|
imageOptimization: useImageOpt,
|
|
1753
1831
|
codeBlockTitle: options.codeBlockTitle ?? true,
|
|
1832
|
+
imageCaptions: options.imageCaptions === "title",
|
|
1754
1833
|
combinedPrefix: hrefPrefix,
|
|
1755
1834
|
});
|
|
1756
1835
|
const imageSetup = useImageOpt ? [
|
|
@@ -1786,9 +1865,12 @@ export async function emitAstroPages(pages, nav, outputDir, options) {
|
|
|
1786
1865
|
// cannot opt out of a site-level lockdown. `draft: true` keeps
|
|
1787
1866
|
// its existing role as a noindex shorthand. See
|
|
1788
1867
|
// plans/site-level-robots-meta.md.
|
|
1868
|
+
const pageCanonical = canonical.get(page.slug);
|
|
1869
|
+
const pageHead = head.get(page.slug);
|
|
1789
1870
|
const pageNoindex = options.noindex === true ||
|
|
1790
1871
|
fm.noindex === true ||
|
|
1791
|
-
fm.draft === true
|
|
1872
|
+
fm.draft === true ||
|
|
1873
|
+
pageCanonical?.noindex === true;
|
|
1792
1874
|
const pageNofollow = options.nofollow === true ||
|
|
1793
1875
|
fm.nofollow === true;
|
|
1794
1876
|
// Independent of noindex: pages can be excluded from in-site
|
|
@@ -1941,7 +2023,7 @@ export async function emitAstroPages(pages, nav, outputDir, options) {
|
|
|
1941
2023
|
...result.imports,
|
|
1942
2024
|
...imageSetup,
|
|
1943
2025
|
"",
|
|
1944
|
-
`const headings = ${
|
|
2026
|
+
`const headings = ${pageLiteral(page.headings || [])};`,
|
|
1945
2027
|
`const nav = navData;`,
|
|
1946
2028
|
// currentPath uses combined so it matches nav.json hrefs
|
|
1947
2029
|
// (which are also combined-prefixed). getPagination compares
|
|
@@ -1959,10 +2041,16 @@ export async function emitAstroPages(pages, nav, outputDir, options) {
|
|
|
1959
2041
|
// → "/docs" makes the filter look for `/docs/…` bucket hrefs that
|
|
1960
2042
|
// don't exist, emptying the pagination nav so prev/next vanish on
|
|
1961
2043
|
// every page of a root-served multi-source site.
|
|
1962
|
-
` basePath: ${
|
|
1963
|
-
` namespace: ${
|
|
1964
|
-
` version: ${
|
|
1965
|
-
` locale: ${
|
|
2044
|
+
` basePath: ${pageLiteral(basePath ?? "")},`,
|
|
2045
|
+
` namespace: ${pageLiteral(page.multiSource?.namespace ?? null)} ?? undefined,`,
|
|
2046
|
+
` version: ${pageLiteral(page.multiSource?.version ?? null)} ?? undefined,`,
|
|
2047
|
+
` locale: ${pageLiteral(page.multiSource?.locale ?? null)} ?? undefined,`,
|
|
2048
|
+
...(rootVersion
|
|
2049
|
+
? [
|
|
2050
|
+
` rootVersion: ${pageLiteral(rootVersion)},`,
|
|
2051
|
+
` versionSegments: ${pageLiteral(otherVersionSegments)},`,
|
|
2052
|
+
]
|
|
2053
|
+
: []),
|
|
1966
2054
|
`});`,
|
|
1967
2055
|
// Blog adjacency is CHRONOLOGICAL, not navigational. getPagination
|
|
1968
2056
|
// walks NavItem[] — sidebar order — and a blog has no sidebar; its
|
|
@@ -1971,25 +2059,28 @@ export async function emitAstroPages(pages, nav, outputDir, options) {
|
|
|
1971
2059
|
// runtime.
|
|
1972
2060
|
...(blogAdj
|
|
1973
2061
|
? [
|
|
1974
|
-
`const prev = ${
|
|
1975
|
-
`const next = ${
|
|
2062
|
+
`const prev = ${pageLiteral(blogAdj.get(page.slug)?.prev ?? null)} ?? undefined;`,
|
|
2063
|
+
`const next = ${pageLiteral(blogAdj.get(page.slug)?.next ?? null)} ?? undefined;`,
|
|
1976
2064
|
]
|
|
1977
2065
|
: [`const { prev, next } = getPagination(currentPath, _navForPagination);`]),
|
|
1978
|
-
`const title = ${
|
|
1979
|
-
`const description = ${
|
|
1980
|
-
`const ogImage = ${
|
|
1981
|
-
`const noindex = ${
|
|
1982
|
-
`const
|
|
1983
|
-
`const
|
|
1984
|
-
`const
|
|
1985
|
-
`const
|
|
1986
|
-
`const
|
|
1987
|
-
`const
|
|
1988
|
-
`const
|
|
1989
|
-
`const
|
|
1990
|
-
`const
|
|
1991
|
-
`const
|
|
1992
|
-
`const
|
|
2066
|
+
`const title = ${pageLiteral(page.title)};`,
|
|
2067
|
+
`const description = ${pageLiteral(pageDescription)} || undefined;`,
|
|
2068
|
+
`const ogImage = ${pageLiteral(pageOgImage)} || undefined;`,
|
|
2069
|
+
`const noindex = ${pageLiteral(pageNoindex)};`,
|
|
2070
|
+
`const canonicalUrl = ${pageLiteral(pageCanonical?.url ?? null)} ?? undefined;`,
|
|
2071
|
+
`const alternates = ${pageLiteral(pageHead?.alternates ?? [])};`,
|
|
2072
|
+
`const breadcrumbJsonLd = ${pageLiteral(pageHead?.breadcrumbJsonLd ?? null)} ?? undefined;`,
|
|
2073
|
+
`const nofollow = ${pageLiteral(pageNofollow)};`,
|
|
2074
|
+
`const excludeFromSearch = ${pageLiteral(pageExcludeFromSearch)};`,
|
|
2075
|
+
`const pageTags = ${pageLiteral(pageTags ?? null)};`,
|
|
2076
|
+
`const pageStatus = ${pageLiteral(pageStatus ?? null)};`,
|
|
2077
|
+
`const pageType = ${pageLiteral(pageTypeStr ?? null)};`,
|
|
2078
|
+
`const pageAudience = ${pageLiteral(pageAudience ?? null)};`,
|
|
2079
|
+
`const pageCategory = ${pageLiteral(pageCategory ?? null)};`,
|
|
2080
|
+
`const pageTaxonomies = ${pageLiteral(pageTaxonomies ?? null)};`,
|
|
2081
|
+
`const tagsIndexPath = ${pageLiteral(tagsIndexPath)};`,
|
|
2082
|
+
`const lastUpdated = ${pageLiteral(pageLastUpdated)} || undefined;`,
|
|
2083
|
+
`const llmActionsProps = ${pageLiteral(llmActionsEnabled
|
|
1993
2084
|
? {
|
|
1994
2085
|
enabled: true,
|
|
1995
2086
|
providers: options.llmActions?.providers,
|
|
@@ -2005,6 +2096,7 @@ export async function emitAstroPages(pages, nav, outputDir, options) {
|
|
|
2005
2096
|
"",
|
|
2006
2097
|
`<DocsLayout`,
|
|
2007
2098
|
` siteName={siteConfig.siteName}`,
|
|
2099
|
+
` siteTagline={siteConfig.tagline}`,
|
|
2008
2100
|
` title={title}`,
|
|
2009
2101
|
` nav={nav as any[]}`,
|
|
2010
2102
|
` headings={headings}`,
|
|
@@ -2019,6 +2111,10 @@ export async function emitAstroPages(pages, nav, outputDir, options) {
|
|
|
2019
2111
|
` themeColor={siteConfig.themeColor || undefined}`,
|
|
2020
2112
|
` noindex={noindex}`,
|
|
2021
2113
|
` nofollow={nofollow}`,
|
|
2114
|
+
` canonicalUrl={canonicalUrl}`,
|
|
2115
|
+
` alternates={alternates}`,
|
|
2116
|
+
` breadcrumbJsonLd={breadcrumbJsonLd}`,
|
|
2117
|
+
...(pageHead?.home ? [` ogType="website"`] : []),
|
|
2022
2118
|
` excludeFromSearch={excludeFromSearch}`,
|
|
2023
2119
|
` plausibleDomain={siteConfig.plausible?.domain}`,
|
|
2024
2120
|
` plausibleScriptUrl={siteConfig.plausible?.scriptUrl}`,
|
|
@@ -2028,6 +2124,7 @@ export async function emitAstroPages(pages, nav, outputDir, options) {
|
|
|
2028
2124
|
` tagsIndexPath={tagsIndexPath}`,
|
|
2029
2125
|
` tagPrefixes={siteConfig.tagPrefixes}`,
|
|
2030
2126
|
` tagLabels={siteConfig.tagLabels}`,
|
|
2127
|
+
` tagAutoColor={siteConfig.tagAutoColor}`,
|
|
2031
2128
|
` taxonomyIndexPaths={siteConfig.taxonomyIndexPaths}`,
|
|
2032
2129
|
` taxonomyDisplay={siteConfig.taxonomyDisplay}`,
|
|
2033
2130
|
` status={pageStatus ?? undefined}`,
|
|
@@ -2042,7 +2139,7 @@ export async function emitAstroPages(pages, nav, outputDir, options) {
|
|
|
2042
2139
|
` linkIcons={siteConfig.linkIcons}`,
|
|
2043
2140
|
// Default ON: an absent key means yes, `false` means no.
|
|
2044
2141
|
` poweredBy={siteConfig.poweredBy !== false}`,
|
|
2045
|
-
` multiSource={${
|
|
2142
|
+
` multiSource={${pageLiteral(page.multiSource ?? null)} ?? undefined}`,
|
|
2046
2143
|
` switcherMap={switcherMapData}`,
|
|
2047
2144
|
// basePath here is the COMBINED URL prefix (urlBase from
|
|
2048
2145
|
// site.url's path + dogsbay basePath). DocsLayout uses it
|
|
@@ -2051,19 +2148,19 @@ export async function emitAstroPages(pages, nav, outputDir, options) {
|
|
|
2051
2148
|
// prefix the host actually serves under. Empty string is
|
|
2052
2149
|
// valid (root-served sites with no urlBase or basePath);
|
|
2053
2150
|
// don't fall back to "/docs" — that would 404 for those.
|
|
2054
|
-
` basePath={${
|
|
2151
|
+
` basePath={${pageLiteral(combined)}}`,
|
|
2055
2152
|
// navMode — controls whether DocsLayout server-renders the
|
|
2056
2153
|
// full sidebar nav tree per page (`ssr-full`) or emits a
|
|
2057
2154
|
// client-hydrated placeholder (`client`, default). The
|
|
2058
2155
|
// client mode shrinks per-page HTML dramatically at scale.
|
|
2059
2156
|
// See plans/client-rendered-nav.md.
|
|
2060
|
-
` navMode={${
|
|
2157
|
+
` navMode={${pageLiteral(options.navMode ?? "client")}}`,
|
|
2061
2158
|
// Pagefind index URL — must include the combined prefix or
|
|
2062
2159
|
// the loader 404s on subpath-mounted deploys. The pagefind
|
|
2063
2160
|
// CLI writes to <astroOutput>/dist/pagefind/ which Astro
|
|
2064
2161
|
// serves under its `base` (= urlBase); dogsbay's basePath
|
|
2065
2162
|
// adds the second prefix layer. Empty combined → `/pagefind/`.
|
|
2066
|
-
` pagefindUrl={${
|
|
2163
|
+
` pagefindUrl={${pageLiteral(combined ? `${combined}/pagefind/` : "/pagefind/")}}`,
|
|
2067
2164
|
// Base Pagefind prefixes RESULT urls with. The index stores URLs
|
|
2068
2165
|
// relative to dist/, so the base is whatever served path dist/'s
|
|
2069
2166
|
// root maps to. A Workers subpath mount builds pages INTO
|
|
@@ -2073,7 +2170,7 @@ export async function emitAstroPages(pages, nav, outputDir, options) {
|
|
|
2073
2170
|
// is right only for the second, and doubled the mount on the first:
|
|
2074
2171
|
// /dogsbay-xml-docs/dogsbay-xml-docs/…, measured. Same rule as
|
|
2075
2172
|
// docs-layout's pagefindBaseUrl() — keep the two in step.
|
|
2076
|
-
` pagefindBaseUrl={${
|
|
2173
|
+
` pagefindBaseUrl={${pageLiteral(searchBaseUrl)}}`,
|
|
2077
2174
|
// Favicon — composed with combined prefix so the
|
|
2078
2175
|
// <link rel="icon"> resolves on subpath-mounted deploys.
|
|
2079
2176
|
//
|
|
@@ -2083,7 +2180,7 @@ export async function emitAstroPages(pages, nav, outputDir, options) {
|
|
|
2083
2180
|
// `public/favicon.ico` (or `.svg`) in their Astro project and the
|
|
2084
2181
|
// link appears on the next build; DocsLayout takes `false` to mean
|
|
2085
2182
|
// "emit no <link rel=icon>".
|
|
2086
|
-
` favicon={${
|
|
2183
|
+
` favicon={${pageLiteral(faviconHref(outputDir, parseSiteUrl(options.siteUrl).urlBase))}}`,
|
|
2087
2184
|
` wideLayout={${wideLayout}}`,
|
|
2088
2185
|
// Blog chrome — sidebar dropped, byline on. Every other prop above
|
|
2089
2186
|
// is unchanged, which is what keeps the blog visually continuous
|
|
@@ -2091,9 +2188,9 @@ export async function emitAstroPages(pages, nav, outputDir, options) {
|
|
|
2091
2188
|
...(options.blog
|
|
2092
2189
|
? [
|
|
2093
2190
|
` chrome="blog"`,
|
|
2094
|
-
` author={${
|
|
2095
|
-
` publishedDate={${
|
|
2096
|
-
` updatedDate={${
|
|
2191
|
+
` author={${pageLiteral(page.meta?.author ?? null)} ?? undefined}`,
|
|
2192
|
+
` publishedDate={${pageLiteral(page.meta?.[options.blog.dateField] ?? null)} ?? undefined}`,
|
|
2193
|
+
` updatedDate={${pageLiteral(page.meta?.updated ?? null)} ?? undefined}`,
|
|
2097
2194
|
` readingMinutes={${blogMinutes.get(page.slug) ?? 1}}`,
|
|
2098
2195
|
// Series position, derived in buildBlogData from publication
|
|
2099
2196
|
// order — never hand-typed in prose, which goes stale the
|
|
@@ -2102,12 +2199,12 @@ export async function emitAstroPages(pages, nav, outputDir, options) {
|
|
|
2102
2199
|
// `heroImage: {src, alt}` object would otherwise reach
|
|
2103
2200
|
// DocsLayout's `heroImage?: string` prop and render as
|
|
2104
2201
|
// src="[object Object]".
|
|
2105
|
-
` heroImage={${
|
|
2202
|
+
` heroImage={${pageLiteral(typeof (page.frontmatter ?? {}).heroImage === "string"
|
|
2106
2203
|
? (page.frontmatter ?? {}).heroImage
|
|
2107
2204
|
: null)} ?? undefined}`,
|
|
2108
2205
|
]
|
|
2109
2206
|
: []),
|
|
2110
|
-
` toc={${
|
|
2207
|
+
` toc={${pageLiteral(tocMode)}}`,
|
|
2111
2208
|
`>`,
|
|
2112
2209
|
// RightRail region — assigned to DocsLayout's `right-rail` named
|
|
2113
2210
|
// slot. Inert (empty passthrough) until a plugin contributes a
|
|
@@ -2214,8 +2311,14 @@ export async function emitAstroPages(pages, nav, outputDir, options) {
|
|
|
2214
2311
|
// the same contract the site root gets: redirect to its first nav
|
|
2215
2312
|
// entry.
|
|
2216
2313
|
const subtreeRoots = axisSubtreeRoots(pages);
|
|
2314
|
+
// A subtree's entries leave out the deeper subtrees nested in it: with the
|
|
2315
|
+
// default version at the root (`path: ""`) and a locale axis, `/en/` is
|
|
2316
|
+
// that version's subtree and `/en/v3/` another version's.
|
|
2317
|
+
const nestedIn = (slugPrefix) => [...subtreeRoots.keys()]
|
|
2318
|
+
.filter((q) => slugPrefix === "" ? q !== "" : q.startsWith(`${slugPrefix}/`))
|
|
2319
|
+
.map((q) => `${combined}/${q}`);
|
|
2217
2320
|
for (const slugPrefix of subtreeRoots.keys()) {
|
|
2218
|
-
const target = findFirstNavHrefUnder(outputNav, [`${combined}/${slugPrefix}`]);
|
|
2321
|
+
const target = findFirstNavHrefUnder(outputNav, [`${combined}/${slugPrefix}`], nestedIn(slugPrefix));
|
|
2219
2322
|
if (!target)
|
|
2220
2323
|
continue;
|
|
2221
2324
|
const subtreeIndexPath = join(pagesDir, section ? section : "", slugPrefix, "index.astro");
|
|
@@ -2241,9 +2344,16 @@ export async function emitAstroPages(pages, nav, outputDir, options) {
|
|
|
2241
2344
|
.filter(([, version]) => version === options.defaultVersion)
|
|
2242
2345
|
.map(([slugPrefix]) => `${combined}/${slugPrefix}`)
|
|
2243
2346
|
: [];
|
|
2347
|
+
// With the default version at the root (`path: ""`) and no locale or
|
|
2348
|
+
// namespace, it has no subtree: its entries are the ones outside every
|
|
2349
|
+
// other version's subtree.
|
|
2244
2350
|
const firstHref = (defaultVersionPrefixes.length > 0
|
|
2245
|
-
? findFirstNavHrefUnder(outputNav, defaultVersionPrefixes
|
|
2246
|
-
|
|
2351
|
+
? findFirstNavHrefUnder(outputNav, defaultVersionPrefixes, [...subtreeRoots]
|
|
2352
|
+
.filter(([q]) => defaultVersionPrefixes.some((d) => `${combined}/${q}`.startsWith(`${d}/`)))
|
|
2353
|
+
.map(([q]) => `${combined}/${q}`))
|
|
2354
|
+
: rootVersion && subtreeRoots.size > 0
|
|
2355
|
+
? findFirstNavHrefUnder(outputNav, [combined], nestedIn(""))
|
|
2356
|
+
: undefined) ?? findFirstNavHref(outputNav, basePath);
|
|
2247
2357
|
const needsRootRedirect = basePath !== "" ||
|
|
2248
2358
|
(!section && !rootIndexEmitted && firstHref !== "");
|
|
2249
2359
|
if (needsRootRedirect) {
|
|
@@ -2343,6 +2453,8 @@ function emitRedirects(outputDir, options, pages) {
|
|
|
2343
2453
|
slug: p.slug,
|
|
2344
2454
|
redirect: p.redirect,
|
|
2345
2455
|
frontmatter: p.frontmatter,
|
|
2456
|
+
// Front matter paths are relative to the page's language and version (#096): collectRedirects needs its original slug.
|
|
2457
|
+
multiSource: p.multiSource,
|
|
2346
2458
|
})),
|
|
2347
2459
|
config: options.redirects,
|
|
2348
2460
|
prefix,
|
|
@@ -2464,8 +2576,9 @@ export function emitSwitcherMap(pages, outputDir, options) {
|
|
|
2464
2576
|
writeFileSync(outPath, JSON.stringify({ versions: [], locales: [], byLogicalKey: {} }, null, 2));
|
|
2465
2577
|
return;
|
|
2466
2578
|
}
|
|
2579
|
+
const rootVersion = rootVersionOf(options);
|
|
2467
2580
|
const versions = composeAxisHeader(options.versions ?? [], versionsSeen, options.defaultVersion,
|
|
2468
|
-
/* allowEol */ true);
|
|
2581
|
+
/* allowEol */ true).map((v) => (v.id === rootVersion ? { ...v, root: true } : v));
|
|
2469
2582
|
const locales = composeAxisHeader(options.locales ?? [], localesSeen, options.defaultLocale,
|
|
2470
2583
|
/* allowEol */ false);
|
|
2471
2584
|
// Per-logical-key list of variants — every (locale, version)
|
|
@@ -2557,65 +2670,50 @@ export function emitMissingTranslationStubs(pages, outputDir, options) {
|
|
|
2557
2670
|
const defaultLocale = options.defaultLocale;
|
|
2558
2671
|
if (!defaultLocale)
|
|
2559
2672
|
return;
|
|
2560
|
-
const knownLocales = new Set();
|
|
2561
|
-
for (const p of pages) {
|
|
2562
|
-
const l = p.multiSource?.locale;
|
|
2563
|
-
if (l)
|
|
2564
|
-
knownLocales.add(l);
|
|
2565
|
-
}
|
|
2566
|
-
if (knownLocales.size < 2)
|
|
2567
|
-
return;
|
|
2568
|
-
if (!knownLocales.has(defaultLocale))
|
|
2569
|
-
return;
|
|
2570
2673
|
const basePath = normalizeBasePath(options.basePath);
|
|
2571
2674
|
const baseSegments = basePathSegments(basePath);
|
|
2572
2675
|
// combined drives the redirect URL (the user-facing path they
|
|
2573
2676
|
// get bounced to); basePath stays the filesystem path under
|
|
2574
2677
|
// src/pages/ where the stub lives.
|
|
2575
2678
|
const combined = combinedPrefix(options);
|
|
2576
|
-
//
|
|
2577
|
-
//
|
|
2578
|
-
//
|
|
2579
|
-
//
|
|
2580
|
-
//
|
|
2581
|
-
//
|
|
2582
|
-
//
|
|
2583
|
-
|
|
2584
|
-
|
|
2585
|
-
const
|
|
2679
|
+
// Which pages need a stub is @dogsbay/site-files' missingTranslations,
|
|
2680
|
+
// shared with dogsbay-ssg. A page's key is its slug after the locale
|
|
2681
|
+
// segment, inside its version. Slugs on a namespaced site start with the
|
|
2682
|
+
// namespace, not the locale, so such a site gets no stubs. Only pages that
|
|
2683
|
+
// are routed get one: not include-only fragments, `excludeFromRoutes`
|
|
2684
|
+
// folders or `redirect:` pages. In another language, a `redirect:` page
|
|
2685
|
+
// holds the URL, so it counts as present.
|
|
2686
|
+
const units = new Map();
|
|
2687
|
+
const defaultPages = new Map();
|
|
2688
|
+
const isExcludedSlug = makeSlugExcluder(options.excludeFromRoutes);
|
|
2689
|
+
const groupOf = (ms) => `${ms.namespace ?? ""}\0${ms.version ?? ""}`;
|
|
2586
2690
|
for (const p of pages) {
|
|
2587
|
-
|
|
2588
|
-
|
|
2589
|
-
let emitted = 0;
|
|
2590
|
-
for (const defaultPage of pages) {
|
|
2591
|
-
const ms = defaultPage.multiSource;
|
|
2592
|
-
if (!ms || ms.locale !== defaultLocale)
|
|
2691
|
+
const ms = p.multiSource;
|
|
2692
|
+
if (!ms?.locale || !p.slug.startsWith(`${ms.locale}/`))
|
|
2593
2693
|
continue;
|
|
2594
|
-
|
|
2595
|
-
// the locale-neutral tail. Defensive: if for some reason the
|
|
2596
|
-
// slug doesn't start with the locale (e.g. unprefixed
|
|
2597
|
-
// baseline page in a half-active site), skip.
|
|
2598
|
-
const localePrefix = `${defaultLocale}/`;
|
|
2599
|
-
if (!defaultPage.slug.startsWith(localePrefix))
|
|
2694
|
+
if (p.fragment === true || p.frontmatter?._fragment === true || isExcludedSlug(p.slug))
|
|
2600
2695
|
continue;
|
|
2601
|
-
|
|
2602
|
-
|
|
2603
|
-
|
|
2604
|
-
|
|
2605
|
-
|
|
2606
|
-
|
|
2607
|
-
|
|
2608
|
-
|
|
2609
|
-
|
|
2610
|
-
|
|
2611
|
-
|
|
2612
|
-
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
|
|
2696
|
+
if (typeof p.redirect === "string" && p.redirect.trim() && ms.locale === defaultLocale)
|
|
2697
|
+
continue;
|
|
2698
|
+
const tail = p.slug.slice(ms.locale.length + 1);
|
|
2699
|
+
const group = groupOf(ms);
|
|
2700
|
+
const k = `${group}\0${ms.locale}`;
|
|
2701
|
+
if (!units.has(k))
|
|
2702
|
+
units.set(k, { locale: ms.locale, version: group, pages: [] });
|
|
2703
|
+
units.get(k).pages.push(tail);
|
|
2704
|
+
if (ms.locale === defaultLocale)
|
|
2705
|
+
defaultPages.set(`${group}\0${tail}`, p);
|
|
2706
|
+
}
|
|
2707
|
+
let emitted = 0;
|
|
2708
|
+
for (const m of missingTranslations([...units.values()], defaultLocale)) {
|
|
2709
|
+
const defaultPage = defaultPages.get(`${m.version}\0${m.page}`);
|
|
2710
|
+
const targetSlug = `${m.locale}/${m.page}`;
|
|
2711
|
+
// The page's URL: an `index` slug is its folder (`/en/v3/index` is not served).
|
|
2712
|
+
const defaultUrl = `${combined}/${defaultPage.slug.replace(/(^|\/)index$/, "")}`;
|
|
2713
|
+
const filePath = join(outputDir, "src", "pages", ...baseSegments, ...targetSlug.split("/"));
|
|
2714
|
+
mkdirSync(dirname(filePath), { recursive: true });
|
|
2715
|
+
writeFileSync(`${filePath}.astro`, buildMissingTranslationStub(defaultUrl, m.locale, defaultLocale));
|
|
2716
|
+
emitted++;
|
|
2619
2717
|
}
|
|
2620
2718
|
if (emitted > 0) {
|
|
2621
2719
|
console.log(`Emitted ${emitted} missing-translation stub(s) → redirect to default locale.`);
|
|
@@ -2654,7 +2752,7 @@ export function emitAgentReadinessFiles(pages, outputNav, outputDir, siteName, o
|
|
|
2654
2752
|
mkdirSync(join(outputDir, "public"), { recursive: true });
|
|
2655
2753
|
// _headers Link header points at the per-mount llms.txt at
|
|
2656
2754
|
// <combined>/llms.txt — the URL agents would actually fetch.
|
|
2657
|
-
writeFileSync(join(outputDir, "public", "_headers"), buildHeadersFile(combinedPrefix(options)));
|
|
2755
|
+
writeFileSync(join(outputDir, "public", "_headers"), buildHeadersFile(withBasePath(combinedPrefix(options), "/llms.txt")));
|
|
2658
2756
|
}
|
|
2659
2757
|
// Sitemap — emitted by Dogsbay (not @astrojs/sitemap) into
|
|
2660
2758
|
// public/<basePath>/sitemap-{index,0}.xml so multi-mount deploys
|
|
@@ -2695,7 +2793,9 @@ export function emitAgentReadinessFiles(pages, outputNav, outputDir, siteName, o
|
|
|
2695
2793
|
const mdMirrorOn = options.mdMirror !== false && !staticDeploy;
|
|
2696
2794
|
const knownVersions = pageVersions(pages);
|
|
2697
2795
|
const knownLocales = pageLocales(pages);
|
|
2698
|
-
|
|
2796
|
+
// With the default version at the root (`path: ""`), a versionless URL IS
|
|
2797
|
+
// one of its pages: there is nothing to redirect.
|
|
2798
|
+
const versionRedirectOn = options.defaultVersion !== undefined && knownVersions.length >= 2 && !rootVersionOf(options);
|
|
2699
2799
|
const localeRedirectOn = options.defaultLocale !== undefined && knownLocales.length >= 2;
|
|
2700
2800
|
const axisRedirectOn = versionRedirectOn || localeRedirectOn;
|
|
2701
2801
|
const middlewarePath = join(outputDir, "src", "middleware.ts");
|
|
@@ -2807,104 +2907,25 @@ function serializePageMd(page) {
|
|
|
2807
2907
|
* opt-in.
|
|
2808
2908
|
*/
|
|
2809
2909
|
function buildRobotsTxt(options, hasSiteUrl) {
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
//
|
|
2815
|
-
// index under <basePath>/, so robots.txt must point there too.
|
|
2816
|
-
// (Multi-mount deploys end up with one robots.txt per site at
|
|
2817
|
-
// their respective hosts / paths; each correctly references its
|
|
2818
|
-
// own mount's sitemap-index.)
|
|
2819
|
-
// Sitemap URL = origin + combined + /sitemap-index.xml. Use the
|
|
2820
|
-
// origin (no path) from site.url and the combined prefix (urlBase
|
|
2821
|
-
// + basePath); siteUrl could itself include a path component when
|
|
2822
|
-
// hosting on a subpath (GH Pages project page), so we strip it
|
|
2823
|
-
// here to avoid double-counting.
|
|
2910
|
+
// Per-mount URLs: each Dogsbay site emits its sitemap index and
|
|
2911
|
+
// llms.txt under <basePath>/, so robots.txt points there. The origin
|
|
2912
|
+
// is taken without its path (site.url may carry one when hosting on
|
|
2913
|
+
// a subpath) and the combined prefix (urlBase + basePath) added, so
|
|
2914
|
+
// the path is not counted twice.
|
|
2824
2915
|
const { origin } = parseSiteUrl(options.siteUrl);
|
|
2825
2916
|
const combined = combinedPrefix(options);
|
|
2826
|
-
const
|
|
2827
|
-
|
|
2828
|
-
:
|
|
2829
|
-
|
|
2830
|
-
|
|
2831
|
-
|
|
2832
|
-
|
|
2833
|
-
|
|
2834
|
-
|
|
2835
|
-
|
|
2836
|
-
|
|
2837
|
-
|
|
2838
|
-
// unknown directives, but Lighthouse's validator reports this one as
|
|
2839
|
-
// `Unknown directive` and fails the "robots.txt is valid" audit —
|
|
2840
|
-
// measured: SEO 100 -> 92 on every page. Nothing consumes the line
|
|
2841
|
-
// either; `Llms-Txt:` is not part of the llms.txt proposal, and
|
|
2842
|
-
// agents find the file at its well-known path. A measured penalty
|
|
2843
|
-
// for a speculative benefit is not a sensible default.
|
|
2844
|
-
//
|
|
2845
|
-
// `llms.txt` itself is still emitted; this controls only the pointer.
|
|
2846
|
-
const llmsTxt = options.llmsTxtDirective === true &&
|
|
2847
|
-
options.llmsTxt !== false &&
|
|
2848
|
-
hasSiteUrl &&
|
|
2849
|
-
origin
|
|
2850
|
-
? `Llms-Txt: ${origin}${withBasePath(combined, "/llms.txt")}\n`
|
|
2851
|
-
: "";
|
|
2852
|
-
return `${contentSignalPreamble(options)}User-agent: *\nAllow: /\n${contentSignal}${sitemap}${llmsTxt}`;
|
|
2853
|
-
}
|
|
2854
|
-
/**
|
|
2855
|
-
* The Article 4 reservation-of-rights preamble, as robots.txt comments.
|
|
2856
|
-
*
|
|
2857
|
-
* Opt-in via `agent.contentSignal.preamble`. Default off: it is 25
|
|
2858
|
-
* lines of legal text on every site, and it asserts a legal position
|
|
2859
|
-
* that is the operator's to take, not ours to take for them.
|
|
2860
|
-
*
|
|
2861
|
-
* Why it is worth having at all — `Content-Signal: ai-train=no` on its
|
|
2862
|
-
* own is a preference. Article 4(3) of EU Directive 2019/790 makes the
|
|
2863
|
-
* text-and-data-mining exception conditional on rights not having been
|
|
2864
|
-
* "expressly reserved in an appropriate manner", including
|
|
2865
|
-
* machine-readable means. The preamble is what turns the signal into
|
|
2866
|
-
* that express reservation, which is the difference between a request
|
|
2867
|
-
* and a reservation with legal effect in the EU.
|
|
2868
|
-
*
|
|
2869
|
-
* The wording is Cloudflare's Content Signals Policy text, which is
|
|
2870
|
-
* published under CC0 precisely so it can be reproduced. It is
|
|
2871
|
-
* reproduced rather than linked because a reservation has to be present
|
|
2872
|
-
* where the machine reads it.
|
|
2873
|
-
*
|
|
2874
|
-
* Kept BYTE-IDENTICAL to the published policy on purpose. It is a legal
|
|
2875
|
-
* instrument, not prose we own: reword it and a site is asserting
|
|
2876
|
-
* something subtly different from what its operator believes it is
|
|
2877
|
-
* asserting. `tests/robots-preamble.test.ts` pins the text.
|
|
2878
|
-
*/
|
|
2879
|
-
function contentSignalPreamble(options) {
|
|
2880
|
-
if (options.contentSignalPreamble !== true)
|
|
2881
|
-
return "";
|
|
2882
|
-
return `# As a condition of accessing this website, you agree to abide by the following
|
|
2883
|
-
# content signals:
|
|
2884
|
-
|
|
2885
|
-
# (a) If a Content-Signal = yes, you may collect content for the corresponding
|
|
2886
|
-
# use.
|
|
2887
|
-
# (b) If a Content-Signal = no, you may not collect content for the
|
|
2888
|
-
# corresponding use.
|
|
2889
|
-
# (c) If the website operator does not include a Content-Signal for a
|
|
2890
|
-
# corresponding use, the website operator neither grants nor restricts
|
|
2891
|
-
# permission via Content-Signal with respect to the corresponding use.
|
|
2892
|
-
|
|
2893
|
-
# The content signals and their meanings are:
|
|
2894
|
-
|
|
2895
|
-
# search: building a search index and providing search results (e.g., returning
|
|
2896
|
-
# hyperlinks and short excerpts from your website's contents). Search does not
|
|
2897
|
-
# include providing AI-generated search summaries.
|
|
2898
|
-
# ai-input: inputting content into one or more AI models (e.g., retrieval
|
|
2899
|
-
# augmented generation, grounding, or other real-time taking of content for
|
|
2900
|
-
# generative AI search answers).
|
|
2901
|
-
# ai-train: training or fine-tuning AI models.
|
|
2902
|
-
|
|
2903
|
-
# ANY RESTRICTIONS EXPRESSED VIA CONTENT SIGNALS ARE EXPRESS RESERVATIONS OF
|
|
2904
|
-
# RIGHTS UNDER ARTICLE 4 OF THE EUROPEAN UNION DIRECTIVE 2019/790 ON COPYRIGHT
|
|
2905
|
-
# AND RELATED RIGHTS IN THE DIGITAL SINGLE MARKET.
|
|
2906
|
-
|
|
2907
|
-
`;
|
|
2917
|
+
const absolute = hasSiteUrl && origin;
|
|
2918
|
+
return buildRobotsTxtFile({
|
|
2919
|
+
signals: { search: options.aiSearch, aiInput: options.aiInput, aiTrain: options.aiTrain },
|
|
2920
|
+
preamble: options.contentSignalPreamble === true,
|
|
2921
|
+
sitemapUrl: absolute ? `${origin}${withBasePath(combined, "/sitemap-index.xml")}` : undefined,
|
|
2922
|
+
// `Llms-Txt:` is opt-in (`agent.llmsTxtDirective`): Lighthouse reports
|
|
2923
|
+
// it as an unknown directive and fails "robots.txt is valid"
|
|
2924
|
+
// (measured: SEO 100 -> 92 on every page), and nothing consumes it.
|
|
2925
|
+
llmsTxtUrl: absolute && options.llmsTxtDirective === true && options.llmsTxt !== false
|
|
2926
|
+
? `${origin}${withBasePath(combined, "/llms.txt")}`
|
|
2927
|
+
: undefined,
|
|
2928
|
+
});
|
|
2908
2929
|
}
|
|
2909
2930
|
/**
|
|
2910
2931
|
* Build the contents of `public/_headers` (Cloudflare Pages / Workers
|
|
@@ -2947,14 +2968,6 @@ export function faviconHref(outputDir, urlBase) {
|
|
|
2947
2968
|
}
|
|
2948
2969
|
return false;
|
|
2949
2970
|
}
|
|
2950
|
-
function buildHeadersFile(basePath) {
|
|
2951
|
-
const llmsHref = withBasePath(basePath, "/llms.txt");
|
|
2952
|
-
return [
|
|
2953
|
-
"/*",
|
|
2954
|
-
` Link: <${llmsHref}>; rel="describedby"; type="text/plain"`,
|
|
2955
|
-
"",
|
|
2956
|
-
].join("\n");
|
|
2957
|
-
}
|
|
2958
2971
|
function buildMiddlewareSource(config) {
|
|
2959
2972
|
const lines = [
|
|
2960
2973
|
"// AUTO-GENERATED by `dogsbay site build` — do not edit.",
|
|
@@ -3095,7 +3108,13 @@ function emitLlmsTxtFiles(outputDir, siteName, options, nav, pages) {
|
|
|
3095
3108
|
continue;
|
|
3096
3109
|
let relDir;
|
|
3097
3110
|
if (combinedSegs && dir === combinedSegs) {
|
|
3098
|
-
|
|
3111
|
+
// The section's pages share no directory below the mount — they
|
|
3112
|
+
// are spread across several, so the common prefix collapsed to
|
|
3113
|
+
// the mount itself. There is no directory that belongs to this
|
|
3114
|
+
// section, and writing here would land on `<mount>/llms.txt`:
|
|
3115
|
+
// the site index, which was written just above. Skip it. The
|
|
3116
|
+
// index already lists the section, so nothing is lost.
|
|
3117
|
+
continue;
|
|
3099
3118
|
}
|
|
3100
3119
|
else if (combinedSegs && dir.startsWith(`${combinedSegs}/`)) {
|
|
3101
3120
|
relDir = dir.slice(combinedSegs.length + 1);
|
|
@@ -3106,9 +3125,7 @@ function emitLlmsTxtFiles(outputDir, siteName, options, nav, pages) {
|
|
|
3106
3125
|
// back to the raw value rather than rooting at /.
|
|
3107
3126
|
relDir = dir;
|
|
3108
3127
|
}
|
|
3109
|
-
const sectionPath = relDir
|
|
3110
|
-
? join(mountDir, relDir, "llms.txt")
|
|
3111
|
-
: join(mountDir, "llms.txt");
|
|
3128
|
+
const sectionPath = join(mountDir, relDir, "llms.txt");
|
|
3112
3129
|
mkdirSync(dirname(sectionPath), { recursive: true });
|
|
3113
3130
|
writeFileSync(sectionPath, buildSectionLlmsTxt(siteConfig, group, pages, { hrefPrefix }));
|
|
3114
3131
|
}
|
|
@@ -3129,7 +3146,7 @@ function emitSitemapFiles(outputDir, options, pages) {
|
|
|
3129
3146
|
// Filesystem path uses basePath (sitemap files live in
|
|
3130
3147
|
// public/<basePath>/sitemap-*.xml). The URL prefix encoded into
|
|
3131
3148
|
// each <loc> uses combined so the absolute URLs resolve under the
|
|
3132
|
-
// host's served subpath.
|
|
3149
|
+
// host's served subpath. buildSitemapFiles strips path off siteUrl
|
|
3133
3150
|
// internally, so passing siteUrl + combined as basePath gives
|
|
3134
3151
|
// origin + combined as the final URL.
|
|
3135
3152
|
const basePath = normalizeBasePath(options.basePath);
|
|
@@ -3137,14 +3154,28 @@ function emitSitemapFiles(outputDir, options, pages) {
|
|
|
3137
3154
|
const baseSegments = basePathSegments(basePath);
|
|
3138
3155
|
const mountDir = join(outputDir, "public", ...baseSegments);
|
|
3139
3156
|
mkdirSync(mountDir, { recursive: true });
|
|
3140
|
-
|
|
3157
|
+
// An older version's page whose canonical is the default version's, or a noindex version's page, is left out.
|
|
3158
|
+
const excluded = makeSlugExcluder(options.excludeFromRoutes);
|
|
3159
|
+
const plan = canonicalPlan(pages, {
|
|
3160
|
+
isEmitted: (p) => !(p.fragment === true || p.frontmatter?._fragment === true) && !excluded(p.slug),
|
|
3161
|
+
siteUrl: options.siteUrl,
|
|
3162
|
+
basePath: combined,
|
|
3163
|
+
versions: options.versions,
|
|
3164
|
+
defaultVersion: options.defaultVersion,
|
|
3165
|
+
redirects: options.redirects,
|
|
3166
|
+
});
|
|
3167
|
+
// More than one file past 50,000 URLs, the protocol's limit per file.
|
|
3168
|
+
const files = buildSitemapFiles(pages.filter((p) => plan.get(p.slug)?.sitemap !== false), {
|
|
3141
3169
|
siteUrl: options.siteUrl,
|
|
3142
3170
|
basePath: combined,
|
|
3143
3171
|
siteNoindex: options.noindex === true,
|
|
3144
|
-
})
|
|
3172
|
+
});
|
|
3173
|
+
for (const [name, xml] of files)
|
|
3174
|
+
writeFileSync(join(mountDir, name), xml);
|
|
3145
3175
|
writeFileSync(join(mountDir, "sitemap-index.xml"), buildSitemapIndex({
|
|
3146
3176
|
siteUrl: options.siteUrl,
|
|
3147
3177
|
basePath: combined,
|
|
3178
|
+
sitemapCount: files.length,
|
|
3148
3179
|
extraSitemaps: (options.aggregates ?? [])
|
|
3149
3180
|
.map((a) => a.sitemap)
|
|
3150
3181
|
.filter((u) => !!u),
|
|
@@ -3232,29 +3263,50 @@ function axisSubtreeRoots(pages) {
|
|
|
3232
3263
|
}
|
|
3233
3264
|
return roots;
|
|
3234
3265
|
}
|
|
3266
|
+
/**
|
|
3267
|
+
* The version served at the site root: the `defaultVersion` when its
|
|
3268
|
+
* `versions` entry has `path: ""` (the CLI's config load allows it on no
|
|
3269
|
+
* other version). Its pages have no version segment.
|
|
3270
|
+
*/
|
|
3271
|
+
export function rootVersionOf(options) {
|
|
3272
|
+
const def = options.defaultVersion;
|
|
3273
|
+
if (!def)
|
|
3274
|
+
return undefined;
|
|
3275
|
+
return options.versions?.find((v) => v.id === def)?.path === "" ? def : undefined;
|
|
3276
|
+
}
|
|
3235
3277
|
/**
|
|
3236
3278
|
* First nav href (in nav order) living under any of `prefixes`, which
|
|
3237
3279
|
* are URL paths without a trailing slash. Used to pick a redirect
|
|
3238
3280
|
* target for a subtree root that has no page of its own.
|
|
3239
3281
|
*/
|
|
3240
|
-
function findFirstNavHrefUnder(items, prefixes
|
|
3282
|
+
function findFirstNavHrefUnder(items, prefixes,
|
|
3283
|
+
/** Deeper subtrees to skip (another version's, nested in the root version's). */
|
|
3284
|
+
exclude = []) {
|
|
3241
3285
|
for (const item of items) {
|
|
3242
3286
|
const href = item.href;
|
|
3243
|
-
if (href &&
|
|
3287
|
+
if (href &&
|
|
3288
|
+
prefixes.some((prefix) => href.startsWith(`${prefix}/`)) &&
|
|
3289
|
+
!exclude.some((x) => href === x || href.startsWith(`${x}/`))) {
|
|
3244
3290
|
return href;
|
|
3245
3291
|
}
|
|
3246
3292
|
if (item.children) {
|
|
3247
|
-
const found = findFirstNavHrefUnder(item.children, prefixes);
|
|
3293
|
+
const found = findFirstNavHrefUnder(item.children, prefixes, exclude);
|
|
3248
3294
|
if (found)
|
|
3249
3295
|
return found;
|
|
3250
3296
|
}
|
|
3251
3297
|
}
|
|
3252
3298
|
return undefined;
|
|
3253
3299
|
}
|
|
3254
|
-
|
|
3300
|
+
/**
|
|
3301
|
+
* Copy the UI component folders the serializer can emit into the site's
|
|
3302
|
+
* `src/components/ui/`, skipping any that exist (project-owned, maybe
|
|
3303
|
+
* edited). Returns the names added. `site build` calls it so a component
|
|
3304
|
+
* introduced in a release (Kbd, FileTree) reaches sites scaffolded earlier.
|
|
3305
|
+
*/
|
|
3306
|
+
export function copyComponents(outputDir) {
|
|
3255
3307
|
const componentsSource = resolveComponentsSource();
|
|
3256
3308
|
if (!componentsSource)
|
|
3257
|
-
return;
|
|
3309
|
+
return [];
|
|
3258
3310
|
// Components copied into the generated project. These are the ones
|
|
3259
3311
|
// format-astro's serializer emits imports for, plus a few used by the
|
|
3260
3312
|
// project shell (DocsLayout, pagination, theme-toggle).
|
|
@@ -3281,14 +3333,19 @@ function copyComponents(outputDir) {
|
|
|
3281
3333
|
// inline `:icon[name]` directive. Without this entry every page
|
|
3282
3334
|
// emitting the icon import 500s with "module not found".
|
|
3283
3335
|
"icon",
|
|
3336
|
+
// Inline :kbd and the :::files tree.
|
|
3337
|
+
"kbd", "files",
|
|
3284
3338
|
];
|
|
3339
|
+
const added = [];
|
|
3285
3340
|
for (const name of needed) {
|
|
3286
3341
|
const src = join(componentsSource, name);
|
|
3287
3342
|
const dest = join(outputDir, "src", "components", "ui", name);
|
|
3288
3343
|
if (existsSync(src) && !existsSync(dest)) {
|
|
3289
3344
|
cpSync(src, dest, { recursive: true });
|
|
3345
|
+
added.push(name);
|
|
3290
3346
|
}
|
|
3291
3347
|
}
|
|
3348
|
+
return added;
|
|
3292
3349
|
}
|
|
3293
3350
|
/** Media files worth carrying from a `mediaOnly` asset mount. */
|
|
3294
3351
|
const MOUNT_MEDIA_EXTS = new Set([
|
|
@@ -3381,630 +3438,8 @@ function copyAssets(sourceDir, outputDir, imageOptimization, urlPrefix) {
|
|
|
3381
3438
|
}
|
|
3382
3439
|
catch { /* source may not exist */ }
|
|
3383
3440
|
}
|
|
3384
|
-
// ── CSS generation (ported from import-mkdocs.ts) ───────
|
|
3385
|
-
/**
|
|
3386
|
-
* Build the `@source inline("...")` directive that pins the
|
|
3387
|
-
* grid-tone palette into the generated stylesheet.
|
|
3388
|
-
*
|
|
3389
|
-
* Why we need it: tone classes like `bg-primary/10` only appear in
|
|
3390
|
-
* `.astro` pages emitted by Dogsbay's grid-item serializer. When
|
|
3391
|
-
* Tailwind's content scanner doesn't pick them up — because the
|
|
3392
|
-
* page lives outside the default scan globs, or because a class
|
|
3393
|
-
* is composed at the boundary of an interpolation — they get
|
|
3394
|
-
* purged. Result observed in dogsbay-docs-markdown audit: half
|
|
3395
|
-
* the grid demo cells render with no background. Pinning forces
|
|
3396
|
-
* generation regardless of scanner reach.
|
|
3397
|
-
*
|
|
3398
|
-
* Single source of truth: derived from TONE_CLASSES so any new
|
|
3399
|
-
* tone added to the palette is automatically safelisted.
|
|
3400
|
-
*/
|
|
3401
|
-
function buildToneSafelist() {
|
|
3402
|
-
const seen = new Set();
|
|
3403
|
-
for (const classes of Object.values(TONE_CLASSES)) {
|
|
3404
|
-
for (const cls of classes.split(/\s+/)) {
|
|
3405
|
-
if (cls)
|
|
3406
|
-
seen.add(cls);
|
|
3407
|
-
}
|
|
3408
|
-
}
|
|
3409
|
-
return [...seen].sort().join(" ");
|
|
3410
|
-
}
|
|
3411
|
-
function generateGlobalCss() {
|
|
3412
|
-
return `@import "tailwindcss";
|
|
3413
|
-
@import "./theme.css";
|
|
3414
|
-
|
|
3415
|
-
/* Custom elements have no user-agent style, so an unstyled one is
|
|
3416
|
-
display:inline — on which w-full does nothing and a border paints
|
|
3417
|
-
as detached fragments rather than around the content. @dogsbay/elements
|
|
3418
|
-
injects these same defaults at runtime, which is what reaches EXISTING
|
|
3419
|
-
sites (global.css is scaffolded once and never rewritten). Having them
|
|
3420
|
-
here too means a new site is correct at CSS time, with no flash before
|
|
3421
|
-
the module script runs.
|
|
3422
|
-
|
|
3423
|
-
:where() keeps specificity at zero, so any utility class wins. */
|
|
3424
|
-
:where(db-tabs, db-accordion, db-collapsible, db-card, db-steps, db-code-block) {
|
|
3425
|
-
display: block;
|
|
3426
|
-
}
|
|
3427
|
-
:where(db-link-button) {
|
|
3428
|
-
display: inline-block;
|
|
3429
|
-
}
|
|
3430
|
-
|
|
3431
|
-
/* Scan @dogsbay packages for Tailwind classes */
|
|
3432
|
-
@source "../../node_modules/@dogsbay/ui/src";
|
|
3433
|
-
@source "../../node_modules/@dogsbay/docs-layout/src";
|
|
3434
|
-
|
|
3435
|
-
/* Pin the grid-tone palette. These classes are emitted into
|
|
3436
|
-
markdown-generated .astro pages by the grid-item serializer
|
|
3437
|
-
(TONE_CLASSES in @dogsbay/format-astro). Without inlining,
|
|
3438
|
-
opacity-modified utilities like bg-primary/10 get purged when
|
|
3439
|
-
Tailwind doesn't see them in the scanned globs, leaving grid
|
|
3440
|
-
demo cells with no visible background. */
|
|
3441
|
-
@source inline("${buildToneSafelist()}");
|
|
3442
|
-
|
|
3443
|
-
/* Prose typography for rendered content.
|
|
3444
|
-
Every element rule is guarded with :not(.dba-api *): API-reference
|
|
3445
|
-
regions (ApiLayout root carries the dba-api marker) compose their own
|
|
3446
|
-
typography from utility classes, and unguarded .docs-prose rules would
|
|
3447
|
-
out-specify them. The guard lets mixed prose+endpoint pages (MDX
|
|
3448
|
-
imports) keep full typography outside the cards. */
|
|
3449
|
-
.docs-prose {
|
|
3450
|
-
line-height: 1.7;
|
|
3451
|
-
|
|
3452
|
-
& h1:not(.dba-api *) { font-family: var(--font-heading); font-size: 2rem; font-weight: 700; margin-top: 0; margin-bottom: 1rem; line-height: 1.2; letter-spacing: -0.025em; }
|
|
3453
|
-
& h2:not(.dba-api *) { font-family: var(--font-heading); font-size: 1.5rem; font-weight: 600; margin-top: 2.5rem; margin-bottom: 0.75rem; line-height: 1.3; border-bottom: 1px solid var(--border); padding-bottom: 0.5rem; letter-spacing: -0.015em; }
|
|
3454
|
-
& h3:not(.dba-api *) { font-family: var(--font-heading); font-size: 1.25rem; font-weight: 600; margin-top: 2rem; margin-bottom: 0.5rem; line-height: 1.4; }
|
|
3455
|
-
& h4:not(.dba-api *) { font-family: var(--font-heading); font-size: 1.1rem; font-weight: 600; margin-top: 1.5rem; margin-bottom: 0.5rem; }
|
|
3456
|
-
|
|
3457
|
-
& p:not(.dba-api *) { margin-top: 0.75rem; margin-bottom: 0.75rem; }
|
|
3458
|
-
|
|
3459
|
-
/* The :not([data-variant]) guard exempts anchors that ARE components. A
|
|
3460
|
-
link-button renders as an <a> carrying bg-primary + text-primary-foreground
|
|
3461
|
-
and a data-variant attribute, and this rule repainted its text with
|
|
3462
|
-
var(--primary) — primary on primary: an unreadable solid block in light
|
|
3463
|
-
mode, a blank one in dark. The secondary variant survived only because its
|
|
3464
|
-
background is light enough to read primary-coloured text, which is why it
|
|
3465
|
-
looked like a colour bug in one button rather than a cascade bug in both.
|
|
3466
|
-
(No backticks in this comment: the whole stylesheet is a template
|
|
3467
|
-
literal.) */
|
|
3468
|
-
& a:not(.dba-api *):not([data-variant]) { color: var(--primary); text-decoration: underline; text-underline-offset: 2px; }
|
|
3469
|
-
& a:hover:not(.dba-api *):not([data-variant]) { opacity: 0.8; }
|
|
3470
|
-
|
|
3471
|
-
& strong:not(.dba-api *) { font-weight: 600; }
|
|
3472
|
-
& code:not(.dba-api *) { font-size: 0.875em; padding: 0.15em 0.35em; border-radius: 0.25rem; background: var(--muted); font-family: var(--font-code, ui-monospace, monospace); }
|
|
3473
|
-
& pre code:not(.dba-api *) { padding: 0; background: none; font-size: 1em; }
|
|
3474
|
-
|
|
3475
|
-
/* Spacing between consecutive block elements (code blocks, alerts, etc.) */
|
|
3476
|
-
& > * + * { margin-top: 0.75rem; }
|
|
3477
|
-
& li > * + *:not(.dba-api *) { margin-top: 0.75rem; }
|
|
3478
|
-
|
|
3479
|
-
& ul:not(.dba-api *) { list-style: disc; padding-left: 1.5rem; margin: 0.75rem 0; }
|
|
3480
|
-
& ol:not(.dba-api *) { list-style: decimal; padding-left: 1.5rem; margin: 0.75rem 0; }
|
|
3481
|
-
& li:not(.dba-api *) { margin: 0.25rem 0; }
|
|
3482
|
-
& li > ul:not(.dba-api *), & li > ol:not(.dba-api *) { margin: 0.25rem 0; }
|
|
3483
|
-
|
|
3484
|
-
& blockquote:not(.dba-api *) { border-left: 4px solid var(--border); padding-left: 1rem; color: var(--muted-foreground); font-style: italic; margin: 1rem 0; }
|
|
3485
|
-
|
|
3486
|
-
& hr:not(.dba-api *) { border: none; border-top: 1px solid var(--border); margin: 2rem 0; }
|
|
3487
|
-
|
|
3488
|
-
& table:not(.dba-api *) { width: 100%; border-collapse: collapse; margin: 1rem 0; font-size: 0.875rem; }
|
|
3489
|
-
& th:not(.dba-api *) { text-align: left; vertical-align: bottom; font-weight: 600; padding: 0.5rem; border-bottom: 2px solid var(--border); }
|
|
3490
|
-
& td:not(.dba-api *) { padding: 0.5rem; vertical-align: top; border-bottom: 1px solid var(--border); }
|
|
3491
|
-
|
|
3492
|
-
/* Tailwind Preflight sets display: block on every img, which turns an
|
|
3493
|
-
INLINE icon into its own line and splits the sentence around it —
|
|
3494
|
-
AsciiDoc UI macros render mid-step and were breaking every procedure
|
|
3495
|
-
that used one. inline-block restores the flow and leaves a standalone
|
|
3496
|
-
image (the only child of its paragraph) looking exactly as before. */
|
|
3497
|
-
& img:not(.dba-api *) { display: inline-block; max-width: 100%; border-radius: 0.5rem; }
|
|
3498
|
-
/* A standalone image — the only child of its paragraph — keeps block
|
|
3499
|
-
layout, so the vertical rhythm and the absence of baseline descender
|
|
3500
|
-
space are exactly what they were before inline icons were fixed. */
|
|
3501
|
-
& p > img:only-child:not(.dba-api *) { display: block; }
|
|
3502
|
-
|
|
3503
|
-
& .heading-anchor { text-decoration: none; opacity: 0; margin-right: 0.25rem; transition: opacity 0.2s; }
|
|
3504
|
-
& h1:hover .heading-anchor, & h2:hover .heading-anchor, & h3:hover .heading-anchor, & h4:hover .heading-anchor { opacity: 0.4; }
|
|
3505
|
-
|
|
3506
|
-
& details:not(.dba-api *) { border: 1px solid var(--border); border-radius: 0.5rem; padding: 1rem; margin: 1rem 0; }
|
|
3507
|
-
& summary:not(.dba-api *) { cursor: pointer; font-weight: 600; }
|
|
3508
|
-
|
|
3509
|
-
/* mark is emitted by the highlight directive AND can arrive as raw HTML
|
|
3510
|
-
from an importer. Without a rule here it renders as the browser default
|
|
3511
|
-
yellow, which clashes in light mode and is unreadable on a dark page
|
|
3512
|
-
(nothing declares color-scheme). Token-based so it follows the theme. */
|
|
3513
|
-
& mark:not(.dba-api *) { background: var(--dsb-mark-bg, rgb(254 240 138 / 0.6)); color: inherit; border-radius: 0.125rem; padding: 0 0.125rem; }
|
|
3514
|
-
&:where(.dark *) mark:not(.dba-api *), .dark & mark:not(.dba-api *) { background: var(--dsb-mark-bg-dark, rgb(234 179 8 / 0.3)); }
|
|
3515
|
-
|
|
3516
|
-
& dl:not(.dba-api *) { margin: 1rem 0; }
|
|
3517
|
-
& dt:not(.dba-api *) { font-weight: 600; margin-top: 0.75rem; }
|
|
3518
|
-
& dd:not(.dba-api *) { margin-left: 1.5rem; color: var(--muted-foreground); }
|
|
3519
|
-
|
|
3520
|
-
/* In-cell admonitions. Inside a raw HTML table cell nothing
|
|
3521
|
-
markdown-native works, so the AsciiDoc engine emits a neutral
|
|
3522
|
-
dl.db-admonition shape (DD-010) and this rule renders it as a compact
|
|
3523
|
-
callout box — same contract the Obsidian plugin styles. Per-type
|
|
3524
|
-
accent from theme tokens; overrides the generic dl/dt/dd rules above. */
|
|
3525
|
-
& dl.db-admonition {
|
|
3526
|
-
margin: 0.75rem 0;
|
|
3527
|
-
padding: 0.5rem 0.75rem;
|
|
3528
|
-
border-left: 3px solid var(--info);
|
|
3529
|
-
border-radius: 0.25rem;
|
|
3530
|
-
background: color-mix(in oklab, var(--info) 8%, transparent);
|
|
3531
|
-
}
|
|
3532
|
-
/* The LABEL blends the accent toward the foreground; the border and the
|
|
3533
|
-
tint keep the accent at full strength, so the callout still reads as
|
|
3534
|
-
its colour while the text stays legible.
|
|
3535
|
-
|
|
3536
|
-
Measured from painted pixels on a real page, light mode, 13px/600 —
|
|
3537
|
-
the accent used raw is a genuine AA failure, not one of axe's oklch
|
|
3538
|
-
misreads:
|
|
3539
|
-
|
|
3540
|
-
--info raw 2.18:1 mixed 5.66:1
|
|
3541
|
-
--warning raw 2.25:1 mixed 5.74:1
|
|
3542
|
-
--destructive raw 5.97:1 mixed 10.98:1
|
|
3543
|
-
|
|
3544
|
-
Mixed with var(--foreground), not black, on purpose. Black fixes
|
|
3545
|
-
light and BREAKS dark: 70% black measures 5.21:1 light but 3.25:1
|
|
3546
|
-
dark, because the tint it sits on is dark too. --foreground is
|
|
3547
|
-
near-black in light and near-white in dark, so one rule serves both
|
|
3548
|
-
(dark measures 10.96 / 10.87 / 9.79). A fixed colour used in both
|
|
3549
|
-
themes is a bug waiting. */
|
|
3550
|
-
& dl.db-admonition > dt {
|
|
3551
|
-
margin-top: 0;
|
|
3552
|
-
font-weight: 600;
|
|
3553
|
-
font-size: 0.8125rem;
|
|
3554
|
-
text-transform: uppercase;
|
|
3555
|
-
letter-spacing: 0.03em;
|
|
3556
|
-
color: color-mix(in oklab, var(--info) 60%, var(--foreground));
|
|
3557
|
-
}
|
|
3558
|
-
& dl.db-admonition > dd { margin-left: 0; margin-top: 0.25rem; color: var(--foreground); }
|
|
3559
|
-
& dl.db-admonition-warning, & dl.db-admonition-caution {
|
|
3560
|
-
border-left-color: var(--warning);
|
|
3561
|
-
background: color-mix(in oklab, var(--warning) 8%, transparent);
|
|
3562
|
-
}
|
|
3563
|
-
& dl.db-admonition-warning > dt, & dl.db-admonition-caution > dt { color: color-mix(in oklab, var(--warning) 60%, var(--foreground)); }
|
|
3564
|
-
& dl.db-admonition-important {
|
|
3565
|
-
border-left-color: var(--destructive);
|
|
3566
|
-
background: color-mix(in oklab, var(--destructive) 8%, transparent);
|
|
3567
|
-
}
|
|
3568
|
-
& dl.db-admonition-important > dt { color: color-mix(in oklab, var(--destructive) 60%, var(--foreground)); }
|
|
3569
|
-
|
|
3570
|
-
/* Granularity book-view affordances (plans/granularity-views.md):
|
|
3571
|
-
"Read as single page →" on topics, "Open as page ↗" on book sections.
|
|
3572
|
-
Deliberately subtle/muted — the book has one per section. */
|
|
3573
|
-
& .dsb-read-single-page {
|
|
3574
|
-
display: inline-block;
|
|
3575
|
-
margin: 0 0 1.25rem;
|
|
3576
|
-
font-size: 0.8125rem;
|
|
3577
|
-
color: var(--muted-foreground);
|
|
3578
|
-
text-decoration: none;
|
|
3579
|
-
}
|
|
3580
|
-
& .dsb-read-single-page:hover { color: var(--foreground); text-decoration: underline; }
|
|
3581
|
-
& .dsb-open-as-page { margin: -0.25rem 0 0.5rem; font-size: 0.75rem; }
|
|
3582
|
-
& .dsb-open-as-page a { color: var(--muted-foreground); text-decoration: none; }
|
|
3583
|
-
& .dsb-open-as-page a:hover { color: var(--foreground); text-decoration: underline; }
|
|
3584
|
-
|
|
3585
|
-
/* AsciiDoc block roles ([role="x"] / [.x]) preserved as classes (#384).
|
|
3586
|
-
_abstract marks a module's lead/intro paragraph; .lead/.small are
|
|
3587
|
-
Asciidoctor built-ins; _additional-resources marks the trailing
|
|
3588
|
-
"Additional resources" section heading. */
|
|
3589
|
-
& ._abstract, & .abstract, & .lead {
|
|
3590
|
-
font-size: 1.05rem;
|
|
3591
|
-
line-height: 1.6;
|
|
3592
|
-
color: var(--muted-foreground);
|
|
3593
|
-
}
|
|
3594
|
-
& .small { font-size: 0.875rem; }
|
|
3595
|
-
& h2._additional-resources, & h3._additional-resources {
|
|
3596
|
-
font-size: 1.15rem;
|
|
3597
|
-
margin-top: 2rem;
|
|
3598
|
-
}
|
|
3599
|
-
|
|
3600
|
-
/* Related resources (#384 R3): [role="_additional-resources"] + .Title +
|
|
3601
|
-
list folds to a <section class="related-resources"> (→ DITA related-links). */
|
|
3602
|
-
& .related-resources {
|
|
3603
|
-
margin-top: 2rem;
|
|
3604
|
-
padding-top: 1rem;
|
|
3605
|
-
border-top: 1px solid var(--border);
|
|
3606
|
-
}
|
|
3607
|
-
& .related-resources-title {
|
|
3608
|
-
font-weight: 600;
|
|
3609
|
-
margin: 0 0 0.5rem;
|
|
3610
|
-
}
|
|
3611
|
-
}
|
|
3612
|
-
|
|
3613
|
-
@utility scrollbar-none {
|
|
3614
|
-
-ms-overflow-style: none;
|
|
3615
|
-
scrollbar-width: none;
|
|
3616
|
-
&::-webkit-scrollbar {
|
|
3617
|
-
display: none;
|
|
3618
|
-
}
|
|
3619
|
-
}
|
|
3620
|
-
`;
|
|
3621
|
-
}
|
|
3622
|
-
// ── Theme presets ───────────────────────────────────────
|
|
3623
|
-
const THEME_DEFAULT = {
|
|
3624
|
-
light: `:root {
|
|
3625
|
-
--background: oklch(1 0 0);
|
|
3626
|
-
--foreground: oklch(0.145 0.014 285.823);
|
|
3627
|
-
--card: oklch(1 0 0);
|
|
3628
|
-
--card-foreground: oklch(0.145 0.014 285.823);
|
|
3629
|
-
--popover: oklch(1 0 0);
|
|
3630
|
-
--popover-foreground: oklch(0.145 0.014 285.823);
|
|
3631
|
-
--primary: oklch(0.205 0.014 285.823);
|
|
3632
|
-
--primary-foreground: oklch(0.985 0 0);
|
|
3633
|
-
--secondary: oklch(0.965 0.001 286.375);
|
|
3634
|
-
--secondary-foreground: oklch(0.205 0.014 285.823);
|
|
3635
|
-
--muted: oklch(0.965 0.001 286.375);
|
|
3636
|
-
--muted-foreground: oklch(0.45 0.014 285.938);
|
|
3637
|
-
--accent: oklch(0.965 0.001 286.375);
|
|
3638
|
-
--accent-foreground: oklch(0.205 0.014 285.823);
|
|
3639
|
-
--destructive: oklch(0.45 0.245 27.325);
|
|
3640
|
-
--destructive-foreground: oklch(0.985 0 0);
|
|
3641
|
-
--border: oklch(0.922 0.004 286.32);
|
|
3642
|
-
--input: oklch(0.922 0.004 286.32);
|
|
3643
|
-
--ring: oklch(0.708 0.014 285.823);
|
|
3644
|
-
--radius: 0.625rem;
|
|
3645
|
-
--note: oklch(0.637 0.174 259.126);
|
|
3646
|
-
--note-foreground: var(--foreground);
|
|
3647
|
-
--abstract: oklch(0.691 0.148 221.723);
|
|
3648
|
-
--info: oklch(0.722 0.128 207.084);
|
|
3649
|
-
--tip: oklch(0.731 0.112 178.627);
|
|
3650
|
-
--success: oklch(0.754 0.175 149.577);
|
|
3651
|
-
--question: oklch(0.809 0.19 126.526);
|
|
3652
|
-
--warning: oklch(0.74 0.175 62.778);
|
|
3653
|
-
--failure: oklch(0.614 0.194 21.602);
|
|
3654
|
-
--danger: oklch(0.577 0.245 16.439);
|
|
3655
|
-
--bug: oklch(0.548 0.258 349.761);
|
|
3656
|
-
--example: oklch(0.534 0.222 286.033);
|
|
3657
|
-
--quote: oklch(0.65 0 0);
|
|
3658
|
-
--sidebar: oklch(0.985 0 0);
|
|
3659
|
-
--sidebar-foreground: oklch(0.145 0.014 285.823);
|
|
3660
|
-
--sidebar-primary: oklch(0.205 0.014 285.823);
|
|
3661
|
-
--sidebar-primary-foreground: oklch(0.985 0 0);
|
|
3662
|
-
--sidebar-accent: oklch(0.965 0.001 286.375);
|
|
3663
|
-
--sidebar-accent-foreground: oklch(0.205 0.014 285.823);
|
|
3664
|
-
--sidebar-border: oklch(0.922 0.004 286.32);
|
|
3665
|
-
--sidebar-ring: oklch(0.708 0.014 285.823);
|
|
3666
|
-
--code-background: oklch(0.965 0.004 264);
|
|
3667
|
-
--code-foreground: oklch(0.3 0.014 253.833);
|
|
3668
|
-
|
|
3669
|
-
/* API reference — always-dark code panel */
|
|
3670
|
-
--api-panel-bg: oklch(0.145 0.014 253.833);
|
|
3671
|
-
--api-panel-fg: oklch(0.926 0.013 253.833);
|
|
3672
|
-
|
|
3673
|
-
/* API semantic colors — HTTP methods, types, status codes */
|
|
3674
|
-
/* Light mode: darkened for WCAG AA on the 20%/10% tints. See .dark. */
|
|
3675
|
-
--api-get: oklch(0.435 0.095 162);
|
|
3676
|
-
--api-post: oklch(0.45 0.174 259);
|
|
3677
|
-
--api-put: oklch(0.455 0.105 63);
|
|
3678
|
-
--api-patch: oklch(0.455 0.13 45);
|
|
3679
|
-
--api-delete: oklch(0.45 0.179 22);
|
|
3680
|
-
--api-head: oklch(0.47 0.222 286);
|
|
3681
|
-
--api-required: oklch(0.45 0.179 22);
|
|
3682
|
-
--api-deprecated: oklch(0.455 0.105 63);
|
|
3683
|
-
--api-type-string: oklch(0.435 0.095 162);
|
|
3684
|
-
--api-type-number: oklch(0.45 0.174 259);
|
|
3685
|
-
--api-type-boolean: oklch(0.455 0.105 63);
|
|
3686
|
-
--api-type-object: oklch(0.47 0.222 286);
|
|
3687
|
-
--api-type-array: oklch(0.455 0.13 45);
|
|
3688
|
-
--api-status-2xx: oklch(0.435 0.095 162);
|
|
3689
|
-
--api-status-3xx: oklch(0.45 0.174 259);
|
|
3690
|
-
--api-status-4xx: oklch(0.455 0.105 63);
|
|
3691
|
-
--api-status-5xx: oklch(0.45 0.179 22);
|
|
3692
|
-
|
|
3693
|
-
--font-sans: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
|
3694
|
-
--font-heading: var(--font-sans);
|
|
3695
|
-
--font-code: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
|
|
3696
|
-
}`,
|
|
3697
|
-
dark: `.dark {
|
|
3698
|
-
--background: oklch(0.145 0.014 285.823);
|
|
3699
|
-
--foreground: oklch(0.985 0 0);
|
|
3700
|
-
--card: oklch(0.145 0.014 285.823);
|
|
3701
|
-
--card-foreground: oklch(0.985 0 0);
|
|
3702
|
-
--popover: oklch(0.145 0.014 285.823);
|
|
3703
|
-
--popover-foreground: oklch(0.985 0 0);
|
|
3704
|
-
--primary: oklch(0.985 0 0);
|
|
3705
|
-
--primary-foreground: oklch(0.205 0.014 285.823);
|
|
3706
|
-
--secondary: oklch(0.269 0.007 286.033);
|
|
3707
|
-
--secondary-foreground: oklch(0.985 0 0);
|
|
3708
|
-
--muted: oklch(0.269 0.007 286.033);
|
|
3709
|
-
--muted-foreground: oklch(0.708 0.014 285.823);
|
|
3710
|
-
--accent: oklch(0.269 0.007 286.033);
|
|
3711
|
-
--accent-foreground: oklch(0.985 0 0);
|
|
3712
|
-
--destructive: oklch(0.704 0.191 22.216);
|
|
3713
|
-
--destructive-foreground: oklch(0.145 0.014 285.823);
|
|
3714
|
-
--border: oklch(0.269 0.007 286.033);
|
|
3715
|
-
--input: oklch(0.269 0.007 286.033);
|
|
3716
|
-
--ring: oklch(0.439 0.01 286.375);
|
|
3717
|
-
--note-foreground: var(--foreground);
|
|
3718
|
-
--sidebar: oklch(0.205 0.014 285.823);
|
|
3719
|
-
--sidebar-foreground: oklch(0.985 0 0);
|
|
3720
|
-
--sidebar-primary: oklch(0.488 0.243 264.376);
|
|
3721
|
-
--sidebar-primary-foreground: oklch(0.985 0 0);
|
|
3722
|
-
--sidebar-accent: oklch(0.269 0.007 286.033);
|
|
3723
|
-
--sidebar-accent-foreground: oklch(0.985 0 0);
|
|
3724
|
-
--sidebar-border: oklch(0.269 0.007 286.033);
|
|
3725
|
-
--sidebar-ring: oklch(0.439 0.01 286.375);
|
|
3726
|
-
--code-background: oklch(0.18 0.014 253.833);
|
|
3727
|
-
--code-foreground: oklch(0.926 0.013 253.833);
|
|
3728
|
-
|
|
3729
|
-
/* API semantic colors — lightened for dark mode (AA on 20%/10% tints). */
|
|
3730
|
-
--api-get: oklch(0.720 0.135 162);
|
|
3731
|
-
--api-post: oklch(0.755 0.174 259);
|
|
3732
|
-
--api-put: oklch(0.745 0.15 63);
|
|
3733
|
-
--api-patch: oklch(0.725 0.18 45);
|
|
3734
|
-
--api-delete: oklch(0.805 0.194 22);
|
|
3735
|
-
--api-head: oklch(0.740 0.182 286);
|
|
3736
|
-
--api-required: oklch(0.805 0.194 22);
|
|
3737
|
-
--api-deprecated: oklch(0.745 0.15 63);
|
|
3738
|
-
--api-type-string: oklch(0.690 0.135 162);
|
|
3739
|
-
--api-type-number: oklch(0.725 0.174 259);
|
|
3740
|
-
--api-type-boolean: oklch(0.720 0.15 63);
|
|
3741
|
-
--api-type-object: oklch(0.740 0.182 286);
|
|
3742
|
-
--api-type-array: oklch(0.725 0.18 45);
|
|
3743
|
-
--api-status-2xx: oklch(0.720 0.135 162);
|
|
3744
|
-
--api-status-3xx: oklch(0.755 0.174 259);
|
|
3745
|
-
--api-status-4xx: oklch(0.770 0.15 63);
|
|
3746
|
-
--api-status-5xx: oklch(0.805 0.194 22);
|
|
3747
|
-
}`,
|
|
3748
|
-
};
|
|
3749
|
-
/**
|
|
3750
|
-
* Mintlify-inspired theme — clean, neutral palette matching modern doc sites.
|
|
3751
|
-
*
|
|
3752
|
-
* Extracted from Claude Code docs (Mintlify):
|
|
3753
|
-
* - White background, near-black text
|
|
3754
|
-
* - Serif display font for H1, sans-serif body
|
|
3755
|
-
* - Soft colored callout backgrounds (blue note, green tip, amber warning)
|
|
3756
|
-
* - Rounded callout borders (16px radius)
|
|
3757
|
-
* - Light gray code background, JetBrains Mono
|
|
3758
|
-
*/
|
|
3759
|
-
const THEME_MINTLIFY = {
|
|
3760
|
-
light: `:root {
|
|
3761
|
-
--background: oklch(0.993 0.005 95);
|
|
3762
|
-
--foreground: oklch(0.15 0.01 260);
|
|
3763
|
-
--card: oklch(0.993 0.005 95);
|
|
3764
|
-
--card-foreground: oklch(0.15 0.01 260);
|
|
3765
|
-
--popover: oklch(0.993 0.005 95);
|
|
3766
|
-
--popover-foreground: oklch(0.15 0.01 260);
|
|
3767
|
-
--primary: oklch(0.15 0.01 260);
|
|
3768
|
-
--primary-foreground: oklch(0.985 0 0);
|
|
3769
|
-
--secondary: oklch(0.97 0.002 260);
|
|
3770
|
-
--secondary-foreground: oklch(0.15 0.01 260);
|
|
3771
|
-
--muted: oklch(0.97 0.002 260);
|
|
3772
|
-
--muted-foreground: oklch(0.45 0.01 260);
|
|
3773
|
-
--accent: oklch(0.97 0.002 260);
|
|
3774
|
-
--accent-foreground: oklch(0.15 0.01 260);
|
|
3775
|
-
--destructive: oklch(0.55 0.22 25);
|
|
3776
|
-
--destructive-foreground: oklch(0.985 0 0);
|
|
3777
|
-
--border: oklch(0.91 0.004 260);
|
|
3778
|
-
--input: oklch(0.91 0.004 260);
|
|
3779
|
-
--ring: oklch(0.7 0.01 260);
|
|
3780
|
-
--radius: 1rem;
|
|
3781
|
-
/* Callout colors — soft pastel backgrounds matching Mintlify */
|
|
3782
|
-
--note: oklch(0.62 0.18 260);
|
|
3783
|
-
--note-foreground: var(--foreground);
|
|
3784
|
-
--abstract: oklch(0.65 0.14 220);
|
|
3785
|
-
--info: oklch(0.65 0.14 230);
|
|
3786
|
-
--tip: oklch(0.65 0.16 155);
|
|
3787
|
-
--success: oklch(0.65 0.16 155);
|
|
3788
|
-
--question: oklch(0.7 0.15 270);
|
|
3789
|
-
--warning: oklch(0.7 0.15 75);
|
|
3790
|
-
--failure: oklch(0.6 0.2 25);
|
|
3791
|
-
--danger: oklch(0.55 0.22 25);
|
|
3792
|
-
--bug: oklch(0.55 0.22 350);
|
|
3793
|
-
--example: oklch(0.55 0.2 285);
|
|
3794
|
-
--quote: oklch(0.55 0 0);
|
|
3795
|
-
/* Sidebar */
|
|
3796
|
-
--sidebar: oklch(0.993 0.005 95);
|
|
3797
|
-
--sidebar-foreground: oklch(0.15 0.01 260);
|
|
3798
|
-
--sidebar-primary: oklch(0.15 0.01 260);
|
|
3799
|
-
--sidebar-primary-foreground: oklch(0.985 0 0);
|
|
3800
|
-
--sidebar-accent: oklch(0.97 0.002 260);
|
|
3801
|
-
--sidebar-accent-foreground: oklch(0.15 0.01 260);
|
|
3802
|
-
--sidebar-border: oklch(0.93 0.004 260);
|
|
3803
|
-
--sidebar-ring: oklch(0.7 0.01 260);
|
|
3804
|
-
/* Code — light gray background, matching Mintlify's inline code */
|
|
3805
|
-
--code-background: oklch(0.96 0.003 260);
|
|
3806
|
-
--code-foreground: oklch(0.25 0.01 260);
|
|
3807
|
-
|
|
3808
|
-
/* API reference — always-dark code panel */
|
|
3809
|
-
--api-panel-bg: oklch(0.145 0.014 253.833);
|
|
3810
|
-
--api-panel-fg: oklch(0.926 0.013 253.833);
|
|
3811
|
-
|
|
3812
|
-
/* API semantic colors — HTTP methods, types, status codes */
|
|
3813
|
-
/* Light mode: darkened for WCAG AA on the 20%/10% tints. See .dark. */
|
|
3814
|
-
--api-get: oklch(0.435 0.095 162);
|
|
3815
|
-
--api-post: oklch(0.45 0.174 259);
|
|
3816
|
-
--api-put: oklch(0.455 0.105 63);
|
|
3817
|
-
--api-patch: oklch(0.455 0.13 45);
|
|
3818
|
-
--api-delete: oklch(0.45 0.179 22);
|
|
3819
|
-
--api-head: oklch(0.47 0.222 286);
|
|
3820
|
-
--api-required: oklch(0.45 0.179 22);
|
|
3821
|
-
--api-deprecated: oklch(0.455 0.105 63);
|
|
3822
|
-
--api-type-string: oklch(0.435 0.095 162);
|
|
3823
|
-
--api-type-number: oklch(0.45 0.174 259);
|
|
3824
|
-
--api-type-boolean: oklch(0.455 0.105 63);
|
|
3825
|
-
--api-type-object: oklch(0.47 0.222 286);
|
|
3826
|
-
--api-type-array: oklch(0.455 0.13 45);
|
|
3827
|
-
--api-status-2xx: oklch(0.435 0.095 162);
|
|
3828
|
-
--api-status-3xx: oklch(0.45 0.174 259);
|
|
3829
|
-
--api-status-4xx: oklch(0.455 0.105 63);
|
|
3830
|
-
--api-status-5xx: oklch(0.45 0.179 22);
|
|
3831
|
-
|
|
3832
|
-
/* Fonts — system sans for body, JetBrains Mono for code */
|
|
3833
|
-
--font-sans: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
|
3834
|
-
--font-heading: var(--font-sans);
|
|
3835
|
-
--font-code: "JetBrains Mono", "Fira Code", ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
|
3836
|
-
}`,
|
|
3837
|
-
dark: `.dark {
|
|
3838
|
-
--background: oklch(0.16 0.01 260);
|
|
3839
|
-
--foreground: oklch(0.93 0.005 260);
|
|
3840
|
-
--card: oklch(0.18 0.01 260);
|
|
3841
|
-
--card-foreground: oklch(0.93 0.005 260);
|
|
3842
|
-
--popover: oklch(0.18 0.01 260);
|
|
3843
|
-
--popover-foreground: oklch(0.93 0.005 260);
|
|
3844
|
-
--primary: oklch(0.93 0.005 260);
|
|
3845
|
-
--primary-foreground: oklch(0.16 0.01 260);
|
|
3846
|
-
--secondary: oklch(0.22 0.008 260);
|
|
3847
|
-
--secondary-foreground: oklch(0.93 0.005 260);
|
|
3848
|
-
--muted: oklch(0.22 0.008 260);
|
|
3849
|
-
--muted-foreground: oklch(0.65 0.01 260);
|
|
3850
|
-
--accent: oklch(0.22 0.008 260);
|
|
3851
|
-
--accent-foreground: oklch(0.93 0.005 260);
|
|
3852
|
-
--destructive: oklch(0.65 0.2 25);
|
|
3853
|
-
--destructive-foreground: oklch(0.16 0.01 260);
|
|
3854
|
-
--border: oklch(0.28 0.008 260);
|
|
3855
|
-
--input: oklch(0.28 0.008 260);
|
|
3856
|
-
--ring: oklch(0.45 0.01 260);
|
|
3857
|
-
--note-foreground: var(--foreground);
|
|
3858
|
-
--sidebar: oklch(0.16 0.01 260);
|
|
3859
|
-
--sidebar-foreground: oklch(0.93 0.005 260);
|
|
3860
|
-
--sidebar-primary: oklch(0.55 0.2 260);
|
|
3861
|
-
--sidebar-primary-foreground: oklch(0.93 0.005 260);
|
|
3862
|
-
--sidebar-accent: oklch(0.22 0.008 260);
|
|
3863
|
-
--sidebar-accent-foreground: oklch(0.93 0.005 260);
|
|
3864
|
-
--sidebar-border: oklch(0.28 0.008 260);
|
|
3865
|
-
--sidebar-ring: oklch(0.45 0.01 260);
|
|
3866
|
-
--code-background: oklch(0.2 0.01 260);
|
|
3867
|
-
--code-foreground: oklch(0.9 0.01 260);
|
|
3868
|
-
|
|
3869
|
-
/* API semantic colors — lightened for dark mode (AA on 20%/10% tints). */
|
|
3870
|
-
--api-get: oklch(0.720 0.135 162);
|
|
3871
|
-
--api-post: oklch(0.755 0.174 259);
|
|
3872
|
-
--api-put: oklch(0.745 0.15 63);
|
|
3873
|
-
--api-patch: oklch(0.725 0.18 45);
|
|
3874
|
-
--api-delete: oklch(0.805 0.194 22);
|
|
3875
|
-
--api-head: oklch(0.740 0.182 286);
|
|
3876
|
-
--api-required: oklch(0.805 0.194 22);
|
|
3877
|
-
--api-deprecated: oklch(0.745 0.15 63);
|
|
3878
|
-
--api-type-string: oklch(0.690 0.135 162);
|
|
3879
|
-
--api-type-number: oklch(0.725 0.174 259);
|
|
3880
|
-
--api-type-boolean: oklch(0.720 0.15 63);
|
|
3881
|
-
--api-type-object: oklch(0.740 0.182 286);
|
|
3882
|
-
--api-type-array: oklch(0.725 0.18 45);
|
|
3883
|
-
--api-status-2xx: oklch(0.720 0.135 162);
|
|
3884
|
-
--api-status-3xx: oklch(0.755 0.174 259);
|
|
3885
|
-
--api-status-4xx: oklch(0.770 0.15 63);
|
|
3886
|
-
--api-status-5xx: oklch(0.805 0.194 22);
|
|
3887
|
-
}`,
|
|
3888
|
-
};
|
|
3889
3441
|
function writeThemeFile(path, themeName) {
|
|
3890
|
-
|
|
3891
|
-
writeFileSync(path, `@custom-variant dark (&:where(.dark, .dark *));
|
|
3892
|
-
|
|
3893
|
-
@theme inline {
|
|
3894
|
-
--color-background: var(--background);
|
|
3895
|
-
--color-foreground: var(--foreground);
|
|
3896
|
-
--color-card: var(--card);
|
|
3897
|
-
--color-card-foreground: var(--card-foreground);
|
|
3898
|
-
--color-popover: var(--popover);
|
|
3899
|
-
--color-popover-foreground: var(--popover-foreground);
|
|
3900
|
-
--color-primary: var(--primary);
|
|
3901
|
-
--color-primary-foreground: var(--primary-foreground);
|
|
3902
|
-
--color-secondary: var(--secondary);
|
|
3903
|
-
--color-secondary-foreground: var(--secondary-foreground);
|
|
3904
|
-
--color-muted: var(--muted);
|
|
3905
|
-
--color-muted-foreground: var(--muted-foreground);
|
|
3906
|
-
--color-accent: var(--accent);
|
|
3907
|
-
--color-accent-foreground: var(--accent-foreground);
|
|
3908
|
-
--color-destructive: var(--destructive);
|
|
3909
|
-
--color-destructive-foreground: var(--destructive-foreground);
|
|
3910
|
-
--color-border: var(--border);
|
|
3911
|
-
--color-input: var(--input);
|
|
3912
|
-
--color-ring: var(--ring);
|
|
3913
|
-
--radius-sm: calc(var(--radius) - 4px);
|
|
3914
|
-
--radius-md: calc(var(--radius) - 2px);
|
|
3915
|
-
--radius-lg: var(--radius);
|
|
3916
|
-
--radius-xl: calc(var(--radius) + 4px);
|
|
3917
|
-
|
|
3918
|
-
/* Sidebar */
|
|
3919
|
-
--color-sidebar: var(--sidebar);
|
|
3920
|
-
--color-sidebar-foreground: var(--sidebar-foreground);
|
|
3921
|
-
--color-sidebar-primary: var(--sidebar-primary);
|
|
3922
|
-
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
|
3923
|
-
--color-sidebar-accent: var(--sidebar-accent);
|
|
3924
|
-
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
|
3925
|
-
--color-sidebar-border: var(--sidebar-border);
|
|
3926
|
-
--color-sidebar-ring: var(--sidebar-ring);
|
|
3927
|
-
|
|
3928
|
-
/* Admonition colors */
|
|
3929
|
-
--color-note: var(--note);
|
|
3930
|
-
--color-note-foreground: var(--note-foreground);
|
|
3931
|
-
--color-abstract: var(--abstract);
|
|
3932
|
-
--color-info: var(--info);
|
|
3933
|
-
--color-tip: var(--tip);
|
|
3934
|
-
--color-success: var(--success);
|
|
3935
|
-
--color-question: var(--question);
|
|
3936
|
-
--color-warning: var(--warning);
|
|
3937
|
-
--color-failure: var(--failure);
|
|
3938
|
-
--color-danger: var(--danger);
|
|
3939
|
-
--color-bug: var(--bug);
|
|
3940
|
-
--color-example: var(--example);
|
|
3941
|
-
--color-quote: var(--quote);
|
|
3942
|
-
|
|
3943
|
-
/* Code */
|
|
3944
|
-
--color-code-bg: var(--code-background);
|
|
3945
|
-
--color-code-fg: var(--code-foreground);
|
|
3946
|
-
|
|
3947
|
-
/* API reference — HTTP method, type, status, role colors */
|
|
3948
|
-
--color-api-get: var(--api-get);
|
|
3949
|
-
--color-api-post: var(--api-post);
|
|
3950
|
-
--color-api-put: var(--api-put);
|
|
3951
|
-
--color-api-patch: var(--api-patch);
|
|
3952
|
-
--color-api-delete: var(--api-delete);
|
|
3953
|
-
--color-api-head: var(--api-head);
|
|
3954
|
-
--color-api-required: var(--api-required);
|
|
3955
|
-
--color-api-deprecated: var(--api-deprecated);
|
|
3956
|
-
--color-api-type-string: var(--api-type-string);
|
|
3957
|
-
--color-api-type-number: var(--api-type-number);
|
|
3958
|
-
--color-api-type-boolean: var(--api-type-boolean);
|
|
3959
|
-
--color-api-type-object: var(--api-type-object);
|
|
3960
|
-
--color-api-type-array: var(--api-type-array);
|
|
3961
|
-
--color-api-status-2xx: var(--api-status-2xx);
|
|
3962
|
-
--color-api-status-3xx: var(--api-status-3xx);
|
|
3963
|
-
--color-api-status-4xx: var(--api-status-4xx);
|
|
3964
|
-
--color-api-status-5xx: var(--api-status-5xx);
|
|
3965
|
-
|
|
3966
|
-
/* Fonts */
|
|
3967
|
-
--font-sans: var(--font-sans);
|
|
3968
|
-
--font-mono: var(--font-code);
|
|
3969
|
-
--font-family-code: var(--font-code);
|
|
3970
|
-
}
|
|
3971
|
-
|
|
3972
|
-
${tokens.light}
|
|
3973
|
-
|
|
3974
|
-
${tokens.dark}
|
|
3975
|
-
|
|
3976
|
-
@layer base {
|
|
3977
|
-
* { @apply border-border outline-ring/50; }
|
|
3978
|
-
body { @apply bg-background text-foreground font-sans; }
|
|
3979
|
-
}
|
|
3980
|
-
|
|
3981
|
-
/* Shiki dual-theme: light by default, dark when .dark class is active */
|
|
3982
|
-
.shiki,
|
|
3983
|
-
.shiki span,
|
|
3984
|
-
.astro-code,
|
|
3985
|
-
.astro-code span {
|
|
3986
|
-
color: var(--shiki-light) !important;
|
|
3987
|
-
}
|
|
3988
|
-
.shiki,
|
|
3989
|
-
.astro-code {
|
|
3990
|
-
background-color: var(--shiki-light-bg) !important;
|
|
3991
|
-
}
|
|
3992
|
-
.dark .shiki,
|
|
3993
|
-
.dark .shiki span,
|
|
3994
|
-
.dark .astro-code,
|
|
3995
|
-
.dark .astro-code span {
|
|
3996
|
-
color: var(--shiki-dark) !important;
|
|
3997
|
-
}
|
|
3998
|
-
.dark .shiki,
|
|
3999
|
-
.dark .astro-code {
|
|
4000
|
-
background-color: var(--shiki-dark-bg) !important;
|
|
4001
|
-
}
|
|
4002
|
-
/* Fix comment color contrast for WCAG AA */
|
|
4003
|
-
.shiki span[style*="--shiki-light:#6E7781"],
|
|
4004
|
-
.astro-code span[style*="--shiki-light:#6E7781"] {
|
|
4005
|
-
--shiki-light: #576069 !important;
|
|
4006
|
-
}
|
|
4007
|
-
`);
|
|
3442
|
+
writeFileSync(path, themeCss(themeName));
|
|
4008
3443
|
}
|
|
4009
3444
|
// ── Resolution helpers ──────────────────────────────────
|
|
4010
3445
|
/**
|