@dogsbay/format-astro 0.2.0-beta.16 → 0.2.0-beta.161
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 +7 -0
- package/dist/api-markdown.js +49 -0
- package/dist/base-path.d.ts +93 -8
- package/dist/base-path.js +122 -9
- package/dist/blog.d.ts +133 -0
- package/dist/blog.js +325 -0
- package/dist/browser.d.ts +13 -0
- package/dist/browser.js +13 -0
- package/dist/canonical.d.ts +35 -0
- package/dist/canonical.js +110 -0
- package/dist/capabilities.d.ts +28 -0
- package/dist/capabilities.js +94 -0
- package/dist/cli.d.ts +0 -1
- package/dist/cli.js +3 -1
- package/dist/diff-decoration.d.ts +35 -0
- package/dist/diff-decoration.js +101 -0
- package/dist/granularity.d.ts +82 -0
- package/dist/granularity.js +246 -0
- package/dist/head-metadata.d.ts +25 -0
- package/dist/head-metadata.js +55 -0
- package/dist/index.d.ts +26 -6
- package/dist/index.js +34 -5
- package/dist/lead.d.ts +1 -39
- package/dist/lead.js +3 -38
- package/dist/llms-txt.d.ts +47 -11
- package/dist/llms-txt.js +83 -275
- package/dist/plugins.d.ts +0 -1
- package/dist/plugins.js +4 -3
- package/dist/project.d.ts +502 -13
- package/dist/project.js +2506 -697
- package/dist/redirects.d.ts +117 -0
- package/dist/redirects.js +223 -0
- package/dist/serialize.d.ts +14 -14
- package/dist/serialize.js +908 -270
- package/dist/site-chrome.d.ts +62 -0
- package/dist/site-chrome.js +56 -0
- package/dist/sitemap.d.ts +62 -4
- package/dist/sitemap.js +63 -56
- package/dist/taxonomy.d.ts +48 -2
- package/dist/taxonomy.js +50 -12
- package/dist/theme-css.d.ts +33 -0
- package/dist/theme-css.js +686 -0
- package/dist/tone-classes.d.ts +52 -0
- package/dist/tone-classes.js +84 -0
- package/dist/write-if-changed.d.ts +20 -0
- package/dist/write-if-changed.js +57 -0
- package/package.json +17 -7
- package/dist/base-path.d.ts.map +0 -1
- package/dist/base-path.js.map +0 -1
- package/dist/cli.d.ts.map +0 -1
- package/dist/cli.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/lead.d.ts.map +0 -1
- package/dist/lead.js.map +0 -1
- package/dist/llms-txt.d.ts.map +0 -1
- package/dist/llms-txt.js.map +0 -1
- package/dist/plugins.d.ts.map +0 -1
- package/dist/plugins.js.map +0 -1
- package/dist/project.d.ts.map +0 -1
- package/dist/project.js.map +0 -1
- package/dist/serialize.d.ts.map +0 -1
- package/dist/serialize.js.map +0 -1
- package/dist/sitemap.d.ts.map +0 -1
- package/dist/sitemap.js.map +0 -1
- package/dist/taxonomy.d.ts.map +0 -1
- package/dist/taxonomy.js.map +0 -1
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { type SiteChrome } from "@dogsbay/site-files";
|
|
2
|
+
/** What DocsLayout reads (`chrome` in site.json). */
|
|
3
|
+
export interface ChromeData {
|
|
4
|
+
colorMode: "light" | "dark" | "system";
|
|
5
|
+
logo?: {
|
|
6
|
+
light: string;
|
|
7
|
+
dark?: string;
|
|
8
|
+
alt: string;
|
|
9
|
+
replacesName: boolean;
|
|
10
|
+
};
|
|
11
|
+
/** `key`: what a reader's close is stored under, the id with the site's prefix (sites on one origin keep their own). */
|
|
12
|
+
announcement?: {
|
|
13
|
+
id: string;
|
|
14
|
+
key: string;
|
|
15
|
+
html: string;
|
|
16
|
+
tone: string;
|
|
17
|
+
closeable: boolean;
|
|
18
|
+
};
|
|
19
|
+
header: {
|
|
20
|
+
links: Array<{
|
|
21
|
+
label: string;
|
|
22
|
+
href: string;
|
|
23
|
+
} | {
|
|
24
|
+
label: string;
|
|
25
|
+
items: Array<{
|
|
26
|
+
label: string;
|
|
27
|
+
href: string;
|
|
28
|
+
}>;
|
|
29
|
+
}>;
|
|
30
|
+
social: ChromeSocial[];
|
|
31
|
+
};
|
|
32
|
+
footer: {
|
|
33
|
+
columns: Array<{
|
|
34
|
+
title: string;
|
|
35
|
+
links: Array<{
|
|
36
|
+
label: string;
|
|
37
|
+
href: string;
|
|
38
|
+
}>;
|
|
39
|
+
}>;
|
|
40
|
+
social: ChromeSocial[];
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
export interface ChromeSocial {
|
|
44
|
+
label: string;
|
|
45
|
+
href: string;
|
|
46
|
+
svg: string;
|
|
47
|
+
}
|
|
48
|
+
export interface ChromeEmitOptions {
|
|
49
|
+
/** The URL prefix site paths take (urlBase + basePath). */
|
|
50
|
+
combined: string;
|
|
51
|
+
/** The host's base (the path of site.url): where `public/` is served. */
|
|
52
|
+
urlBase: string;
|
|
53
|
+
/** The logo files, absolute and already checked inside the project by the caller. */
|
|
54
|
+
logoFiles?: {
|
|
55
|
+
light: string;
|
|
56
|
+
dark?: string;
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
/** `chrome` for site.json; copies the logo files into `<outputDir>/public/_dogsbay/site/`. */
|
|
60
|
+
export declare function chromeData(chrome: SiteChrome, outputDir: string, o: ChromeEmitOptions): ChromeData;
|
|
61
|
+
/** `site.copyright` as HTML: inline Markdown, links with the site's prefix. */
|
|
62
|
+
export declare function copyrightHtml(copyright: string, combined: string): string;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The site's chrome (@dogsbay/site-files `siteChrome`) as DocsLayout draws it, written into
|
|
3
|
+
* `src/data/site.json` as `chrome`: links with the site's prefix, the texts rendered once with
|
|
4
|
+
* site-files' inline Markdown (the same HTML as dogsbay-ssg), the brand icons as SVG, and the
|
|
5
|
+
* logo files copied to `public/_dogsbay/site/` (served from the host's base, as the favicon).
|
|
6
|
+
* See plans/site-chrome.md.
|
|
7
|
+
*/
|
|
8
|
+
import { mkdirSync } from "node:fs";
|
|
9
|
+
import { extname, join } from "node:path";
|
|
10
|
+
import { inlineMarkdownHtml, isChromeMenu, versionMenuHref } from "@dogsbay/site-files";
|
|
11
|
+
import { socialIconSvg } from "@dogsbay/icons/social";
|
|
12
|
+
import { copyFileSync } from "./write-if-changed.js";
|
|
13
|
+
/** `chrome` for site.json; copies the logo files into `<outputDir>/public/_dogsbay/site/`. */
|
|
14
|
+
export function chromeData(chrome, outputDir, o) {
|
|
15
|
+
const href = (h) => versionMenuHref(h, o.combined);
|
|
16
|
+
const social = (list) => list.flatMap((s) => {
|
|
17
|
+
const svg = socialIconSvg(s.icon, 20);
|
|
18
|
+
return svg ? [{ label: s.label, href: href(s.href), svg }] : [];
|
|
19
|
+
});
|
|
20
|
+
const data = {
|
|
21
|
+
colorMode: chrome.colorMode,
|
|
22
|
+
header: {
|
|
23
|
+
links: chrome.header.links.map((l) => (isChromeMenu(l) ? { label: l.label, items: l.items.map((i) => ({ label: i.label, href: href(i.href) })) } : { label: l.label, href: href(l.href) })),
|
|
24
|
+
social: social(chrome.header.social),
|
|
25
|
+
},
|
|
26
|
+
footer: {
|
|
27
|
+
columns: chrome.footer.columns.map((c) => ({ title: c.title, links: c.links.map((l) => ({ label: l.label, href: href(l.href) })) })),
|
|
28
|
+
social: social(chrome.footer.social),
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
if (chrome.announcement) {
|
|
32
|
+
const a = chrome.announcement;
|
|
33
|
+
data.announcement = { id: a.id, key: o.combined ? `${o.combined}#${a.id}` : a.id, html: inlineMarkdownHtml(a.text, { href }), tone: a.tone, closeable: a.closeable };
|
|
34
|
+
}
|
|
35
|
+
if (chrome.logo && o.logoFiles) {
|
|
36
|
+
const dir = join(outputDir, "public", "_dogsbay", "site");
|
|
37
|
+
mkdirSync(dir, { recursive: true });
|
|
38
|
+
// The type is the config's path's (checked by siteChrome), not a symbolic link target's
|
|
39
|
+
const served = (from, written, name) => {
|
|
40
|
+
const file = `${name}${extname(written).toLowerCase()}`;
|
|
41
|
+
copyFileSync(from, join(dir, file));
|
|
42
|
+
return `${o.urlBase}/_dogsbay/site/${file}`;
|
|
43
|
+
};
|
|
44
|
+
data.logo = {
|
|
45
|
+
light: served(o.logoFiles.light, chrome.logo.light, "logo-light"),
|
|
46
|
+
...(o.logoFiles.dark && chrome.logo.dark ? { dark: served(o.logoFiles.dark, chrome.logo.dark, "logo-dark") } : {}),
|
|
47
|
+
alt: chrome.logo.alt,
|
|
48
|
+
replacesName: chrome.logo.replacesName,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
return data;
|
|
52
|
+
}
|
|
53
|
+
/** `site.copyright` as HTML: inline Markdown, links with the site's prefix. */
|
|
54
|
+
export function copyrightHtml(copyright, combined) {
|
|
55
|
+
return inlineMarkdownHtml(copyright, { href: (h) => versionMenuHref(h, combined) });
|
|
56
|
+
}
|
package/dist/sitemap.d.ts
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
* This module replaces it.
|
|
13
13
|
*/
|
|
14
14
|
import type { ExportPage } from "@dogsbay/types";
|
|
15
|
+
export { assertLeafSitemap } from "@dogsbay/site-files";
|
|
15
16
|
export interface BuildSitemapOptions {
|
|
16
17
|
/**
|
|
17
18
|
* Required. Origin of the deployed site (`https://acme.example`)
|
|
@@ -24,6 +25,26 @@ export interface BuildSitemapOptions {
|
|
|
24
25
|
* are joined under this so `<loc>` matches the actual URL.
|
|
25
26
|
*/
|
|
26
27
|
basePath: string;
|
|
28
|
+
/**
|
|
29
|
+
* Site-level noindex lockdown. When true, the sitemap emits its
|
|
30
|
+
* `<urlset>` wrapper with zero entries — there's no point
|
|
31
|
+
* advertising URLs we're also telling crawlers to skip via
|
|
32
|
+
* meta-robots. Keeps the file present so per-mount discovery /
|
|
33
|
+
* llms.txt references don't 404.
|
|
34
|
+
*/
|
|
35
|
+
siteNoindex?: boolean;
|
|
36
|
+
/**
|
|
37
|
+
* Leaf sitemap URLs from other sites on this host to list in the
|
|
38
|
+
* index alongside our own. See `AggregateSource` in `@dogsbay/cli`.
|
|
39
|
+
*
|
|
40
|
+
* Must be `<urlset>` documents, not other indexes — the sitemaps.org
|
|
41
|
+
* protocol forbids nesting index files. `buildSitemapIndex` cannot
|
|
42
|
+
* fetch them to check, so the caller validates; see
|
|
43
|
+
* `assertLeafSitemap`.
|
|
44
|
+
*/
|
|
45
|
+
extraSitemaps?: string[];
|
|
46
|
+
/** How many `sitemap-N.xml` files this site wrote (`buildSitemapFiles`). Default 1. */
|
|
47
|
+
sitemapCount?: number;
|
|
27
48
|
}
|
|
28
49
|
/**
|
|
29
50
|
* Build `<basePath>/sitemap-0.xml` — one `<url>` per non-excluded
|
|
@@ -41,9 +62,46 @@ export interface BuildSitemapOptions {
|
|
|
41
62
|
*/
|
|
42
63
|
export declare function buildSitemap(pages: ExportPage[], options: BuildSitemapOptions): string;
|
|
43
64
|
/**
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
65
|
+
* The sitemap files for the site: `sitemap-0.xml`, and more past the
|
|
66
|
+
* 50,000 URLs one file may hold. Pass their count to
|
|
67
|
+
* `buildSitemapIndex` as `sitemapCount`.
|
|
68
|
+
*/
|
|
69
|
+
export declare function buildSitemapFiles(pages: ExportPage[], options: BuildSitemapOptions): [string, string][];
|
|
70
|
+
/**
|
|
71
|
+
* Build `<basePath>/sitemap-index.xml`: this site's sitemap files, then
|
|
72
|
+
* the aggregated leaf sitemaps of other sites on the host.
|
|
47
73
|
*/
|
|
48
74
|
export declare function buildSitemapIndex(options: BuildSitemapOptions): string;
|
|
49
|
-
|
|
75
|
+
/**
|
|
76
|
+
* Compose a page's URL path under the mount prefix. The slug
|
|
77
|
+
* "index" / "" maps to the mount root. A nested `<section>/index`
|
|
78
|
+
* slug maps to `<section>/` (with trailing slash) — same shape
|
|
79
|
+
* Astro serves: `dist/foo/index.html` is reached at `/foo/`, not
|
|
80
|
+
* `/foo/index`. Without this normalisation the sitemap emitted
|
|
81
|
+
* `/foo/index` while the actual URL was `/foo/`, and the
|
|
82
|
+
* `seo/sitemap-complete` audit then flagged every section index
|
|
83
|
+
* as missing.
|
|
84
|
+
*
|
|
85
|
+
* ALWAYS ends in a trailing slash. It used to omit the slash for
|
|
86
|
+
* ordinary pages while adding it for `/index` ones, so a sitemap
|
|
87
|
+
* disagreed with itself AND with the canonical every layout emits.
|
|
88
|
+
* Measured on the live blog before the fix:
|
|
89
|
+
*
|
|
90
|
+
* sitemap /blog/migrating-asciidoc-to-markdown -> 307
|
|
91
|
+
* canonical /blog/migrating-asciidoc-to-markdown/ -> 200
|
|
92
|
+
*
|
|
93
|
+
* Every URL in every generated sitemap redirected. Google follows the
|
|
94
|
+
* redirect and indexes the canonical, so nothing was broken for
|
|
95
|
+
* readers — it burns crawl budget and reports "Page with redirect"
|
|
96
|
+
* against every URL in Search Console.
|
|
97
|
+
*
|
|
98
|
+
* The slash is correct because Astro's default `build.format` is
|
|
99
|
+
* `"directory"`: a page is emitted as `slug/index.html` and served at
|
|
100
|
+
* `slug/`. Neither the scaffold nor any generated config sets
|
|
101
|
+
* `build.format` or `trailingSlash`, so the default holds everywhere.
|
|
102
|
+
* If `build.format: "file"` is ever supported this must become
|
|
103
|
+
* conditional; the tests pin the coupling so it cannot pass silently.
|
|
104
|
+
*/
|
|
105
|
+
export declare function pageHref(slug: string | undefined, prefix: string): string;
|
|
106
|
+
export declare function normalizePrefix(basePath: string | undefined): string;
|
|
107
|
+
export declare function originOf(siteUrl: string): string;
|
package/dist/sitemap.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
|
|
1
|
+
import { buildSitemapIndex as buildSitemapIndexXml, buildSitemaps, buildUrlset, lastmodOf, } from "@dogsbay/site-files";
|
|
2
|
+
export { assertLeafSitemap } from "@dogsbay/site-files";
|
|
2
3
|
/**
|
|
3
4
|
* Build `<basePath>/sitemap-0.xml` — one `<url>` per non-excluded
|
|
4
5
|
* page.
|
|
@@ -14,46 +15,41 @@ const SITEMAP_NS = "http://www.sitemaps.org/schemas/sitemap/0.9";
|
|
|
14
15
|
* every build and waste crawler budget.
|
|
15
16
|
*/
|
|
16
17
|
export function buildSitemap(pages, options) {
|
|
18
|
+
return buildUrlset(sitemapEntries(pages, options));
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* The sitemap files for the site: `sitemap-0.xml`, and more past the
|
|
22
|
+
* 50,000 URLs one file may hold. Pass their count to
|
|
23
|
+
* `buildSitemapIndex` as `sitemapCount`.
|
|
24
|
+
*/
|
|
25
|
+
export function buildSitemapFiles(pages, options) {
|
|
26
|
+
return buildSitemaps(sitemapEntries(pages, options));
|
|
27
|
+
}
|
|
28
|
+
function sitemapEntries(pages, options) {
|
|
29
|
+
// Site-level noindex: the file stays (the index and robots.txt point
|
|
30
|
+
// at it), with no entries.
|
|
31
|
+
if (options.siteNoindex)
|
|
32
|
+
return [];
|
|
17
33
|
const origin = stripTrailingSlash(originOf(options.siteUrl));
|
|
18
34
|
const prefix = normalizePrefix(options.basePath);
|
|
19
|
-
const
|
|
20
|
-
'<?xml version="1.0" encoding="UTF-8"?>',
|
|
21
|
-
`<urlset xmlns="${SITEMAP_NS}">`,
|
|
22
|
-
];
|
|
35
|
+
const entries = [];
|
|
23
36
|
for (const page of pages) {
|
|
24
37
|
if (isExcluded(page))
|
|
25
38
|
continue;
|
|
26
|
-
const href = pageHref(page.slug, prefix);
|
|
27
|
-
const loc = `${origin}${href}`;
|
|
28
39
|
const lastmod = pageLastmod(page);
|
|
29
|
-
|
|
30
|
-
lines.push(` <loc>${escapeXml(loc)}</loc>`);
|
|
31
|
-
if (lastmod)
|
|
32
|
-
lines.push(` <lastmod>${escapeXml(lastmod)}</lastmod>`);
|
|
33
|
-
lines.push(" </url>");
|
|
40
|
+
entries.push({ loc: `${origin}${pageHref(page.slug, prefix)}`, ...(lastmod ? { lastmod } : {}) });
|
|
34
41
|
}
|
|
35
|
-
|
|
36
|
-
lines.push("");
|
|
37
|
-
return lines.join("\n");
|
|
42
|
+
return entries;
|
|
38
43
|
}
|
|
39
44
|
/**
|
|
40
|
-
* Build `<basePath>/sitemap-index.xml
|
|
41
|
-
*
|
|
42
|
-
* add corresponding `<sitemap>` entries here.
|
|
45
|
+
* Build `<basePath>/sitemap-index.xml`: this site's sitemap files, then
|
|
46
|
+
* the aggregated leaf sitemaps of other sites on the host.
|
|
43
47
|
*/
|
|
44
48
|
export function buildSitemapIndex(options) {
|
|
45
49
|
const origin = stripTrailingSlash(originOf(options.siteUrl));
|
|
46
50
|
const prefix = normalizePrefix(options.basePath);
|
|
47
|
-
const
|
|
48
|
-
return [
|
|
49
|
-
'<?xml version="1.0" encoding="UTF-8"?>',
|
|
50
|
-
`<sitemapindex xmlns="${SITEMAP_NS}">`,
|
|
51
|
-
" <sitemap>",
|
|
52
|
-
` <loc>${escapeXml(subSitemapUrl)}</loc>`,
|
|
53
|
-
" </sitemap>",
|
|
54
|
-
"</sitemapindex>",
|
|
55
|
-
"",
|
|
56
|
-
].join("\n");
|
|
51
|
+
const ours = Array.from({ length: Math.max(1, options.sitemapCount ?? 1) }, (_, i) => `${origin}${prefix}/sitemap-${i}.xml`);
|
|
52
|
+
return buildSitemapIndexXml(ours, options.extraSitemaps ?? []);
|
|
57
53
|
}
|
|
58
54
|
// ── Internals ────────────────────────────────────────────────────
|
|
59
55
|
function isExcluded(page) {
|
|
@@ -67,36 +63,56 @@ function isExcluded(page) {
|
|
|
67
63
|
return false;
|
|
68
64
|
}
|
|
69
65
|
function pageLastmod(page) {
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
return v.trim();
|
|
75
|
-
// YAML may parse ISO-like strings into Date objects depending
|
|
76
|
-
// on quoting. Surface the ISO form so the sitemap stays valid.
|
|
77
|
-
if (v instanceof Date && !isNaN(v.getTime())) {
|
|
78
|
-
return v.toISOString().slice(0, 10);
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
return null;
|
|
66
|
+
// Modification fields first, then creation: a post that was never
|
|
67
|
+
// edited has only a creation date (blog posts carry `created`), and
|
|
68
|
+
// no <lastmod> at all tells a crawler less than the publish date.
|
|
69
|
+
return lastmodOf(page.frontmatter ?? {}) ?? null;
|
|
82
70
|
}
|
|
83
71
|
/**
|
|
84
72
|
* Compose a page's URL path under the mount prefix. The slug
|
|
85
|
-
* "index" / "" maps to the mount root.
|
|
73
|
+
* "index" / "" maps to the mount root. A nested `<section>/index`
|
|
74
|
+
* slug maps to `<section>/` (with trailing slash) — same shape
|
|
75
|
+
* Astro serves: `dist/foo/index.html` is reached at `/foo/`, not
|
|
76
|
+
* `/foo/index`. Without this normalisation the sitemap emitted
|
|
77
|
+
* `/foo/index` while the actual URL was `/foo/`, and the
|
|
78
|
+
* `seo/sitemap-complete` audit then flagged every section index
|
|
79
|
+
* as missing.
|
|
80
|
+
*
|
|
81
|
+
* ALWAYS ends in a trailing slash. It used to omit the slash for
|
|
82
|
+
* ordinary pages while adding it for `/index` ones, so a sitemap
|
|
83
|
+
* disagreed with itself AND with the canonical every layout emits.
|
|
84
|
+
* Measured on the live blog before the fix:
|
|
85
|
+
*
|
|
86
|
+
* sitemap /blog/migrating-asciidoc-to-markdown -> 307
|
|
87
|
+
* canonical /blog/migrating-asciidoc-to-markdown/ -> 200
|
|
88
|
+
*
|
|
89
|
+
* Every URL in every generated sitemap redirected. Google follows the
|
|
90
|
+
* redirect and indexes the canonical, so nothing was broken for
|
|
91
|
+
* readers — it burns crawl budget and reports "Page with redirect"
|
|
92
|
+
* against every URL in Search Console.
|
|
93
|
+
*
|
|
94
|
+
* The slash is correct because Astro's default `build.format` is
|
|
95
|
+
* `"directory"`: a page is emitted as `slug/index.html` and served at
|
|
96
|
+
* `slug/`. Neither the scaffold nor any generated config sets
|
|
97
|
+
* `build.format` or `trailingSlash`, so the default holds everywhere.
|
|
98
|
+
* If `build.format: "file"` is ever supported this must become
|
|
99
|
+
* conditional; the tests pin the coupling so it cannot pass silently.
|
|
86
100
|
*/
|
|
87
|
-
function pageHref(slug, prefix) {
|
|
88
|
-
|
|
101
|
+
export function pageHref(slug, prefix) {
|
|
102
|
+
let clean = (slug ?? "").replace(/^\/+|\/+$/g, "");
|
|
89
103
|
if (!clean || clean === "index")
|
|
90
|
-
return prefix
|
|
91
|
-
|
|
104
|
+
return prefix ? `${prefix}/` : "/";
|
|
105
|
+
if (clean.endsWith("/index"))
|
|
106
|
+
clean = clean.slice(0, -"/index".length);
|
|
107
|
+
return `${prefix}/${clean}/`;
|
|
92
108
|
}
|
|
93
|
-
function normalizePrefix(basePath) {
|
|
109
|
+
export function normalizePrefix(basePath) {
|
|
94
110
|
if (!basePath)
|
|
95
111
|
return "";
|
|
96
112
|
const trimmed = basePath.replace(/\/+$/, "");
|
|
97
113
|
return trimmed.startsWith("/") ? trimmed : `/${trimmed}`;
|
|
98
114
|
}
|
|
99
|
-
function originOf(siteUrl) {
|
|
115
|
+
export function originOf(siteUrl) {
|
|
100
116
|
try {
|
|
101
117
|
const u = new URL(siteUrl);
|
|
102
118
|
return `${u.protocol}//${u.host}`;
|
|
@@ -108,12 +124,3 @@ function originOf(siteUrl) {
|
|
|
108
124
|
function stripTrailingSlash(s) {
|
|
109
125
|
return s.replace(/\/+$/, "");
|
|
110
126
|
}
|
|
111
|
-
function escapeXml(s) {
|
|
112
|
-
return s
|
|
113
|
-
.replace(/&/g, "&")
|
|
114
|
-
.replace(/</g, "<")
|
|
115
|
-
.replace(/>/g, ">")
|
|
116
|
-
.replace(/"/g, """)
|
|
117
|
-
.replace(/'/g, "'");
|
|
118
|
-
}
|
|
119
|
-
//# sourceMappingURL=sitemap.js.map
|
package/dist/taxonomy.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ExportPage } from "@dogsbay/types";
|
|
1
|
+
import type { ExportPage, TreeNode } from "@dogsbay/types";
|
|
2
2
|
/** Per-taxonomy emit config — resolved (defaults filled). */
|
|
3
3
|
export interface TaxonomyEmitConfig {
|
|
4
4
|
/** Path prefix for emitted index/term pages, e.g. "/tags". */
|
|
@@ -68,6 +68,29 @@ export interface TaxonomyData {
|
|
|
68
68
|
* name to the right `PageMeta` slot.
|
|
69
69
|
*/
|
|
70
70
|
export declare function getPageTerms(page: ExportPage, taxonomy: string): string[];
|
|
71
|
+
/**
|
|
72
|
+
* Derive a short description for one page, used to populate
|
|
73
|
+
* `TaxonomyPageRef.description` on the term data file. Card layouts
|
|
74
|
+
* in `<TaxonomyTerm>` consume this — title-only cards read as
|
|
75
|
+
* empty, so a working fallback matters.
|
|
76
|
+
*
|
|
77
|
+
* Resolution order:
|
|
78
|
+
* 1. `frontmatter.description` (string) when set — explicit
|
|
79
|
+
* author-supplied summary always wins.
|
|
80
|
+
* 2. First paragraph of `page.tree`, plain-text-extracted and
|
|
81
|
+
* truncated to {@link DESCRIPTION_LIMIT}. Skips headings so the
|
|
82
|
+
* page title doesn't echo as the description.
|
|
83
|
+
* 3. `undefined` — caller omits the field, card renders title-only.
|
|
84
|
+
*/
|
|
85
|
+
export declare function derivePageDescription(page: ExportPage): string | undefined;
|
|
86
|
+
/**
|
|
87
|
+
* Pull plain text from a TreeNode. Tolerates both shapes used by
|
|
88
|
+
* importers:
|
|
89
|
+
* - `node.inline` array (dogsbay-md parser).
|
|
90
|
+
* - `node.children[{type: "prose", inline: ...}]` (Starlight).
|
|
91
|
+
* - `node.html` prerendered HTML (fallback — strips tags).
|
|
92
|
+
*/
|
|
93
|
+
export declare function textOfNode(node: TreeNode): string;
|
|
71
94
|
/**
|
|
72
95
|
* Build the taxonomy data structure for one taxonomy across all pages.
|
|
73
96
|
* Pure function. Test-friendly. Sorts terms alphabetically by full
|
|
@@ -75,7 +98,25 @@ export declare function getPageTerms(page: ExportPage, taxonomy: string): string
|
|
|
75
98
|
*/
|
|
76
99
|
export declare function buildTaxonomyData(pages: ExportPage[], taxonomy: string, config: TaxonomyEmitConfig, options?: {
|
|
77
100
|
section?: string;
|
|
101
|
+
/**
|
|
102
|
+
* Dogsbay basePath — drives FILESYSTEM layout via `data.basePath`
|
|
103
|
+
* (route files live at `src/pages/<basePath>/<indexPath>/...`).
|
|
104
|
+
*/
|
|
78
105
|
basePath?: string;
|
|
106
|
+
/**
|
|
107
|
+
* URL prefix that all hrefs inside `data` (page URLs, indexPath
|
|
108
|
+
* baked for chip composition, term hrefs) carry. Should be the
|
|
109
|
+
* combined prefix (urlBase from site.url's path + basePath) so
|
|
110
|
+
* URLs resolve correctly on subpath-mounted deploys. When
|
|
111
|
+
* omitted, defaults to `basePath` for back-compat (single-site
|
|
112
|
+
* deploys without a urlBase). Without this split, callers that
|
|
113
|
+
* pass `combined` as the only basePath blow up the filesystem
|
|
114
|
+
* layout into `src/pages/<urlBase>/<basePath>/<indexPath>/...`,
|
|
115
|
+
* which then double-prefixes once Astro's `base` config kicks
|
|
116
|
+
* in at request time. Same family as the section-llms.txt fix
|
|
117
|
+
* (commit 47db909).
|
|
118
|
+
*/
|
|
119
|
+
urlPrefix?: string;
|
|
79
120
|
}): TaxonomyData;
|
|
80
121
|
/**
|
|
81
122
|
* Emit route files + data file for one taxonomy.
|
|
@@ -95,6 +136,11 @@ export declare function emitTaxonomyForName(data: TaxonomyData, outputDir: strin
|
|
|
95
136
|
*/
|
|
96
137
|
export declare function emitTaxonomyRoutes(pages: ExportPage[], outputDir: string, taxonomies: Record<string, TaxonomyEmitConfig>, options?: {
|
|
97
138
|
section?: string;
|
|
139
|
+
/** Dogsbay basePath — filesystem layout under src/pages/. */
|
|
98
140
|
basePath?: string;
|
|
141
|
+
/**
|
|
142
|
+
* Combined URL prefix (urlBase + basePath) baked into URLs in
|
|
143
|
+
* emitted data. Defaults to `basePath` when omitted.
|
|
144
|
+
*/
|
|
145
|
+
urlPrefix?: string;
|
|
99
146
|
}): string[];
|
|
100
|
-
//# sourceMappingURL=taxonomy.d.ts.map
|
package/dist/taxonomy.js
CHANGED
|
@@ -25,7 +25,9 @@
|
|
|
25
25
|
*
|
|
26
26
|
* See plans/metadata-and-taxonomies.md.
|
|
27
27
|
*/
|
|
28
|
-
import { existsSync, mkdirSync, readFileSync
|
|
28
|
+
import { existsSync, mkdirSync, readFileSync } from "node:fs";
|
|
29
|
+
// Unchanged files are not rewritten, so `site dev` reloads only what changed
|
|
30
|
+
import { writeFileSync } from "./write-if-changed.js";
|
|
29
31
|
import { dirname, join } from "node:path";
|
|
30
32
|
import { joinBaseUrl, normalizeBasePath, withBasePath, basePathSegments } from "./base-path.js";
|
|
31
33
|
/** Cap on description length emitted into taxonomy data. */
|
|
@@ -75,7 +77,7 @@ function pageUrl(slug, section, basePath) {
|
|
|
75
77
|
* page title doesn't echo as the description.
|
|
76
78
|
* 3. `undefined` — caller omits the field, card renders title-only.
|
|
77
79
|
*/
|
|
78
|
-
function derivePageDescription(page) {
|
|
80
|
+
export function derivePageDescription(page) {
|
|
79
81
|
const fm = page.frontmatter;
|
|
80
82
|
const fmDesc = fm?.description;
|
|
81
83
|
if (typeof fmDesc === "string" && fmDesc.trim().length > 0) {
|
|
@@ -106,7 +108,7 @@ function findFirstParagraph(nodes) {
|
|
|
106
108
|
* - `node.children[{type: "prose", inline: ...}]` (Starlight).
|
|
107
109
|
* - `node.html` prerendered HTML (fallback — strips tags).
|
|
108
110
|
*/
|
|
109
|
-
function textOfNode(node) {
|
|
111
|
+
export function textOfNode(node) {
|
|
110
112
|
const parts = [];
|
|
111
113
|
if (Array.isArray(node.inline)) {
|
|
112
114
|
parts.push(textOfInline(node.inline));
|
|
@@ -173,6 +175,9 @@ export function buildTaxonomyData(pages, taxonomy, config, options = {}) {
|
|
|
173
175
|
// Map a path key (joined by "/") → term entry under construction
|
|
174
176
|
const termMap = new Map();
|
|
175
177
|
const basePath = normalizeBasePath(options.basePath);
|
|
178
|
+
const urlPrefix = options.urlPrefix !== undefined
|
|
179
|
+
? normalizeBasePath(options.urlPrefix)
|
|
180
|
+
: basePath;
|
|
176
181
|
for (const page of pages) {
|
|
177
182
|
const terms = getPageTerms(page, taxonomy);
|
|
178
183
|
if (terms.length === 0)
|
|
@@ -181,7 +186,10 @@ export function buildTaxonomyData(pages, taxonomy, config, options = {}) {
|
|
|
181
186
|
const ref = {
|
|
182
187
|
slug: page.slug,
|
|
183
188
|
title: page.title,
|
|
184
|
-
|
|
189
|
+
// URLs go through urlPrefix (combined) so chip links resolve
|
|
190
|
+
// under subpath-mounted deploys. Filesystem layout uses
|
|
191
|
+
// basePath alone — split via `data.basePath` below.
|
|
192
|
+
url: pageUrl(page.slug, options.section, urlPrefix),
|
|
185
193
|
...(description ? { description } : {}),
|
|
186
194
|
};
|
|
187
195
|
// Track which path keys this page has already contributed to so a
|
|
@@ -219,15 +227,15 @@ export function buildTaxonomyData(pages, taxonomy, config, options = {}) {
|
|
|
219
227
|
allTerms.sort((a, b) => a.fullPath.join("/").localeCompare(b.fullPath.join("/")));
|
|
220
228
|
// `data.indexPath` is the URL-bearing form — components compose
|
|
221
229
|
// hrefs as `${data.indexPath}/${term}/`, so it has to include
|
|
222
|
-
//
|
|
223
|
-
//
|
|
224
|
-
//
|
|
225
|
-
//
|
|
226
|
-
//
|
|
227
|
-
//
|
|
230
|
+
// the combined URL prefix here (urlBase + basePath). Without
|
|
231
|
+
// urlBase, chip strips / breadcrumbs / sub-tag pills 404 on
|
|
232
|
+
// subpath-mounted deploys; without basePath, they 404 on sites
|
|
233
|
+
// with `site.basePath` set. `data.basePath` carries the
|
|
234
|
+
// basePath-only form for the file-system layout step in
|
|
235
|
+
// `emitTaxonomyForName`.
|
|
228
236
|
return {
|
|
229
237
|
name: taxonomy,
|
|
230
|
-
indexPath: withBasePath(
|
|
238
|
+
indexPath: withBasePath(urlPrefix, config.indexPath),
|
|
231
239
|
basePath,
|
|
232
240
|
hierarchical: config.hierarchical,
|
|
233
241
|
terms: allTerms,
|
|
@@ -356,6 +364,21 @@ export function emitTaxonomyRoutes(pages, outputDir, taxonomies, options = {}) {
|
|
|
356
364
|
// Writers who want a custom shell (different layout, no sidebar) can
|
|
357
365
|
// drop their own `src/pages<indexPath>/{index,[...path]}.astro` —
|
|
358
366
|
// `emitTaxonomyForName` skips emission when those files exist.
|
|
367
|
+
/**
|
|
368
|
+
* The prefix the host actually serves this site under, recovered from
|
|
369
|
+
* the urlPrefix-composed `indexPath`.
|
|
370
|
+
*
|
|
371
|
+
* DocsLayout builds the llms.txt link, the sitemap fallback and the
|
|
372
|
+
* switcher hrefs from its `basePath` prop. Taxonomy routes never passed
|
|
373
|
+
* one, so on a sub-path-mounted site those came out as `/llms.txt` and
|
|
374
|
+
* `/sitemap-index.xml` — outside the mount, 404 for every one of them.
|
|
375
|
+
*/
|
|
376
|
+
function servingPrefix(data, config) {
|
|
377
|
+
const suffix = normalizeBasePath(config.indexPath);
|
|
378
|
+
return suffix && data.indexPath.endsWith(suffix)
|
|
379
|
+
? data.indexPath.slice(0, data.indexPath.length - suffix.length)
|
|
380
|
+
: "";
|
|
381
|
+
}
|
|
359
382
|
function buildIndexAstro(data, config, routeSegments) {
|
|
360
383
|
const dataImport = dataImportPath(routeSegments, data);
|
|
361
384
|
const title = displayName(data.name);
|
|
@@ -372,11 +395,13 @@ import data from "${dataImport}";
|
|
|
372
395
|
import navData from "@/data/nav.json";
|
|
373
396
|
import siteConfigData from "@/data/site.json";
|
|
374
397
|
const siteConfig = siteConfigData as SiteConfig;
|
|
398
|
+
const favicon = (siteConfigData as { favicon?: string }).favicon;
|
|
375
399
|
const title = ${JSON.stringify(title)};
|
|
376
400
|
${displayConfig}
|
|
377
401
|
---
|
|
378
402
|
<DocsLayout
|
|
379
403
|
siteName={siteConfig.siteName}
|
|
404
|
+
siteTagline={siteConfig.tagline}
|
|
380
405
|
title={title}
|
|
381
406
|
nav={navData as any[]}
|
|
382
407
|
headings={[]}
|
|
@@ -386,12 +411,18 @@ ${displayConfig}
|
|
|
386
411
|
twitterHandle={siteConfig.twitterHandle || undefined}
|
|
387
412
|
themeColor={siteConfig.themeColor || undefined}
|
|
388
413
|
repoUrl={siteConfig.repoUrl || undefined}
|
|
414
|
+
poweredBy={siteConfig.poweredBy !== false}
|
|
415
|
+
siteChrome={(siteConfig as { chrome?: never }).chrome}
|
|
416
|
+
copyright={(siteConfig as { chrome?: unknown }).chrome ? siteConfig.copyright || undefined : undefined}
|
|
389
417
|
plausibleDomain={siteConfig.plausible?.domain}
|
|
390
418
|
plausibleScriptUrl={siteConfig.plausible?.scriptUrl}
|
|
391
419
|
tagPrefixes={siteConfig.tagPrefixes}
|
|
392
420
|
tagLabels={siteConfig.tagLabels}
|
|
421
|
+
tagAutoColor={siteConfig.tagAutoColor}
|
|
393
422
|
taxonomyIndexPaths={siteConfig.taxonomyIndexPaths}
|
|
394
423
|
taxonomyDisplay={siteConfig.taxonomyDisplay}
|
|
424
|
+
favicon={favicon}
|
|
425
|
+
basePath={${JSON.stringify(servingPrefix(data, config))}}
|
|
395
426
|
noindex={true}
|
|
396
427
|
excludeFromSearch={true}
|
|
397
428
|
>
|
|
@@ -415,6 +446,7 @@ import data from "${dataImport}";
|
|
|
415
446
|
import navData from "@/data/nav.json";
|
|
416
447
|
import siteConfigData from "@/data/site.json";
|
|
417
448
|
const siteConfig = siteConfigData as SiteConfig;
|
|
449
|
+
const favicon = (siteConfigData as { favicon?: string }).favicon;
|
|
418
450
|
|
|
419
451
|
export async function getStaticPaths() {
|
|
420
452
|
return data.terms.map((term) => ({
|
|
@@ -429,6 +461,7 @@ const title = \`\${term.label} — ${escapeTemplate(taxonomyDisplay)}\`;
|
|
|
429
461
|
---
|
|
430
462
|
<DocsLayout
|
|
431
463
|
siteName={siteConfig.siteName}
|
|
464
|
+
siteTagline={siteConfig.tagline}
|
|
432
465
|
title={title}
|
|
433
466
|
nav={navData as any[]}
|
|
434
467
|
headings={[]}
|
|
@@ -438,12 +471,18 @@ const title = \`\${term.label} — ${escapeTemplate(taxonomyDisplay)}\`;
|
|
|
438
471
|
twitterHandle={siteConfig.twitterHandle || undefined}
|
|
439
472
|
themeColor={siteConfig.themeColor || undefined}
|
|
440
473
|
repoUrl={siteConfig.repoUrl || undefined}
|
|
474
|
+
poweredBy={siteConfig.poweredBy !== false}
|
|
475
|
+
siteChrome={(siteConfig as { chrome?: never }).chrome}
|
|
476
|
+
copyright={(siteConfig as { chrome?: unknown }).chrome ? siteConfig.copyright || undefined : undefined}
|
|
441
477
|
plausibleDomain={siteConfig.plausible?.domain}
|
|
442
478
|
plausibleScriptUrl={siteConfig.plausible?.scriptUrl}
|
|
443
479
|
tagPrefixes={siteConfig.tagPrefixes}
|
|
444
480
|
tagLabels={siteConfig.tagLabels}
|
|
481
|
+
tagAutoColor={siteConfig.tagAutoColor}
|
|
445
482
|
taxonomyIndexPaths={siteConfig.taxonomyIndexPaths}
|
|
446
483
|
taxonomyDisplay={siteConfig.taxonomyDisplay}
|
|
484
|
+
favicon={favicon}
|
|
485
|
+
basePath={${JSON.stringify(servingPrefix(data, config))}}
|
|
447
486
|
noindex={true}
|
|
448
487
|
excludeFromSearch={true}
|
|
449
488
|
>
|
|
@@ -499,4 +538,3 @@ function escapeHtml(s) {
|
|
|
499
538
|
.replace(/>/g, ">")
|
|
500
539
|
.replace(/"/g, """);
|
|
501
540
|
}
|
|
502
|
-
//# sourceMappingURL=taxonomy.js.map
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Build the `@source inline("...")` directive that pins the
|
|
3
|
+
* grid-tone palette into the generated stylesheet.
|
|
4
|
+
*
|
|
5
|
+
* Why we need it: tone classes like `bg-primary/10` only appear in
|
|
6
|
+
* `.astro` pages emitted by Dogsbay's grid-item serializer. When
|
|
7
|
+
* Tailwind's content scanner doesn't pick them up — because the
|
|
8
|
+
* page lives outside the default scan globs, or because a class
|
|
9
|
+
* is composed at the boundary of an interpolation — they get
|
|
10
|
+
* purged. Result observed in dogsbay-docs-markdown audit: half
|
|
11
|
+
* the grid demo cells render with no background. Pinning forces
|
|
12
|
+
* generation regardless of scanner reach.
|
|
13
|
+
*
|
|
14
|
+
* Single source of truth: derived from TONE_CLASSES so any new
|
|
15
|
+
* tone added to the palette is automatically safelisted.
|
|
16
|
+
*/
|
|
17
|
+
export declare function buildToneSafelist(): string;
|
|
18
|
+
/**
|
|
19
|
+
* The same pin for the variant, tone and theme-scope tables
|
|
20
|
+
* (plans/dita-variants-themes.md). A line of its own, marked ASTRO-ONLY:
|
|
21
|
+
* render-html copies this stylesheet, and a renderer that compiles from the
|
|
22
|
+
* page's own classes (render-html, dogsbay-ssg) needs no safelist — there it
|
|
23
|
+
* put every variant's classes on every page (scripts/sources.mjs drops it).
|
|
24
|
+
*/
|
|
25
|
+
export declare function buildVariantSafelist(): string;
|
|
26
|
+
export declare function generateGlobalCss(): string;
|
|
27
|
+
/** The theme presets by name. A name not here falls back to "default", as writeThemeFile always did. */
|
|
28
|
+
export declare const THEMES: Record<string, {
|
|
29
|
+
light: string;
|
|
30
|
+
dark: string;
|
|
31
|
+
}>;
|
|
32
|
+
/** A site's src/styles/theme.css for a preset: the Tailwind theme mapping plus the preset's tokens. */
|
|
33
|
+
export declare function themeCss(themeName?: string): string;
|