@dogsbay/format-astro 0.2.0-beta.16 → 0.2.0-beta.160

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/dist/api-markdown.d.ts +7 -0
  2. package/dist/api-markdown.js +49 -0
  3. package/dist/base-path.d.ts +93 -8
  4. package/dist/base-path.js +122 -9
  5. package/dist/blog.d.ts +133 -0
  6. package/dist/blog.js +325 -0
  7. package/dist/browser.d.ts +13 -0
  8. package/dist/browser.js +13 -0
  9. package/dist/canonical.d.ts +35 -0
  10. package/dist/canonical.js +110 -0
  11. package/dist/capabilities.d.ts +28 -0
  12. package/dist/capabilities.js +94 -0
  13. package/dist/cli.d.ts +0 -1
  14. package/dist/cli.js +3 -1
  15. package/dist/diff-decoration.d.ts +35 -0
  16. package/dist/diff-decoration.js +101 -0
  17. package/dist/granularity.d.ts +82 -0
  18. package/dist/granularity.js +246 -0
  19. package/dist/head-metadata.d.ts +25 -0
  20. package/dist/head-metadata.js +55 -0
  21. package/dist/index.d.ts +26 -6
  22. package/dist/index.js +34 -5
  23. package/dist/lead.d.ts +1 -39
  24. package/dist/lead.js +3 -38
  25. package/dist/llms-txt.d.ts +47 -11
  26. package/dist/llms-txt.js +83 -275
  27. package/dist/plugins.d.ts +0 -1
  28. package/dist/plugins.js +4 -3
  29. package/dist/project.d.ts +502 -13
  30. package/dist/project.js +2506 -697
  31. package/dist/redirects.d.ts +117 -0
  32. package/dist/redirects.js +223 -0
  33. package/dist/serialize.d.ts +14 -14
  34. package/dist/serialize.js +908 -270
  35. package/dist/site-chrome.d.ts +62 -0
  36. package/dist/site-chrome.js +56 -0
  37. package/dist/sitemap.d.ts +62 -4
  38. package/dist/sitemap.js +63 -56
  39. package/dist/taxonomy.d.ts +48 -2
  40. package/dist/taxonomy.js +50 -12
  41. package/dist/theme-css.d.ts +33 -0
  42. package/dist/theme-css.js +686 -0
  43. package/dist/tone-classes.d.ts +52 -0
  44. package/dist/tone-classes.js +84 -0
  45. package/dist/write-if-changed.d.ts +20 -0
  46. package/dist/write-if-changed.js +57 -0
  47. package/package.json +17 -7
  48. package/dist/base-path.d.ts.map +0 -1
  49. package/dist/base-path.js.map +0 -1
  50. package/dist/cli.d.ts.map +0 -1
  51. package/dist/cli.js.map +0 -1
  52. package/dist/index.d.ts.map +0 -1
  53. package/dist/index.js.map +0 -1
  54. package/dist/lead.d.ts.map +0 -1
  55. package/dist/lead.js.map +0 -1
  56. package/dist/llms-txt.d.ts.map +0 -1
  57. package/dist/llms-txt.js.map +0 -1
  58. package/dist/plugins.d.ts.map +0 -1
  59. package/dist/plugins.js.map +0 -1
  60. package/dist/project.d.ts.map +0 -1
  61. package/dist/project.js.map +0 -1
  62. package/dist/serialize.d.ts.map +0 -1
  63. package/dist/serialize.js.map +0 -1
  64. package/dist/sitemap.d.ts.map +0 -1
  65. package/dist/sitemap.js.map +0 -1
  66. package/dist/taxonomy.d.ts.map +0 -1
  67. 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
- * Build `<basePath>/sitemap-index.xml` — points at the one (today)
45
- * sub-sitemap. Future split: emit additional `sitemap-N.xml` and
46
- * add corresponding `<sitemap>` entries here.
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
- //# sourceMappingURL=sitemap.d.ts.map
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
- const SITEMAP_NS = "http://www.sitemaps.org/schemas/sitemap/0.9";
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 lines = [
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
- lines.push(" <url>");
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
- lines.push("</urlset>");
36
- lines.push("");
37
- return lines.join("\n");
42
+ return entries;
38
43
  }
39
44
  /**
40
- * Build `<basePath>/sitemap-index.xml` — points at the one (today)
41
- * sub-sitemap. Future split: emit additional `sitemap-N.xml` and
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 subSitemapUrl = `${origin}${prefix}/sitemap-0.xml`;
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
- const fm = page.frontmatter ?? {};
71
- const candidates = [fm["lastUpdated"], fm["lastmod"]];
72
- for (const v of candidates) {
73
- if (typeof v === "string" && v.trim())
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
- const clean = (slug ?? "").replace(/^\/+|\/+$/g, "");
101
+ export function pageHref(slug, prefix) {
102
+ let clean = (slug ?? "").replace(/^\/+|\/+$/g, "");
89
103
  if (!clean || clean === "index")
90
- return prefix || "/";
91
- return `${prefix}/${clean}`;
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, "&amp;")
114
- .replace(/</g, "&lt;")
115
- .replace(/>/g, "&gt;")
116
- .replace(/"/g, "&quot;")
117
- .replace(/'/g, "&apos;");
118
- }
119
- //# sourceMappingURL=sitemap.js.map
@@ -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, writeFileSync } from "node:fs";
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
- url: pageUrl(page.slug, options.section, basePath),
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
- // basePath here. Without this, taxonomy chip strips, breadcrumbs,
223
- // sub-tag pills, and the type/status badge hrefs all 404 on any
224
- // site with `site.basePath` set. The route-file location (computed
225
- // separately in `emitTaxonomyForName` from `config.indexPath`)
226
- // still uses the raw form so Astro's `base` config handles the
227
- // file-system → URL mapping.
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(basePath, config.indexPath),
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, "&gt;")
500
539
  .replace(/"/g, "&quot;");
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;