@dogsbay/format-astro 0.2.0-beta.114 → 0.2.0-beta.116
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/llms-txt.d.ts +3 -3
- package/dist/llms-txt.d.ts.map +1 -1
- package/dist/llms-txt.js +48 -242
- 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 +292 -840
- 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 +305 -103
- 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
|
|
@@ -1507,6 +1550,13 @@ export async function emitAstroPages(pages, nav, outputDir, options) {
|
|
|
1507
1550
|
const basePath = normalizeBasePath(options.basePath);
|
|
1508
1551
|
const combined = combinedPrefix(options);
|
|
1509
1552
|
const baseSegments = basePathSegments(basePath);
|
|
1553
|
+
// Base Pagefind prefixes RESULT urls with: the served path dist/'s root
|
|
1554
|
+
// maps to. Same rule as docs-layout's pagefindBaseUrl(); see the comment
|
|
1555
|
+
// on the pagefindBaseUrl prop below. Computed once, not per page.
|
|
1556
|
+
const pageUrlBase = parseSiteUrl(options.siteUrl).urlBase;
|
|
1557
|
+
const searchBaseUrl = workersSubpathOutDir(options.deploy, pageUrlBase)
|
|
1558
|
+
? "/"
|
|
1559
|
+
: `${pageUrlBase}/`;
|
|
1510
1560
|
// Ensure dirs exist (callers may invoke us without going through the
|
|
1511
1561
|
// full exportAstroProject orchestrator, e.g. dogsbay convert at Step 7).
|
|
1512
1562
|
mkdirSync(join(outputDir, "src", "pages", ...baseSegments), { recursive: true });
|
|
@@ -1711,6 +1761,33 @@ export async function emitAstroPages(pages, nav, outputDir, options) {
|
|
|
1711
1761
|
: undefined;
|
|
1712
1762
|
const blogAdj = blogData ? blogAdjacency(blogData) : undefined;
|
|
1713
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
|
+
});
|
|
1714
1791
|
for (const page of emitPages) {
|
|
1715
1792
|
try {
|
|
1716
1793
|
// Skip excluded pages before any expensive work (tree rewrite,
|
|
@@ -1734,6 +1811,14 @@ export async function emitAstroPages(pages, nav, outputDir, options) {
|
|
|
1734
1811
|
if (typeof page.redirect === "string" && page.redirect.trim()) {
|
|
1735
1812
|
continue;
|
|
1736
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
|
+
}
|
|
1737
1822
|
// Rewrite internal hrefs to match the output URL structure
|
|
1738
1823
|
rewriteTreeHrefs(page.tree, hrefPrefix);
|
|
1739
1824
|
// Same for raw image srcs — Astro doesn't auto-prefix
|
|
@@ -1744,6 +1829,7 @@ export async function emitAstroPages(pages, nav, outputDir, options) {
|
|
|
1744
1829
|
const result = treeToAstro(page.tree, {
|
|
1745
1830
|
imageOptimization: useImageOpt,
|
|
1746
1831
|
codeBlockTitle: options.codeBlockTitle ?? true,
|
|
1832
|
+
imageCaptions: options.imageCaptions === "title",
|
|
1747
1833
|
combinedPrefix: hrefPrefix,
|
|
1748
1834
|
});
|
|
1749
1835
|
const imageSetup = useImageOpt ? [
|
|
@@ -1779,9 +1865,12 @@ export async function emitAstroPages(pages, nav, outputDir, options) {
|
|
|
1779
1865
|
// cannot opt out of a site-level lockdown. `draft: true` keeps
|
|
1780
1866
|
// its existing role as a noindex shorthand. See
|
|
1781
1867
|
// plans/site-level-robots-meta.md.
|
|
1868
|
+
const pageCanonical = canonical.get(page.slug);
|
|
1869
|
+
const pageHead = head.get(page.slug);
|
|
1782
1870
|
const pageNoindex = options.noindex === true ||
|
|
1783
1871
|
fm.noindex === true ||
|
|
1784
|
-
fm.draft === true
|
|
1872
|
+
fm.draft === true ||
|
|
1873
|
+
pageCanonical?.noindex === true;
|
|
1785
1874
|
const pageNofollow = options.nofollow === true ||
|
|
1786
1875
|
fm.nofollow === true;
|
|
1787
1876
|
// Independent of noindex: pages can be excluded from in-site
|
|
@@ -1934,7 +2023,7 @@ export async function emitAstroPages(pages, nav, outputDir, options) {
|
|
|
1934
2023
|
...result.imports,
|
|
1935
2024
|
...imageSetup,
|
|
1936
2025
|
"",
|
|
1937
|
-
`const headings = ${
|
|
2026
|
+
`const headings = ${pageLiteral(page.headings || [])};`,
|
|
1938
2027
|
`const nav = navData;`,
|
|
1939
2028
|
// currentPath uses combined so it matches nav.json hrefs
|
|
1940
2029
|
// (which are also combined-prefixed). getPagination compares
|
|
@@ -1952,10 +2041,16 @@ export async function emitAstroPages(pages, nav, outputDir, options) {
|
|
|
1952
2041
|
// → "/docs" makes the filter look for `/docs/…` bucket hrefs that
|
|
1953
2042
|
// don't exist, emptying the pagination nav so prev/next vanish on
|
|
1954
2043
|
// every page of a root-served multi-source site.
|
|
1955
|
-
` basePath: ${
|
|
1956
|
-
` namespace: ${
|
|
1957
|
-
` version: ${
|
|
1958
|
-
` 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
|
+
: []),
|
|
1959
2054
|
`});`,
|
|
1960
2055
|
// Blog adjacency is CHRONOLOGICAL, not navigational. getPagination
|
|
1961
2056
|
// walks NavItem[] — sidebar order — and a blog has no sidebar; its
|
|
@@ -1964,25 +2059,28 @@ export async function emitAstroPages(pages, nav, outputDir, options) {
|
|
|
1964
2059
|
// runtime.
|
|
1965
2060
|
...(blogAdj
|
|
1966
2061
|
? [
|
|
1967
|
-
`const prev = ${
|
|
1968
|
-
`const next = ${
|
|
2062
|
+
`const prev = ${pageLiteral(blogAdj.get(page.slug)?.prev ?? null)} ?? undefined;`,
|
|
2063
|
+
`const next = ${pageLiteral(blogAdj.get(page.slug)?.next ?? null)} ?? undefined;`,
|
|
1969
2064
|
]
|
|
1970
2065
|
: [`const { prev, next } = getPagination(currentPath, _navForPagination);`]),
|
|
1971
|
-
`const title = ${
|
|
1972
|
-
`const description = ${
|
|
1973
|
-
`const ogImage = ${
|
|
1974
|
-
`const noindex = ${
|
|
1975
|
-
`const
|
|
1976
|
-
`const
|
|
1977
|
-
`const
|
|
1978
|
-
`const
|
|
1979
|
-
`const
|
|
1980
|
-
`const
|
|
1981
|
-
`const
|
|
1982
|
-
`const
|
|
1983
|
-
`const
|
|
1984
|
-
`const
|
|
1985
|
-
`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
|
|
1986
2084
|
? {
|
|
1987
2085
|
enabled: true,
|
|
1988
2086
|
providers: options.llmActions?.providers,
|
|
@@ -1998,6 +2096,7 @@ export async function emitAstroPages(pages, nav, outputDir, options) {
|
|
|
1998
2096
|
"",
|
|
1999
2097
|
`<DocsLayout`,
|
|
2000
2098
|
` siteName={siteConfig.siteName}`,
|
|
2099
|
+
` siteTagline={siteConfig.tagline}`,
|
|
2001
2100
|
` title={title}`,
|
|
2002
2101
|
` nav={nav as any[]}`,
|
|
2003
2102
|
` headings={headings}`,
|
|
@@ -2012,6 +2111,10 @@ export async function emitAstroPages(pages, nav, outputDir, options) {
|
|
|
2012
2111
|
` themeColor={siteConfig.themeColor || undefined}`,
|
|
2013
2112
|
` noindex={noindex}`,
|
|
2014
2113
|
` nofollow={nofollow}`,
|
|
2114
|
+
` canonicalUrl={canonicalUrl}`,
|
|
2115
|
+
` alternates={alternates}`,
|
|
2116
|
+
` breadcrumbJsonLd={breadcrumbJsonLd}`,
|
|
2117
|
+
...(pageHead?.home ? [` ogType="website"`] : []),
|
|
2015
2118
|
` excludeFromSearch={excludeFromSearch}`,
|
|
2016
2119
|
` plausibleDomain={siteConfig.plausible?.domain}`,
|
|
2017
2120
|
` plausibleScriptUrl={siteConfig.plausible?.scriptUrl}`,
|
|
@@ -2021,6 +2124,7 @@ export async function emitAstroPages(pages, nav, outputDir, options) {
|
|
|
2021
2124
|
` tagsIndexPath={tagsIndexPath}`,
|
|
2022
2125
|
` tagPrefixes={siteConfig.tagPrefixes}`,
|
|
2023
2126
|
` tagLabels={siteConfig.tagLabels}`,
|
|
2127
|
+
` tagAutoColor={siteConfig.tagAutoColor}`,
|
|
2024
2128
|
` taxonomyIndexPaths={siteConfig.taxonomyIndexPaths}`,
|
|
2025
2129
|
` taxonomyDisplay={siteConfig.taxonomyDisplay}`,
|
|
2026
2130
|
` status={pageStatus ?? undefined}`,
|
|
@@ -2035,7 +2139,7 @@ export async function emitAstroPages(pages, nav, outputDir, options) {
|
|
|
2035
2139
|
` linkIcons={siteConfig.linkIcons}`,
|
|
2036
2140
|
// Default ON: an absent key means yes, `false` means no.
|
|
2037
2141
|
` poweredBy={siteConfig.poweredBy !== false}`,
|
|
2038
|
-
` multiSource={${
|
|
2142
|
+
` multiSource={${pageLiteral(page.multiSource ?? null)} ?? undefined}`,
|
|
2039
2143
|
` switcherMap={switcherMapData}`,
|
|
2040
2144
|
// basePath here is the COMBINED URL prefix (urlBase from
|
|
2041
2145
|
// site.url's path + dogsbay basePath). DocsLayout uses it
|
|
@@ -2044,19 +2148,29 @@ export async function emitAstroPages(pages, nav, outputDir, options) {
|
|
|
2044
2148
|
// prefix the host actually serves under. Empty string is
|
|
2045
2149
|
// valid (root-served sites with no urlBase or basePath);
|
|
2046
2150
|
// don't fall back to "/docs" — that would 404 for those.
|
|
2047
|
-
` basePath={${
|
|
2151
|
+
` basePath={${pageLiteral(combined)}}`,
|
|
2048
2152
|
// navMode — controls whether DocsLayout server-renders the
|
|
2049
2153
|
// full sidebar nav tree per page (`ssr-full`) or emits a
|
|
2050
2154
|
// client-hydrated placeholder (`client`, default). The
|
|
2051
2155
|
// client mode shrinks per-page HTML dramatically at scale.
|
|
2052
2156
|
// See plans/client-rendered-nav.md.
|
|
2053
|
-
` navMode={${
|
|
2157
|
+
` navMode={${pageLiteral(options.navMode ?? "client")}}`,
|
|
2054
2158
|
// Pagefind index URL — must include the combined prefix or
|
|
2055
2159
|
// the loader 404s on subpath-mounted deploys. The pagefind
|
|
2056
2160
|
// CLI writes to <astroOutput>/dist/pagefind/ which Astro
|
|
2057
2161
|
// serves under its `base` (= urlBase); dogsbay's basePath
|
|
2058
2162
|
// adds the second prefix layer. Empty combined → `/pagefind/`.
|
|
2059
|
-
` pagefindUrl={${
|
|
2163
|
+
` pagefindUrl={${pageLiteral(combined ? `${combined}/pagefind/` : "/pagefind/")}}`,
|
|
2164
|
+
// Base Pagefind prefixes RESULT urls with. The index stores URLs
|
|
2165
|
+
// relative to dist/, so the base is whatever served path dist/'s
|
|
2166
|
+
// root maps to. A Workers subpath mount builds pages INTO
|
|
2167
|
+
// dist/<mount>/, so its index already carries the mount → "/".
|
|
2168
|
+
// GitHub Pages builds to dist/ and Astro adds `base` at serve time
|
|
2169
|
+
// → "<urlBase>/". Pagefind's default (the folder above the bundle)
|
|
2170
|
+
// is right only for the second, and doubled the mount on the first:
|
|
2171
|
+
// /dogsbay-xml-docs/dogsbay-xml-docs/…, measured. Same rule as
|
|
2172
|
+
// docs-layout's pagefindBaseUrl() — keep the two in step.
|
|
2173
|
+
` pagefindBaseUrl={${pageLiteral(searchBaseUrl)}}`,
|
|
2060
2174
|
// Favicon — composed with combined prefix so the
|
|
2061
2175
|
// <link rel="icon"> resolves on subpath-mounted deploys.
|
|
2062
2176
|
//
|
|
@@ -2066,7 +2180,7 @@ export async function emitAstroPages(pages, nav, outputDir, options) {
|
|
|
2066
2180
|
// `public/favicon.ico` (or `.svg`) in their Astro project and the
|
|
2067
2181
|
// link appears on the next build; DocsLayout takes `false` to mean
|
|
2068
2182
|
// "emit no <link rel=icon>".
|
|
2069
|
-
` favicon={${
|
|
2183
|
+
` favicon={${pageLiteral(faviconHref(outputDir, parseSiteUrl(options.siteUrl).urlBase))}}`,
|
|
2070
2184
|
` wideLayout={${wideLayout}}`,
|
|
2071
2185
|
// Blog chrome — sidebar dropped, byline on. Every other prop above
|
|
2072
2186
|
// is unchanged, which is what keeps the blog visually continuous
|
|
@@ -2074,9 +2188,9 @@ export async function emitAstroPages(pages, nav, outputDir, options) {
|
|
|
2074
2188
|
...(options.blog
|
|
2075
2189
|
? [
|
|
2076
2190
|
` chrome="blog"`,
|
|
2077
|
-
` author={${
|
|
2078
|
-
` publishedDate={${
|
|
2079
|
-
` 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}`,
|
|
2080
2194
|
` readingMinutes={${blogMinutes.get(page.slug) ?? 1}}`,
|
|
2081
2195
|
// Series position, derived in buildBlogData from publication
|
|
2082
2196
|
// order — never hand-typed in prose, which goes stale the
|
|
@@ -2085,12 +2199,12 @@ export async function emitAstroPages(pages, nav, outputDir, options) {
|
|
|
2085
2199
|
// `heroImage: {src, alt}` object would otherwise reach
|
|
2086
2200
|
// DocsLayout's `heroImage?: string` prop and render as
|
|
2087
2201
|
// src="[object Object]".
|
|
2088
|
-
` heroImage={${
|
|
2202
|
+
` heroImage={${pageLiteral(typeof (page.frontmatter ?? {}).heroImage === "string"
|
|
2089
2203
|
? (page.frontmatter ?? {}).heroImage
|
|
2090
2204
|
: null)} ?? undefined}`,
|
|
2091
2205
|
]
|
|
2092
2206
|
: []),
|
|
2093
|
-
` toc={${
|
|
2207
|
+
` toc={${pageLiteral(tocMode)}}`,
|
|
2094
2208
|
`>`,
|
|
2095
2209
|
// RightRail region — assigned to DocsLayout's `right-rail` named
|
|
2096
2210
|
// slot. Inert (empty passthrough) until a plugin contributes a
|
|
@@ -2197,8 +2311,14 @@ export async function emitAstroPages(pages, nav, outputDir, options) {
|
|
|
2197
2311
|
// the same contract the site root gets: redirect to its first nav
|
|
2198
2312
|
// entry.
|
|
2199
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}`);
|
|
2200
2320
|
for (const slugPrefix of subtreeRoots.keys()) {
|
|
2201
|
-
const target = findFirstNavHrefUnder(outputNav, [`${combined}/${slugPrefix}`]);
|
|
2321
|
+
const target = findFirstNavHrefUnder(outputNav, [`${combined}/${slugPrefix}`], nestedIn(slugPrefix));
|
|
2202
2322
|
if (!target)
|
|
2203
2323
|
continue;
|
|
2204
2324
|
const subtreeIndexPath = join(pagesDir, section ? section : "", slugPrefix, "index.astro");
|
|
@@ -2224,9 +2344,16 @@ export async function emitAstroPages(pages, nav, outputDir, options) {
|
|
|
2224
2344
|
.filter(([, version]) => version === options.defaultVersion)
|
|
2225
2345
|
.map(([slugPrefix]) => `${combined}/${slugPrefix}`)
|
|
2226
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.
|
|
2227
2350
|
const firstHref = (defaultVersionPrefixes.length > 0
|
|
2228
|
-
? findFirstNavHrefUnder(outputNav, defaultVersionPrefixes
|
|
2229
|
-
|
|
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);
|
|
2230
2357
|
const needsRootRedirect = basePath !== "" ||
|
|
2231
2358
|
(!section && !rootIndexEmitted && firstHref !== "");
|
|
2232
2359
|
if (needsRootRedirect) {
|
|
@@ -2326,6 +2453,8 @@ function emitRedirects(outputDir, options, pages) {
|
|
|
2326
2453
|
slug: p.slug,
|
|
2327
2454
|
redirect: p.redirect,
|
|
2328
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,
|
|
2329
2458
|
})),
|
|
2330
2459
|
config: options.redirects,
|
|
2331
2460
|
prefix,
|
|
@@ -2447,8 +2576,9 @@ export function emitSwitcherMap(pages, outputDir, options) {
|
|
|
2447
2576
|
writeFileSync(outPath, JSON.stringify({ versions: [], locales: [], byLogicalKey: {} }, null, 2));
|
|
2448
2577
|
return;
|
|
2449
2578
|
}
|
|
2579
|
+
const rootVersion = rootVersionOf(options);
|
|
2450
2580
|
const versions = composeAxisHeader(options.versions ?? [], versionsSeen, options.defaultVersion,
|
|
2451
|
-
/* allowEol */ true);
|
|
2581
|
+
/* allowEol */ true).map((v) => (v.id === rootVersion ? { ...v, root: true } : v));
|
|
2452
2582
|
const locales = composeAxisHeader(options.locales ?? [], localesSeen, options.defaultLocale,
|
|
2453
2583
|
/* allowEol */ false);
|
|
2454
2584
|
// Per-logical-key list of variants — every (locale, version)
|
|
@@ -2540,65 +2670,50 @@ export function emitMissingTranslationStubs(pages, outputDir, options) {
|
|
|
2540
2670
|
const defaultLocale = options.defaultLocale;
|
|
2541
2671
|
if (!defaultLocale)
|
|
2542
2672
|
return;
|
|
2543
|
-
const knownLocales = new Set();
|
|
2544
|
-
for (const p of pages) {
|
|
2545
|
-
const l = p.multiSource?.locale;
|
|
2546
|
-
if (l)
|
|
2547
|
-
knownLocales.add(l);
|
|
2548
|
-
}
|
|
2549
|
-
if (knownLocales.size < 2)
|
|
2550
|
-
return;
|
|
2551
|
-
if (!knownLocales.has(defaultLocale))
|
|
2552
|
-
return;
|
|
2553
2673
|
const basePath = normalizeBasePath(options.basePath);
|
|
2554
2674
|
const baseSegments = basePathSegments(basePath);
|
|
2555
2675
|
// combined drives the redirect URL (the user-facing path they
|
|
2556
2676
|
// get bounced to); basePath stays the filesystem path under
|
|
2557
2677
|
// src/pages/ where the stub lives.
|
|
2558
2678
|
const combined = combinedPrefix(options);
|
|
2559
|
-
//
|
|
2560
|
-
//
|
|
2561
|
-
//
|
|
2562
|
-
//
|
|
2563
|
-
//
|
|
2564
|
-
//
|
|
2565
|
-
//
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
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 ?? ""}`;
|
|
2569
2690
|
for (const p of pages) {
|
|
2570
|
-
|
|
2571
|
-
|
|
2572
|
-
let emitted = 0;
|
|
2573
|
-
for (const defaultPage of pages) {
|
|
2574
|
-
const ms = defaultPage.multiSource;
|
|
2575
|
-
if (!ms || ms.locale !== defaultLocale)
|
|
2691
|
+
const ms = p.multiSource;
|
|
2692
|
+
if (!ms?.locale || !p.slug.startsWith(`${ms.locale}/`))
|
|
2576
2693
|
continue;
|
|
2577
|
-
|
|
2578
|
-
// the locale-neutral tail. Defensive: if for some reason the
|
|
2579
|
-
// slug doesn't start with the locale (e.g. unprefixed
|
|
2580
|
-
// baseline page in a half-active site), skip.
|
|
2581
|
-
const localePrefix = `${defaultLocale}/`;
|
|
2582
|
-
if (!defaultPage.slug.startsWith(localePrefix))
|
|
2694
|
+
if (p.fragment === true || p.frontmatter?._fragment === true || isExcludedSlug(p.slug))
|
|
2583
2695
|
continue;
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
|
|
2589
|
-
|
|
2590
|
-
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
|
|
2597
|
-
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
|
|
2601
|
-
|
|
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++;
|
|
2602
2717
|
}
|
|
2603
2718
|
if (emitted > 0) {
|
|
2604
2719
|
console.log(`Emitted ${emitted} missing-translation stub(s) → redirect to default locale.`);
|
|
@@ -2637,7 +2752,7 @@ export function emitAgentReadinessFiles(pages, outputNav, outputDir, siteName, o
|
|
|
2637
2752
|
mkdirSync(join(outputDir, "public"), { recursive: true });
|
|
2638
2753
|
// _headers Link header points at the per-mount llms.txt at
|
|
2639
2754
|
// <combined>/llms.txt — the URL agents would actually fetch.
|
|
2640
|
-
writeFileSync(join(outputDir, "public", "_headers"), buildHeadersFile(combinedPrefix(options)));
|
|
2755
|
+
writeFileSync(join(outputDir, "public", "_headers"), buildHeadersFile(withBasePath(combinedPrefix(options), "/llms.txt")));
|
|
2641
2756
|
}
|
|
2642
2757
|
// Sitemap — emitted by Dogsbay (not @astrojs/sitemap) into
|
|
2643
2758
|
// public/<basePath>/sitemap-{index,0}.xml so multi-mount deploys
|
|
@@ -2678,7 +2793,9 @@ export function emitAgentReadinessFiles(pages, outputNav, outputDir, siteName, o
|
|
|
2678
2793
|
const mdMirrorOn = options.mdMirror !== false && !staticDeploy;
|
|
2679
2794
|
const knownVersions = pageVersions(pages);
|
|
2680
2795
|
const knownLocales = pageLocales(pages);
|
|
2681
|
-
|
|
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);
|
|
2682
2799
|
const localeRedirectOn = options.defaultLocale !== undefined && knownLocales.length >= 2;
|
|
2683
2800
|
const axisRedirectOn = versionRedirectOn || localeRedirectOn;
|
|
2684
2801
|
const middlewarePath = join(outputDir, "src", "middleware.ts");
|
|
@@ -2790,104 +2907,25 @@ function serializePageMd(page) {
|
|
|
2790
2907
|
* opt-in.
|
|
2791
2908
|
*/
|
|
2792
2909
|
function buildRobotsTxt(options, hasSiteUrl) {
|
|
2793
|
-
|
|
2794
|
-
|
|
2795
|
-
|
|
2796
|
-
|
|
2797
|
-
//
|
|
2798
|
-
// index under <basePath>/, so robots.txt must point there too.
|
|
2799
|
-
// (Multi-mount deploys end up with one robots.txt per site at
|
|
2800
|
-
// their respective hosts / paths; each correctly references its
|
|
2801
|
-
// own mount's sitemap-index.)
|
|
2802
|
-
// Sitemap URL = origin + combined + /sitemap-index.xml. Use the
|
|
2803
|
-
// origin (no path) from site.url and the combined prefix (urlBase
|
|
2804
|
-
// + basePath); siteUrl could itself include a path component when
|
|
2805
|
-
// hosting on a subpath (GH Pages project page), so we strip it
|
|
2806
|
-
// 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.
|
|
2807
2915
|
const { origin } = parseSiteUrl(options.siteUrl);
|
|
2808
2916
|
const combined = combinedPrefix(options);
|
|
2809
|
-
const
|
|
2810
|
-
|
|
2811
|
-
:
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
|
|
2815
|
-
|
|
2816
|
-
|
|
2817
|
-
|
|
2818
|
-
|
|
2819
|
-
|
|
2820
|
-
|
|
2821
|
-
// unknown directives, but Lighthouse's validator reports this one as
|
|
2822
|
-
// `Unknown directive` and fails the "robots.txt is valid" audit —
|
|
2823
|
-
// measured: SEO 100 -> 92 on every page. Nothing consumes the line
|
|
2824
|
-
// either; `Llms-Txt:` is not part of the llms.txt proposal, and
|
|
2825
|
-
// agents find the file at its well-known path. A measured penalty
|
|
2826
|
-
// for a speculative benefit is not a sensible default.
|
|
2827
|
-
//
|
|
2828
|
-
// `llms.txt` itself is still emitted; this controls only the pointer.
|
|
2829
|
-
const llmsTxt = options.llmsTxtDirective === true &&
|
|
2830
|
-
options.llmsTxt !== false &&
|
|
2831
|
-
hasSiteUrl &&
|
|
2832
|
-
origin
|
|
2833
|
-
? `Llms-Txt: ${origin}${withBasePath(combined, "/llms.txt")}\n`
|
|
2834
|
-
: "";
|
|
2835
|
-
return `${contentSignalPreamble(options)}User-agent: *\nAllow: /\n${contentSignal}${sitemap}${llmsTxt}`;
|
|
2836
|
-
}
|
|
2837
|
-
/**
|
|
2838
|
-
* The Article 4 reservation-of-rights preamble, as robots.txt comments.
|
|
2839
|
-
*
|
|
2840
|
-
* Opt-in via `agent.contentSignal.preamble`. Default off: it is 25
|
|
2841
|
-
* lines of legal text on every site, and it asserts a legal position
|
|
2842
|
-
* that is the operator's to take, not ours to take for them.
|
|
2843
|
-
*
|
|
2844
|
-
* Why it is worth having at all — `Content-Signal: ai-train=no` on its
|
|
2845
|
-
* own is a preference. Article 4(3) of EU Directive 2019/790 makes the
|
|
2846
|
-
* text-and-data-mining exception conditional on rights not having been
|
|
2847
|
-
* "expressly reserved in an appropriate manner", including
|
|
2848
|
-
* machine-readable means. The preamble is what turns the signal into
|
|
2849
|
-
* that express reservation, which is the difference between a request
|
|
2850
|
-
* and a reservation with legal effect in the EU.
|
|
2851
|
-
*
|
|
2852
|
-
* The wording is Cloudflare's Content Signals Policy text, which is
|
|
2853
|
-
* published under CC0 precisely so it can be reproduced. It is
|
|
2854
|
-
* reproduced rather than linked because a reservation has to be present
|
|
2855
|
-
* where the machine reads it.
|
|
2856
|
-
*
|
|
2857
|
-
* Kept BYTE-IDENTICAL to the published policy on purpose. It is a legal
|
|
2858
|
-
* instrument, not prose we own: reword it and a site is asserting
|
|
2859
|
-
* something subtly different from what its operator believes it is
|
|
2860
|
-
* asserting. `tests/robots-preamble.test.ts` pins the text.
|
|
2861
|
-
*/
|
|
2862
|
-
function contentSignalPreamble(options) {
|
|
2863
|
-
if (options.contentSignalPreamble !== true)
|
|
2864
|
-
return "";
|
|
2865
|
-
return `# As a condition of accessing this website, you agree to abide by the following
|
|
2866
|
-
# content signals:
|
|
2867
|
-
|
|
2868
|
-
# (a) If a Content-Signal = yes, you may collect content for the corresponding
|
|
2869
|
-
# use.
|
|
2870
|
-
# (b) If a Content-Signal = no, you may not collect content for the
|
|
2871
|
-
# corresponding use.
|
|
2872
|
-
# (c) If the website operator does not include a Content-Signal for a
|
|
2873
|
-
# corresponding use, the website operator neither grants nor restricts
|
|
2874
|
-
# permission via Content-Signal with respect to the corresponding use.
|
|
2875
|
-
|
|
2876
|
-
# The content signals and their meanings are:
|
|
2877
|
-
|
|
2878
|
-
# search: building a search index and providing search results (e.g., returning
|
|
2879
|
-
# hyperlinks and short excerpts from your website's contents). Search does not
|
|
2880
|
-
# include providing AI-generated search summaries.
|
|
2881
|
-
# ai-input: inputting content into one or more AI models (e.g., retrieval
|
|
2882
|
-
# augmented generation, grounding, or other real-time taking of content for
|
|
2883
|
-
# generative AI search answers).
|
|
2884
|
-
# ai-train: training or fine-tuning AI models.
|
|
2885
|
-
|
|
2886
|
-
# ANY RESTRICTIONS EXPRESSED VIA CONTENT SIGNALS ARE EXPRESS RESERVATIONS OF
|
|
2887
|
-
# RIGHTS UNDER ARTICLE 4 OF THE EUROPEAN UNION DIRECTIVE 2019/790 ON COPYRIGHT
|
|
2888
|
-
# AND RELATED RIGHTS IN THE DIGITAL SINGLE MARKET.
|
|
2889
|
-
|
|
2890
|
-
`;
|
|
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
|
+
});
|
|
2891
2929
|
}
|
|
2892
2930
|
/**
|
|
2893
2931
|
* Build the contents of `public/_headers` (Cloudflare Pages / Workers
|
|
@@ -2930,14 +2968,6 @@ export function faviconHref(outputDir, urlBase) {
|
|
|
2930
2968
|
}
|
|
2931
2969
|
return false;
|
|
2932
2970
|
}
|
|
2933
|
-
function buildHeadersFile(basePath) {
|
|
2934
|
-
const llmsHref = withBasePath(basePath, "/llms.txt");
|
|
2935
|
-
return [
|
|
2936
|
-
"/*",
|
|
2937
|
-
` Link: <${llmsHref}>; rel="describedby"; type="text/plain"`,
|
|
2938
|
-
"",
|
|
2939
|
-
].join("\n");
|
|
2940
|
-
}
|
|
2941
2971
|
function buildMiddlewareSource(config) {
|
|
2942
2972
|
const lines = [
|
|
2943
2973
|
"// AUTO-GENERATED by `dogsbay site build` — do not edit.",
|
|
@@ -3078,7 +3108,13 @@ function emitLlmsTxtFiles(outputDir, siteName, options, nav, pages) {
|
|
|
3078
3108
|
continue;
|
|
3079
3109
|
let relDir;
|
|
3080
3110
|
if (combinedSegs && dir === combinedSegs) {
|
|
3081
|
-
|
|
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;
|
|
3082
3118
|
}
|
|
3083
3119
|
else if (combinedSegs && dir.startsWith(`${combinedSegs}/`)) {
|
|
3084
3120
|
relDir = dir.slice(combinedSegs.length + 1);
|
|
@@ -3089,9 +3125,7 @@ function emitLlmsTxtFiles(outputDir, siteName, options, nav, pages) {
|
|
|
3089
3125
|
// back to the raw value rather than rooting at /.
|
|
3090
3126
|
relDir = dir;
|
|
3091
3127
|
}
|
|
3092
|
-
const sectionPath = relDir
|
|
3093
|
-
? join(mountDir, relDir, "llms.txt")
|
|
3094
|
-
: join(mountDir, "llms.txt");
|
|
3128
|
+
const sectionPath = join(mountDir, relDir, "llms.txt");
|
|
3095
3129
|
mkdirSync(dirname(sectionPath), { recursive: true });
|
|
3096
3130
|
writeFileSync(sectionPath, buildSectionLlmsTxt(siteConfig, group, pages, { hrefPrefix }));
|
|
3097
3131
|
}
|
|
@@ -3112,7 +3146,7 @@ function emitSitemapFiles(outputDir, options, pages) {
|
|
|
3112
3146
|
// Filesystem path uses basePath (sitemap files live in
|
|
3113
3147
|
// public/<basePath>/sitemap-*.xml). The URL prefix encoded into
|
|
3114
3148
|
// each <loc> uses combined so the absolute URLs resolve under the
|
|
3115
|
-
// host's served subpath.
|
|
3149
|
+
// host's served subpath. buildSitemapFiles strips path off siteUrl
|
|
3116
3150
|
// internally, so passing siteUrl + combined as basePath gives
|
|
3117
3151
|
// origin + combined as the final URL.
|
|
3118
3152
|
const basePath = normalizeBasePath(options.basePath);
|
|
@@ -3120,14 +3154,28 @@ function emitSitemapFiles(outputDir, options, pages) {
|
|
|
3120
3154
|
const baseSegments = basePathSegments(basePath);
|
|
3121
3155
|
const mountDir = join(outputDir, "public", ...baseSegments);
|
|
3122
3156
|
mkdirSync(mountDir, { recursive: true });
|
|
3123
|
-
|
|
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), {
|
|
3124
3169
|
siteUrl: options.siteUrl,
|
|
3125
3170
|
basePath: combined,
|
|
3126
3171
|
siteNoindex: options.noindex === true,
|
|
3127
|
-
})
|
|
3172
|
+
});
|
|
3173
|
+
for (const [name, xml] of files)
|
|
3174
|
+
writeFileSync(join(mountDir, name), xml);
|
|
3128
3175
|
writeFileSync(join(mountDir, "sitemap-index.xml"), buildSitemapIndex({
|
|
3129
3176
|
siteUrl: options.siteUrl,
|
|
3130
3177
|
basePath: combined,
|
|
3178
|
+
sitemapCount: files.length,
|
|
3131
3179
|
extraSitemaps: (options.aggregates ?? [])
|
|
3132
3180
|
.map((a) => a.sitemap)
|
|
3133
3181
|
.filter((u) => !!u),
|
|
@@ -3215,29 +3263,50 @@ function axisSubtreeRoots(pages) {
|
|
|
3215
3263
|
}
|
|
3216
3264
|
return roots;
|
|
3217
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
|
+
}
|
|
3218
3277
|
/**
|
|
3219
3278
|
* First nav href (in nav order) living under any of `prefixes`, which
|
|
3220
3279
|
* are URL paths without a trailing slash. Used to pick a redirect
|
|
3221
3280
|
* target for a subtree root that has no page of its own.
|
|
3222
3281
|
*/
|
|
3223
|
-
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 = []) {
|
|
3224
3285
|
for (const item of items) {
|
|
3225
3286
|
const href = item.href;
|
|
3226
|
-
if (href &&
|
|
3287
|
+
if (href &&
|
|
3288
|
+
prefixes.some((prefix) => href.startsWith(`${prefix}/`)) &&
|
|
3289
|
+
!exclude.some((x) => href === x || href.startsWith(`${x}/`))) {
|
|
3227
3290
|
return href;
|
|
3228
3291
|
}
|
|
3229
3292
|
if (item.children) {
|
|
3230
|
-
const found = findFirstNavHrefUnder(item.children, prefixes);
|
|
3293
|
+
const found = findFirstNavHrefUnder(item.children, prefixes, exclude);
|
|
3231
3294
|
if (found)
|
|
3232
3295
|
return found;
|
|
3233
3296
|
}
|
|
3234
3297
|
}
|
|
3235
3298
|
return undefined;
|
|
3236
3299
|
}
|
|
3237
|
-
|
|
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) {
|
|
3238
3307
|
const componentsSource = resolveComponentsSource();
|
|
3239
3308
|
if (!componentsSource)
|
|
3240
|
-
return;
|
|
3309
|
+
return [];
|
|
3241
3310
|
// Components copied into the generated project. These are the ones
|
|
3242
3311
|
// format-astro's serializer emits imports for, plus a few used by the
|
|
3243
3312
|
// project shell (DocsLayout, pagination, theme-toggle).
|
|
@@ -3264,14 +3333,19 @@ function copyComponents(outputDir) {
|
|
|
3264
3333
|
// inline `:icon[name]` directive. Without this entry every page
|
|
3265
3334
|
// emitting the icon import 500s with "module not found".
|
|
3266
3335
|
"icon",
|
|
3336
|
+
// Inline :kbd and the :::files tree.
|
|
3337
|
+
"kbd", "files",
|
|
3267
3338
|
];
|
|
3339
|
+
const added = [];
|
|
3268
3340
|
for (const name of needed) {
|
|
3269
3341
|
const src = join(componentsSource, name);
|
|
3270
3342
|
const dest = join(outputDir, "src", "components", "ui", name);
|
|
3271
3343
|
if (existsSync(src) && !existsSync(dest)) {
|
|
3272
3344
|
cpSync(src, dest, { recursive: true });
|
|
3345
|
+
added.push(name);
|
|
3273
3346
|
}
|
|
3274
3347
|
}
|
|
3348
|
+
return added;
|
|
3275
3349
|
}
|
|
3276
3350
|
/** Media files worth carrying from a `mediaOnly` asset mount. */
|
|
3277
3351
|
const MOUNT_MEDIA_EXTS = new Set([
|
|
@@ -3364,630 +3438,8 @@ function copyAssets(sourceDir, outputDir, imageOptimization, urlPrefix) {
|
|
|
3364
3438
|
}
|
|
3365
3439
|
catch { /* source may not exist */ }
|
|
3366
3440
|
}
|
|
3367
|
-
// ── CSS generation (ported from import-mkdocs.ts) ───────
|
|
3368
|
-
/**
|
|
3369
|
-
* Build the `@source inline("...")` directive that pins the
|
|
3370
|
-
* grid-tone palette into the generated stylesheet.
|
|
3371
|
-
*
|
|
3372
|
-
* Why we need it: tone classes like `bg-primary/10` only appear in
|
|
3373
|
-
* `.astro` pages emitted by Dogsbay's grid-item serializer. When
|
|
3374
|
-
* Tailwind's content scanner doesn't pick them up — because the
|
|
3375
|
-
* page lives outside the default scan globs, or because a class
|
|
3376
|
-
* is composed at the boundary of an interpolation — they get
|
|
3377
|
-
* purged. Result observed in dogsbay-docs-markdown audit: half
|
|
3378
|
-
* the grid demo cells render with no background. Pinning forces
|
|
3379
|
-
* generation regardless of scanner reach.
|
|
3380
|
-
*
|
|
3381
|
-
* Single source of truth: derived from TONE_CLASSES so any new
|
|
3382
|
-
* tone added to the palette is automatically safelisted.
|
|
3383
|
-
*/
|
|
3384
|
-
function buildToneSafelist() {
|
|
3385
|
-
const seen = new Set();
|
|
3386
|
-
for (const classes of Object.values(TONE_CLASSES)) {
|
|
3387
|
-
for (const cls of classes.split(/\s+/)) {
|
|
3388
|
-
if (cls)
|
|
3389
|
-
seen.add(cls);
|
|
3390
|
-
}
|
|
3391
|
-
}
|
|
3392
|
-
return [...seen].sort().join(" ");
|
|
3393
|
-
}
|
|
3394
|
-
function generateGlobalCss() {
|
|
3395
|
-
return `@import "tailwindcss";
|
|
3396
|
-
@import "./theme.css";
|
|
3397
|
-
|
|
3398
|
-
/* Custom elements have no user-agent style, so an unstyled one is
|
|
3399
|
-
display:inline — on which w-full does nothing and a border paints
|
|
3400
|
-
as detached fragments rather than around the content. @dogsbay/elements
|
|
3401
|
-
injects these same defaults at runtime, which is what reaches EXISTING
|
|
3402
|
-
sites (global.css is scaffolded once and never rewritten). Having them
|
|
3403
|
-
here too means a new site is correct at CSS time, with no flash before
|
|
3404
|
-
the module script runs.
|
|
3405
|
-
|
|
3406
|
-
:where() keeps specificity at zero, so any utility class wins. */
|
|
3407
|
-
:where(db-tabs, db-accordion, db-collapsible, db-card, db-steps, db-code-block) {
|
|
3408
|
-
display: block;
|
|
3409
|
-
}
|
|
3410
|
-
:where(db-link-button) {
|
|
3411
|
-
display: inline-block;
|
|
3412
|
-
}
|
|
3413
|
-
|
|
3414
|
-
/* Scan @dogsbay packages for Tailwind classes */
|
|
3415
|
-
@source "../../node_modules/@dogsbay/ui/src";
|
|
3416
|
-
@source "../../node_modules/@dogsbay/docs-layout/src";
|
|
3417
|
-
|
|
3418
|
-
/* Pin the grid-tone palette. These classes are emitted into
|
|
3419
|
-
markdown-generated .astro pages by the grid-item serializer
|
|
3420
|
-
(TONE_CLASSES in @dogsbay/format-astro). Without inlining,
|
|
3421
|
-
opacity-modified utilities like bg-primary/10 get purged when
|
|
3422
|
-
Tailwind doesn't see them in the scanned globs, leaving grid
|
|
3423
|
-
demo cells with no visible background. */
|
|
3424
|
-
@source inline("${buildToneSafelist()}");
|
|
3425
|
-
|
|
3426
|
-
/* Prose typography for rendered content.
|
|
3427
|
-
Every element rule is guarded with :not(.dba-api *): API-reference
|
|
3428
|
-
regions (ApiLayout root carries the dba-api marker) compose their own
|
|
3429
|
-
typography from utility classes, and unguarded .docs-prose rules would
|
|
3430
|
-
out-specify them. The guard lets mixed prose+endpoint pages (MDX
|
|
3431
|
-
imports) keep full typography outside the cards. */
|
|
3432
|
-
.docs-prose {
|
|
3433
|
-
line-height: 1.7;
|
|
3434
|
-
|
|
3435
|
-
& 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; }
|
|
3436
|
-
& 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; }
|
|
3437
|
-
& 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; }
|
|
3438
|
-
& h4:not(.dba-api *) { font-family: var(--font-heading); font-size: 1.1rem; font-weight: 600; margin-top: 1.5rem; margin-bottom: 0.5rem; }
|
|
3439
|
-
|
|
3440
|
-
& p:not(.dba-api *) { margin-top: 0.75rem; margin-bottom: 0.75rem; }
|
|
3441
|
-
|
|
3442
|
-
/* The :not([data-variant]) guard exempts anchors that ARE components. A
|
|
3443
|
-
link-button renders as an <a> carrying bg-primary + text-primary-foreground
|
|
3444
|
-
and a data-variant attribute, and this rule repainted its text with
|
|
3445
|
-
var(--primary) — primary on primary: an unreadable solid block in light
|
|
3446
|
-
mode, a blank one in dark. The secondary variant survived only because its
|
|
3447
|
-
background is light enough to read primary-coloured text, which is why it
|
|
3448
|
-
looked like a colour bug in one button rather than a cascade bug in both.
|
|
3449
|
-
(No backticks in this comment: the whole stylesheet is a template
|
|
3450
|
-
literal.) */
|
|
3451
|
-
& a:not(.dba-api *):not([data-variant]) { color: var(--primary); text-decoration: underline; text-underline-offset: 2px; }
|
|
3452
|
-
& a:hover:not(.dba-api *):not([data-variant]) { opacity: 0.8; }
|
|
3453
|
-
|
|
3454
|
-
& strong:not(.dba-api *) { font-weight: 600; }
|
|
3455
|
-
& 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); }
|
|
3456
|
-
& pre code:not(.dba-api *) { padding: 0; background: none; font-size: 1em; }
|
|
3457
|
-
|
|
3458
|
-
/* Spacing between consecutive block elements (code blocks, alerts, etc.) */
|
|
3459
|
-
& > * + * { margin-top: 0.75rem; }
|
|
3460
|
-
& li > * + *:not(.dba-api *) { margin-top: 0.75rem; }
|
|
3461
|
-
|
|
3462
|
-
& ul:not(.dba-api *) { list-style: disc; padding-left: 1.5rem; margin: 0.75rem 0; }
|
|
3463
|
-
& ol:not(.dba-api *) { list-style: decimal; padding-left: 1.5rem; margin: 0.75rem 0; }
|
|
3464
|
-
& li:not(.dba-api *) { margin: 0.25rem 0; }
|
|
3465
|
-
& li > ul:not(.dba-api *), & li > ol:not(.dba-api *) { margin: 0.25rem 0; }
|
|
3466
|
-
|
|
3467
|
-
& blockquote:not(.dba-api *) { border-left: 4px solid var(--border); padding-left: 1rem; color: var(--muted-foreground); font-style: italic; margin: 1rem 0; }
|
|
3468
|
-
|
|
3469
|
-
& hr:not(.dba-api *) { border: none; border-top: 1px solid var(--border); margin: 2rem 0; }
|
|
3470
|
-
|
|
3471
|
-
& table:not(.dba-api *) { width: 100%; border-collapse: collapse; margin: 1rem 0; font-size: 0.875rem; }
|
|
3472
|
-
& th:not(.dba-api *) { text-align: left; vertical-align: bottom; font-weight: 600; padding: 0.5rem; border-bottom: 2px solid var(--border); }
|
|
3473
|
-
& td:not(.dba-api *) { padding: 0.5rem; vertical-align: top; border-bottom: 1px solid var(--border); }
|
|
3474
|
-
|
|
3475
|
-
/* Tailwind Preflight sets display: block on every img, which turns an
|
|
3476
|
-
INLINE icon into its own line and splits the sentence around it —
|
|
3477
|
-
AsciiDoc UI macros render mid-step and were breaking every procedure
|
|
3478
|
-
that used one. inline-block restores the flow and leaves a standalone
|
|
3479
|
-
image (the only child of its paragraph) looking exactly as before. */
|
|
3480
|
-
& img:not(.dba-api *) { display: inline-block; max-width: 100%; border-radius: 0.5rem; }
|
|
3481
|
-
/* A standalone image — the only child of its paragraph — keeps block
|
|
3482
|
-
layout, so the vertical rhythm and the absence of baseline descender
|
|
3483
|
-
space are exactly what they were before inline icons were fixed. */
|
|
3484
|
-
& p > img:only-child:not(.dba-api *) { display: block; }
|
|
3485
|
-
|
|
3486
|
-
& .heading-anchor { text-decoration: none; opacity: 0; margin-right: 0.25rem; transition: opacity 0.2s; }
|
|
3487
|
-
& h1:hover .heading-anchor, & h2:hover .heading-anchor, & h3:hover .heading-anchor, & h4:hover .heading-anchor { opacity: 0.4; }
|
|
3488
|
-
|
|
3489
|
-
& details:not(.dba-api *) { border: 1px solid var(--border); border-radius: 0.5rem; padding: 1rem; margin: 1rem 0; }
|
|
3490
|
-
& summary:not(.dba-api *) { cursor: pointer; font-weight: 600; }
|
|
3491
|
-
|
|
3492
|
-
/* mark is emitted by the highlight directive AND can arrive as raw HTML
|
|
3493
|
-
from an importer. Without a rule here it renders as the browser default
|
|
3494
|
-
yellow, which clashes in light mode and is unreadable on a dark page
|
|
3495
|
-
(nothing declares color-scheme). Token-based so it follows the theme. */
|
|
3496
|
-
& 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; }
|
|
3497
|
-
&:where(.dark *) mark:not(.dba-api *), .dark & mark:not(.dba-api *) { background: var(--dsb-mark-bg-dark, rgb(234 179 8 / 0.3)); }
|
|
3498
|
-
|
|
3499
|
-
& dl:not(.dba-api *) { margin: 1rem 0; }
|
|
3500
|
-
& dt:not(.dba-api *) { font-weight: 600; margin-top: 0.75rem; }
|
|
3501
|
-
& dd:not(.dba-api *) { margin-left: 1.5rem; color: var(--muted-foreground); }
|
|
3502
|
-
|
|
3503
|
-
/* In-cell admonitions. Inside a raw HTML table cell nothing
|
|
3504
|
-
markdown-native works, so the AsciiDoc engine emits a neutral
|
|
3505
|
-
dl.db-admonition shape (DD-010) and this rule renders it as a compact
|
|
3506
|
-
callout box — same contract the Obsidian plugin styles. Per-type
|
|
3507
|
-
accent from theme tokens; overrides the generic dl/dt/dd rules above. */
|
|
3508
|
-
& dl.db-admonition {
|
|
3509
|
-
margin: 0.75rem 0;
|
|
3510
|
-
padding: 0.5rem 0.75rem;
|
|
3511
|
-
border-left: 3px solid var(--info);
|
|
3512
|
-
border-radius: 0.25rem;
|
|
3513
|
-
background: color-mix(in oklab, var(--info) 8%, transparent);
|
|
3514
|
-
}
|
|
3515
|
-
/* The LABEL blends the accent toward the foreground; the border and the
|
|
3516
|
-
tint keep the accent at full strength, so the callout still reads as
|
|
3517
|
-
its colour while the text stays legible.
|
|
3518
|
-
|
|
3519
|
-
Measured from painted pixels on a real page, light mode, 13px/600 —
|
|
3520
|
-
the accent used raw is a genuine AA failure, not one of axe's oklch
|
|
3521
|
-
misreads:
|
|
3522
|
-
|
|
3523
|
-
--info raw 2.18:1 mixed 5.66:1
|
|
3524
|
-
--warning raw 2.25:1 mixed 5.74:1
|
|
3525
|
-
--destructive raw 5.97:1 mixed 10.98:1
|
|
3526
|
-
|
|
3527
|
-
Mixed with var(--foreground), not black, on purpose. Black fixes
|
|
3528
|
-
light and BREAKS dark: 70% black measures 5.21:1 light but 3.25:1
|
|
3529
|
-
dark, because the tint it sits on is dark too. --foreground is
|
|
3530
|
-
near-black in light and near-white in dark, so one rule serves both
|
|
3531
|
-
(dark measures 10.96 / 10.87 / 9.79). A fixed colour used in both
|
|
3532
|
-
themes is a bug waiting. */
|
|
3533
|
-
& dl.db-admonition > dt {
|
|
3534
|
-
margin-top: 0;
|
|
3535
|
-
font-weight: 600;
|
|
3536
|
-
font-size: 0.8125rem;
|
|
3537
|
-
text-transform: uppercase;
|
|
3538
|
-
letter-spacing: 0.03em;
|
|
3539
|
-
color: color-mix(in oklab, var(--info) 60%, var(--foreground));
|
|
3540
|
-
}
|
|
3541
|
-
& dl.db-admonition > dd { margin-left: 0; margin-top: 0.25rem; color: var(--foreground); }
|
|
3542
|
-
& dl.db-admonition-warning, & dl.db-admonition-caution {
|
|
3543
|
-
border-left-color: var(--warning);
|
|
3544
|
-
background: color-mix(in oklab, var(--warning) 8%, transparent);
|
|
3545
|
-
}
|
|
3546
|
-
& dl.db-admonition-warning > dt, & dl.db-admonition-caution > dt { color: color-mix(in oklab, var(--warning) 60%, var(--foreground)); }
|
|
3547
|
-
& dl.db-admonition-important {
|
|
3548
|
-
border-left-color: var(--destructive);
|
|
3549
|
-
background: color-mix(in oklab, var(--destructive) 8%, transparent);
|
|
3550
|
-
}
|
|
3551
|
-
& dl.db-admonition-important > dt { color: color-mix(in oklab, var(--destructive) 60%, var(--foreground)); }
|
|
3552
|
-
|
|
3553
|
-
/* Granularity book-view affordances (plans/granularity-views.md):
|
|
3554
|
-
"Read as single page →" on topics, "Open as page ↗" on book sections.
|
|
3555
|
-
Deliberately subtle/muted — the book has one per section. */
|
|
3556
|
-
& .dsb-read-single-page {
|
|
3557
|
-
display: inline-block;
|
|
3558
|
-
margin: 0 0 1.25rem;
|
|
3559
|
-
font-size: 0.8125rem;
|
|
3560
|
-
color: var(--muted-foreground);
|
|
3561
|
-
text-decoration: none;
|
|
3562
|
-
}
|
|
3563
|
-
& .dsb-read-single-page:hover { color: var(--foreground); text-decoration: underline; }
|
|
3564
|
-
& .dsb-open-as-page { margin: -0.25rem 0 0.5rem; font-size: 0.75rem; }
|
|
3565
|
-
& .dsb-open-as-page a { color: var(--muted-foreground); text-decoration: none; }
|
|
3566
|
-
& .dsb-open-as-page a:hover { color: var(--foreground); text-decoration: underline; }
|
|
3567
|
-
|
|
3568
|
-
/* AsciiDoc block roles ([role="x"] / [.x]) preserved as classes (#384).
|
|
3569
|
-
_abstract marks a module's lead/intro paragraph; .lead/.small are
|
|
3570
|
-
Asciidoctor built-ins; _additional-resources marks the trailing
|
|
3571
|
-
"Additional resources" section heading. */
|
|
3572
|
-
& ._abstract, & .abstract, & .lead {
|
|
3573
|
-
font-size: 1.05rem;
|
|
3574
|
-
line-height: 1.6;
|
|
3575
|
-
color: var(--muted-foreground);
|
|
3576
|
-
}
|
|
3577
|
-
& .small { font-size: 0.875rem; }
|
|
3578
|
-
& h2._additional-resources, & h3._additional-resources {
|
|
3579
|
-
font-size: 1.15rem;
|
|
3580
|
-
margin-top: 2rem;
|
|
3581
|
-
}
|
|
3582
|
-
|
|
3583
|
-
/* Related resources (#384 R3): [role="_additional-resources"] + .Title +
|
|
3584
|
-
list folds to a <section class="related-resources"> (→ DITA related-links). */
|
|
3585
|
-
& .related-resources {
|
|
3586
|
-
margin-top: 2rem;
|
|
3587
|
-
padding-top: 1rem;
|
|
3588
|
-
border-top: 1px solid var(--border);
|
|
3589
|
-
}
|
|
3590
|
-
& .related-resources-title {
|
|
3591
|
-
font-weight: 600;
|
|
3592
|
-
margin: 0 0 0.5rem;
|
|
3593
|
-
}
|
|
3594
|
-
}
|
|
3595
|
-
|
|
3596
|
-
@utility scrollbar-none {
|
|
3597
|
-
-ms-overflow-style: none;
|
|
3598
|
-
scrollbar-width: none;
|
|
3599
|
-
&::-webkit-scrollbar {
|
|
3600
|
-
display: none;
|
|
3601
|
-
}
|
|
3602
|
-
}
|
|
3603
|
-
`;
|
|
3604
|
-
}
|
|
3605
|
-
// ── Theme presets ───────────────────────────────────────
|
|
3606
|
-
const THEME_DEFAULT = {
|
|
3607
|
-
light: `:root {
|
|
3608
|
-
--background: oklch(1 0 0);
|
|
3609
|
-
--foreground: oklch(0.145 0.014 285.823);
|
|
3610
|
-
--card: oklch(1 0 0);
|
|
3611
|
-
--card-foreground: oklch(0.145 0.014 285.823);
|
|
3612
|
-
--popover: oklch(1 0 0);
|
|
3613
|
-
--popover-foreground: oklch(0.145 0.014 285.823);
|
|
3614
|
-
--primary: oklch(0.205 0.014 285.823);
|
|
3615
|
-
--primary-foreground: oklch(0.985 0 0);
|
|
3616
|
-
--secondary: oklch(0.965 0.001 286.375);
|
|
3617
|
-
--secondary-foreground: oklch(0.205 0.014 285.823);
|
|
3618
|
-
--muted: oklch(0.965 0.001 286.375);
|
|
3619
|
-
--muted-foreground: oklch(0.45 0.014 285.938);
|
|
3620
|
-
--accent: oklch(0.965 0.001 286.375);
|
|
3621
|
-
--accent-foreground: oklch(0.205 0.014 285.823);
|
|
3622
|
-
--destructive: oklch(0.45 0.245 27.325);
|
|
3623
|
-
--destructive-foreground: oklch(0.985 0 0);
|
|
3624
|
-
--border: oklch(0.922 0.004 286.32);
|
|
3625
|
-
--input: oklch(0.922 0.004 286.32);
|
|
3626
|
-
--ring: oklch(0.708 0.014 285.823);
|
|
3627
|
-
--radius: 0.625rem;
|
|
3628
|
-
--note: oklch(0.637 0.174 259.126);
|
|
3629
|
-
--note-foreground: var(--foreground);
|
|
3630
|
-
--abstract: oklch(0.691 0.148 221.723);
|
|
3631
|
-
--info: oklch(0.722 0.128 207.084);
|
|
3632
|
-
--tip: oklch(0.731 0.112 178.627);
|
|
3633
|
-
--success: oklch(0.754 0.175 149.577);
|
|
3634
|
-
--question: oklch(0.809 0.19 126.526);
|
|
3635
|
-
--warning: oklch(0.74 0.175 62.778);
|
|
3636
|
-
--failure: oklch(0.614 0.194 21.602);
|
|
3637
|
-
--danger: oklch(0.577 0.245 16.439);
|
|
3638
|
-
--bug: oklch(0.548 0.258 349.761);
|
|
3639
|
-
--example: oklch(0.534 0.222 286.033);
|
|
3640
|
-
--quote: oklch(0.65 0 0);
|
|
3641
|
-
--sidebar: oklch(0.985 0 0);
|
|
3642
|
-
--sidebar-foreground: oklch(0.145 0.014 285.823);
|
|
3643
|
-
--sidebar-primary: oklch(0.205 0.014 285.823);
|
|
3644
|
-
--sidebar-primary-foreground: oklch(0.985 0 0);
|
|
3645
|
-
--sidebar-accent: oklch(0.965 0.001 286.375);
|
|
3646
|
-
--sidebar-accent-foreground: oklch(0.205 0.014 285.823);
|
|
3647
|
-
--sidebar-border: oklch(0.922 0.004 286.32);
|
|
3648
|
-
--sidebar-ring: oklch(0.708 0.014 285.823);
|
|
3649
|
-
--code-background: oklch(0.965 0.004 264);
|
|
3650
|
-
--code-foreground: oklch(0.3 0.014 253.833);
|
|
3651
|
-
|
|
3652
|
-
/* API reference — always-dark code panel */
|
|
3653
|
-
--api-panel-bg: oklch(0.145 0.014 253.833);
|
|
3654
|
-
--api-panel-fg: oklch(0.926 0.013 253.833);
|
|
3655
|
-
|
|
3656
|
-
/* API semantic colors — HTTP methods, types, status codes */
|
|
3657
|
-
/* Light mode: darkened for WCAG AA on the 20%/10% tints. See .dark. */
|
|
3658
|
-
--api-get: oklch(0.435 0.095 162);
|
|
3659
|
-
--api-post: oklch(0.45 0.174 259);
|
|
3660
|
-
--api-put: oklch(0.455 0.105 63);
|
|
3661
|
-
--api-patch: oklch(0.455 0.13 45);
|
|
3662
|
-
--api-delete: oklch(0.45 0.179 22);
|
|
3663
|
-
--api-head: oklch(0.47 0.222 286);
|
|
3664
|
-
--api-required: oklch(0.45 0.179 22);
|
|
3665
|
-
--api-deprecated: oklch(0.455 0.105 63);
|
|
3666
|
-
--api-type-string: oklch(0.435 0.095 162);
|
|
3667
|
-
--api-type-number: oklch(0.45 0.174 259);
|
|
3668
|
-
--api-type-boolean: oklch(0.455 0.105 63);
|
|
3669
|
-
--api-type-object: oklch(0.47 0.222 286);
|
|
3670
|
-
--api-type-array: oklch(0.455 0.13 45);
|
|
3671
|
-
--api-status-2xx: oklch(0.435 0.095 162);
|
|
3672
|
-
--api-status-3xx: oklch(0.45 0.174 259);
|
|
3673
|
-
--api-status-4xx: oklch(0.455 0.105 63);
|
|
3674
|
-
--api-status-5xx: oklch(0.45 0.179 22);
|
|
3675
|
-
|
|
3676
|
-
--font-sans: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
|
3677
|
-
--font-heading: var(--font-sans);
|
|
3678
|
-
--font-code: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
|
|
3679
|
-
}`,
|
|
3680
|
-
dark: `.dark {
|
|
3681
|
-
--background: oklch(0.145 0.014 285.823);
|
|
3682
|
-
--foreground: oklch(0.985 0 0);
|
|
3683
|
-
--card: oklch(0.145 0.014 285.823);
|
|
3684
|
-
--card-foreground: oklch(0.985 0 0);
|
|
3685
|
-
--popover: oklch(0.145 0.014 285.823);
|
|
3686
|
-
--popover-foreground: oklch(0.985 0 0);
|
|
3687
|
-
--primary: oklch(0.985 0 0);
|
|
3688
|
-
--primary-foreground: oklch(0.205 0.014 285.823);
|
|
3689
|
-
--secondary: oklch(0.269 0.007 286.033);
|
|
3690
|
-
--secondary-foreground: oklch(0.985 0 0);
|
|
3691
|
-
--muted: oklch(0.269 0.007 286.033);
|
|
3692
|
-
--muted-foreground: oklch(0.708 0.014 285.823);
|
|
3693
|
-
--accent: oklch(0.269 0.007 286.033);
|
|
3694
|
-
--accent-foreground: oklch(0.985 0 0);
|
|
3695
|
-
--destructive: oklch(0.704 0.191 22.216);
|
|
3696
|
-
--destructive-foreground: oklch(0.145 0.014 285.823);
|
|
3697
|
-
--border: oklch(0.269 0.007 286.033);
|
|
3698
|
-
--input: oklch(0.269 0.007 286.033);
|
|
3699
|
-
--ring: oklch(0.439 0.01 286.375);
|
|
3700
|
-
--note-foreground: var(--foreground);
|
|
3701
|
-
--sidebar: oklch(0.205 0.014 285.823);
|
|
3702
|
-
--sidebar-foreground: oklch(0.985 0 0);
|
|
3703
|
-
--sidebar-primary: oklch(0.488 0.243 264.376);
|
|
3704
|
-
--sidebar-primary-foreground: oklch(0.985 0 0);
|
|
3705
|
-
--sidebar-accent: oklch(0.269 0.007 286.033);
|
|
3706
|
-
--sidebar-accent-foreground: oklch(0.985 0 0);
|
|
3707
|
-
--sidebar-border: oklch(0.269 0.007 286.033);
|
|
3708
|
-
--sidebar-ring: oklch(0.439 0.01 286.375);
|
|
3709
|
-
--code-background: oklch(0.18 0.014 253.833);
|
|
3710
|
-
--code-foreground: oklch(0.926 0.013 253.833);
|
|
3711
|
-
|
|
3712
|
-
/* API semantic colors — lightened for dark mode (AA on 20%/10% tints). */
|
|
3713
|
-
--api-get: oklch(0.720 0.135 162);
|
|
3714
|
-
--api-post: oklch(0.755 0.174 259);
|
|
3715
|
-
--api-put: oklch(0.745 0.15 63);
|
|
3716
|
-
--api-patch: oklch(0.725 0.18 45);
|
|
3717
|
-
--api-delete: oklch(0.805 0.194 22);
|
|
3718
|
-
--api-head: oklch(0.740 0.182 286);
|
|
3719
|
-
--api-required: oklch(0.805 0.194 22);
|
|
3720
|
-
--api-deprecated: oklch(0.745 0.15 63);
|
|
3721
|
-
--api-type-string: oklch(0.690 0.135 162);
|
|
3722
|
-
--api-type-number: oklch(0.725 0.174 259);
|
|
3723
|
-
--api-type-boolean: oklch(0.720 0.15 63);
|
|
3724
|
-
--api-type-object: oklch(0.740 0.182 286);
|
|
3725
|
-
--api-type-array: oklch(0.725 0.18 45);
|
|
3726
|
-
--api-status-2xx: oklch(0.720 0.135 162);
|
|
3727
|
-
--api-status-3xx: oklch(0.755 0.174 259);
|
|
3728
|
-
--api-status-4xx: oklch(0.770 0.15 63);
|
|
3729
|
-
--api-status-5xx: oklch(0.805 0.194 22);
|
|
3730
|
-
}`,
|
|
3731
|
-
};
|
|
3732
|
-
/**
|
|
3733
|
-
* Mintlify-inspired theme — clean, neutral palette matching modern doc sites.
|
|
3734
|
-
*
|
|
3735
|
-
* Extracted from Claude Code docs (Mintlify):
|
|
3736
|
-
* - White background, near-black text
|
|
3737
|
-
* - Serif display font for H1, sans-serif body
|
|
3738
|
-
* - Soft colored callout backgrounds (blue note, green tip, amber warning)
|
|
3739
|
-
* - Rounded callout borders (16px radius)
|
|
3740
|
-
* - Light gray code background, JetBrains Mono
|
|
3741
|
-
*/
|
|
3742
|
-
const THEME_MINTLIFY = {
|
|
3743
|
-
light: `:root {
|
|
3744
|
-
--background: oklch(0.993 0.005 95);
|
|
3745
|
-
--foreground: oklch(0.15 0.01 260);
|
|
3746
|
-
--card: oklch(0.993 0.005 95);
|
|
3747
|
-
--card-foreground: oklch(0.15 0.01 260);
|
|
3748
|
-
--popover: oklch(0.993 0.005 95);
|
|
3749
|
-
--popover-foreground: oklch(0.15 0.01 260);
|
|
3750
|
-
--primary: oklch(0.15 0.01 260);
|
|
3751
|
-
--primary-foreground: oklch(0.985 0 0);
|
|
3752
|
-
--secondary: oklch(0.97 0.002 260);
|
|
3753
|
-
--secondary-foreground: oklch(0.15 0.01 260);
|
|
3754
|
-
--muted: oklch(0.97 0.002 260);
|
|
3755
|
-
--muted-foreground: oklch(0.45 0.01 260);
|
|
3756
|
-
--accent: oklch(0.97 0.002 260);
|
|
3757
|
-
--accent-foreground: oklch(0.15 0.01 260);
|
|
3758
|
-
--destructive: oklch(0.55 0.22 25);
|
|
3759
|
-
--destructive-foreground: oklch(0.985 0 0);
|
|
3760
|
-
--border: oklch(0.91 0.004 260);
|
|
3761
|
-
--input: oklch(0.91 0.004 260);
|
|
3762
|
-
--ring: oklch(0.7 0.01 260);
|
|
3763
|
-
--radius: 1rem;
|
|
3764
|
-
/* Callout colors — soft pastel backgrounds matching Mintlify */
|
|
3765
|
-
--note: oklch(0.62 0.18 260);
|
|
3766
|
-
--note-foreground: var(--foreground);
|
|
3767
|
-
--abstract: oklch(0.65 0.14 220);
|
|
3768
|
-
--info: oklch(0.65 0.14 230);
|
|
3769
|
-
--tip: oklch(0.65 0.16 155);
|
|
3770
|
-
--success: oklch(0.65 0.16 155);
|
|
3771
|
-
--question: oklch(0.7 0.15 270);
|
|
3772
|
-
--warning: oklch(0.7 0.15 75);
|
|
3773
|
-
--failure: oklch(0.6 0.2 25);
|
|
3774
|
-
--danger: oklch(0.55 0.22 25);
|
|
3775
|
-
--bug: oklch(0.55 0.22 350);
|
|
3776
|
-
--example: oklch(0.55 0.2 285);
|
|
3777
|
-
--quote: oklch(0.55 0 0);
|
|
3778
|
-
/* Sidebar */
|
|
3779
|
-
--sidebar: oklch(0.993 0.005 95);
|
|
3780
|
-
--sidebar-foreground: oklch(0.15 0.01 260);
|
|
3781
|
-
--sidebar-primary: oklch(0.15 0.01 260);
|
|
3782
|
-
--sidebar-primary-foreground: oklch(0.985 0 0);
|
|
3783
|
-
--sidebar-accent: oklch(0.97 0.002 260);
|
|
3784
|
-
--sidebar-accent-foreground: oklch(0.15 0.01 260);
|
|
3785
|
-
--sidebar-border: oklch(0.93 0.004 260);
|
|
3786
|
-
--sidebar-ring: oklch(0.7 0.01 260);
|
|
3787
|
-
/* Code — light gray background, matching Mintlify's inline code */
|
|
3788
|
-
--code-background: oklch(0.96 0.003 260);
|
|
3789
|
-
--code-foreground: oklch(0.25 0.01 260);
|
|
3790
|
-
|
|
3791
|
-
/* API reference — always-dark code panel */
|
|
3792
|
-
--api-panel-bg: oklch(0.145 0.014 253.833);
|
|
3793
|
-
--api-panel-fg: oklch(0.926 0.013 253.833);
|
|
3794
|
-
|
|
3795
|
-
/* API semantic colors — HTTP methods, types, status codes */
|
|
3796
|
-
/* Light mode: darkened for WCAG AA on the 20%/10% tints. See .dark. */
|
|
3797
|
-
--api-get: oklch(0.435 0.095 162);
|
|
3798
|
-
--api-post: oklch(0.45 0.174 259);
|
|
3799
|
-
--api-put: oklch(0.455 0.105 63);
|
|
3800
|
-
--api-patch: oklch(0.455 0.13 45);
|
|
3801
|
-
--api-delete: oklch(0.45 0.179 22);
|
|
3802
|
-
--api-head: oklch(0.47 0.222 286);
|
|
3803
|
-
--api-required: oklch(0.45 0.179 22);
|
|
3804
|
-
--api-deprecated: oklch(0.455 0.105 63);
|
|
3805
|
-
--api-type-string: oklch(0.435 0.095 162);
|
|
3806
|
-
--api-type-number: oklch(0.45 0.174 259);
|
|
3807
|
-
--api-type-boolean: oklch(0.455 0.105 63);
|
|
3808
|
-
--api-type-object: oklch(0.47 0.222 286);
|
|
3809
|
-
--api-type-array: oklch(0.455 0.13 45);
|
|
3810
|
-
--api-status-2xx: oklch(0.435 0.095 162);
|
|
3811
|
-
--api-status-3xx: oklch(0.45 0.174 259);
|
|
3812
|
-
--api-status-4xx: oklch(0.455 0.105 63);
|
|
3813
|
-
--api-status-5xx: oklch(0.45 0.179 22);
|
|
3814
|
-
|
|
3815
|
-
/* Fonts — system sans for body, JetBrains Mono for code */
|
|
3816
|
-
--font-sans: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
|
3817
|
-
--font-heading: var(--font-sans);
|
|
3818
|
-
--font-code: "JetBrains Mono", "Fira Code", ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
|
3819
|
-
}`,
|
|
3820
|
-
dark: `.dark {
|
|
3821
|
-
--background: oklch(0.16 0.01 260);
|
|
3822
|
-
--foreground: oklch(0.93 0.005 260);
|
|
3823
|
-
--card: oklch(0.18 0.01 260);
|
|
3824
|
-
--card-foreground: oklch(0.93 0.005 260);
|
|
3825
|
-
--popover: oklch(0.18 0.01 260);
|
|
3826
|
-
--popover-foreground: oklch(0.93 0.005 260);
|
|
3827
|
-
--primary: oklch(0.93 0.005 260);
|
|
3828
|
-
--primary-foreground: oklch(0.16 0.01 260);
|
|
3829
|
-
--secondary: oklch(0.22 0.008 260);
|
|
3830
|
-
--secondary-foreground: oklch(0.93 0.005 260);
|
|
3831
|
-
--muted: oklch(0.22 0.008 260);
|
|
3832
|
-
--muted-foreground: oklch(0.65 0.01 260);
|
|
3833
|
-
--accent: oklch(0.22 0.008 260);
|
|
3834
|
-
--accent-foreground: oklch(0.93 0.005 260);
|
|
3835
|
-
--destructive: oklch(0.65 0.2 25);
|
|
3836
|
-
--destructive-foreground: oklch(0.16 0.01 260);
|
|
3837
|
-
--border: oklch(0.28 0.008 260);
|
|
3838
|
-
--input: oklch(0.28 0.008 260);
|
|
3839
|
-
--ring: oklch(0.45 0.01 260);
|
|
3840
|
-
--note-foreground: var(--foreground);
|
|
3841
|
-
--sidebar: oklch(0.16 0.01 260);
|
|
3842
|
-
--sidebar-foreground: oklch(0.93 0.005 260);
|
|
3843
|
-
--sidebar-primary: oklch(0.55 0.2 260);
|
|
3844
|
-
--sidebar-primary-foreground: oklch(0.93 0.005 260);
|
|
3845
|
-
--sidebar-accent: oklch(0.22 0.008 260);
|
|
3846
|
-
--sidebar-accent-foreground: oklch(0.93 0.005 260);
|
|
3847
|
-
--sidebar-border: oklch(0.28 0.008 260);
|
|
3848
|
-
--sidebar-ring: oklch(0.45 0.01 260);
|
|
3849
|
-
--code-background: oklch(0.2 0.01 260);
|
|
3850
|
-
--code-foreground: oklch(0.9 0.01 260);
|
|
3851
|
-
|
|
3852
|
-
/* API semantic colors — lightened for dark mode (AA on 20%/10% tints). */
|
|
3853
|
-
--api-get: oklch(0.720 0.135 162);
|
|
3854
|
-
--api-post: oklch(0.755 0.174 259);
|
|
3855
|
-
--api-put: oklch(0.745 0.15 63);
|
|
3856
|
-
--api-patch: oklch(0.725 0.18 45);
|
|
3857
|
-
--api-delete: oklch(0.805 0.194 22);
|
|
3858
|
-
--api-head: oklch(0.740 0.182 286);
|
|
3859
|
-
--api-required: oklch(0.805 0.194 22);
|
|
3860
|
-
--api-deprecated: oklch(0.745 0.15 63);
|
|
3861
|
-
--api-type-string: oklch(0.690 0.135 162);
|
|
3862
|
-
--api-type-number: oklch(0.725 0.174 259);
|
|
3863
|
-
--api-type-boolean: oklch(0.720 0.15 63);
|
|
3864
|
-
--api-type-object: oklch(0.740 0.182 286);
|
|
3865
|
-
--api-type-array: oklch(0.725 0.18 45);
|
|
3866
|
-
--api-status-2xx: oklch(0.720 0.135 162);
|
|
3867
|
-
--api-status-3xx: oklch(0.755 0.174 259);
|
|
3868
|
-
--api-status-4xx: oklch(0.770 0.15 63);
|
|
3869
|
-
--api-status-5xx: oklch(0.805 0.194 22);
|
|
3870
|
-
}`,
|
|
3871
|
-
};
|
|
3872
3441
|
function writeThemeFile(path, themeName) {
|
|
3873
|
-
|
|
3874
|
-
writeFileSync(path, `@custom-variant dark (&:where(.dark, .dark *));
|
|
3875
|
-
|
|
3876
|
-
@theme inline {
|
|
3877
|
-
--color-background: var(--background);
|
|
3878
|
-
--color-foreground: var(--foreground);
|
|
3879
|
-
--color-card: var(--card);
|
|
3880
|
-
--color-card-foreground: var(--card-foreground);
|
|
3881
|
-
--color-popover: var(--popover);
|
|
3882
|
-
--color-popover-foreground: var(--popover-foreground);
|
|
3883
|
-
--color-primary: var(--primary);
|
|
3884
|
-
--color-primary-foreground: var(--primary-foreground);
|
|
3885
|
-
--color-secondary: var(--secondary);
|
|
3886
|
-
--color-secondary-foreground: var(--secondary-foreground);
|
|
3887
|
-
--color-muted: var(--muted);
|
|
3888
|
-
--color-muted-foreground: var(--muted-foreground);
|
|
3889
|
-
--color-accent: var(--accent);
|
|
3890
|
-
--color-accent-foreground: var(--accent-foreground);
|
|
3891
|
-
--color-destructive: var(--destructive);
|
|
3892
|
-
--color-destructive-foreground: var(--destructive-foreground);
|
|
3893
|
-
--color-border: var(--border);
|
|
3894
|
-
--color-input: var(--input);
|
|
3895
|
-
--color-ring: var(--ring);
|
|
3896
|
-
--radius-sm: calc(var(--radius) - 4px);
|
|
3897
|
-
--radius-md: calc(var(--radius) - 2px);
|
|
3898
|
-
--radius-lg: var(--radius);
|
|
3899
|
-
--radius-xl: calc(var(--radius) + 4px);
|
|
3900
|
-
|
|
3901
|
-
/* Sidebar */
|
|
3902
|
-
--color-sidebar: var(--sidebar);
|
|
3903
|
-
--color-sidebar-foreground: var(--sidebar-foreground);
|
|
3904
|
-
--color-sidebar-primary: var(--sidebar-primary);
|
|
3905
|
-
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
|
3906
|
-
--color-sidebar-accent: var(--sidebar-accent);
|
|
3907
|
-
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
|
3908
|
-
--color-sidebar-border: var(--sidebar-border);
|
|
3909
|
-
--color-sidebar-ring: var(--sidebar-ring);
|
|
3910
|
-
|
|
3911
|
-
/* Admonition colors */
|
|
3912
|
-
--color-note: var(--note);
|
|
3913
|
-
--color-note-foreground: var(--note-foreground);
|
|
3914
|
-
--color-abstract: var(--abstract);
|
|
3915
|
-
--color-info: var(--info);
|
|
3916
|
-
--color-tip: var(--tip);
|
|
3917
|
-
--color-success: var(--success);
|
|
3918
|
-
--color-question: var(--question);
|
|
3919
|
-
--color-warning: var(--warning);
|
|
3920
|
-
--color-failure: var(--failure);
|
|
3921
|
-
--color-danger: var(--danger);
|
|
3922
|
-
--color-bug: var(--bug);
|
|
3923
|
-
--color-example: var(--example);
|
|
3924
|
-
--color-quote: var(--quote);
|
|
3925
|
-
|
|
3926
|
-
/* Code */
|
|
3927
|
-
--color-code-bg: var(--code-background);
|
|
3928
|
-
--color-code-fg: var(--code-foreground);
|
|
3929
|
-
|
|
3930
|
-
/* API reference — HTTP method, type, status, role colors */
|
|
3931
|
-
--color-api-get: var(--api-get);
|
|
3932
|
-
--color-api-post: var(--api-post);
|
|
3933
|
-
--color-api-put: var(--api-put);
|
|
3934
|
-
--color-api-patch: var(--api-patch);
|
|
3935
|
-
--color-api-delete: var(--api-delete);
|
|
3936
|
-
--color-api-head: var(--api-head);
|
|
3937
|
-
--color-api-required: var(--api-required);
|
|
3938
|
-
--color-api-deprecated: var(--api-deprecated);
|
|
3939
|
-
--color-api-type-string: var(--api-type-string);
|
|
3940
|
-
--color-api-type-number: var(--api-type-number);
|
|
3941
|
-
--color-api-type-boolean: var(--api-type-boolean);
|
|
3942
|
-
--color-api-type-object: var(--api-type-object);
|
|
3943
|
-
--color-api-type-array: var(--api-type-array);
|
|
3944
|
-
--color-api-status-2xx: var(--api-status-2xx);
|
|
3945
|
-
--color-api-status-3xx: var(--api-status-3xx);
|
|
3946
|
-
--color-api-status-4xx: var(--api-status-4xx);
|
|
3947
|
-
--color-api-status-5xx: var(--api-status-5xx);
|
|
3948
|
-
|
|
3949
|
-
/* Fonts */
|
|
3950
|
-
--font-sans: var(--font-sans);
|
|
3951
|
-
--font-mono: var(--font-code);
|
|
3952
|
-
--font-family-code: var(--font-code);
|
|
3953
|
-
}
|
|
3954
|
-
|
|
3955
|
-
${tokens.light}
|
|
3956
|
-
|
|
3957
|
-
${tokens.dark}
|
|
3958
|
-
|
|
3959
|
-
@layer base {
|
|
3960
|
-
* { @apply border-border outline-ring/50; }
|
|
3961
|
-
body { @apply bg-background text-foreground font-sans; }
|
|
3962
|
-
}
|
|
3963
|
-
|
|
3964
|
-
/* Shiki dual-theme: light by default, dark when .dark class is active */
|
|
3965
|
-
.shiki,
|
|
3966
|
-
.shiki span,
|
|
3967
|
-
.astro-code,
|
|
3968
|
-
.astro-code span {
|
|
3969
|
-
color: var(--shiki-light) !important;
|
|
3970
|
-
}
|
|
3971
|
-
.shiki,
|
|
3972
|
-
.astro-code {
|
|
3973
|
-
background-color: var(--shiki-light-bg) !important;
|
|
3974
|
-
}
|
|
3975
|
-
.dark .shiki,
|
|
3976
|
-
.dark .shiki span,
|
|
3977
|
-
.dark .astro-code,
|
|
3978
|
-
.dark .astro-code span {
|
|
3979
|
-
color: var(--shiki-dark) !important;
|
|
3980
|
-
}
|
|
3981
|
-
.dark .shiki,
|
|
3982
|
-
.dark .astro-code {
|
|
3983
|
-
background-color: var(--shiki-dark-bg) !important;
|
|
3984
|
-
}
|
|
3985
|
-
/* Fix comment color contrast for WCAG AA */
|
|
3986
|
-
.shiki span[style*="--shiki-light:#6E7781"],
|
|
3987
|
-
.astro-code span[style*="--shiki-light:#6E7781"] {
|
|
3988
|
-
--shiki-light: #576069 !important;
|
|
3989
|
-
}
|
|
3990
|
-
`);
|
|
3442
|
+
writeFileSync(path, themeCss(themeName));
|
|
3991
3443
|
}
|
|
3992
3444
|
// ── Resolution helpers ──────────────────────────────────
|
|
3993
3445
|
/**
|