@dogsbay/format-astro 0.2.0-beta.153 → 0.2.0-beta.155

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.
@@ -24,6 +24,12 @@ export interface BuildLlmsTxtOptions {
24
24
  * the host root.
25
25
  */
26
26
  hrefPrefix?: string;
27
+ /**
28
+ * The home page's Markdown copy, when the host knows it (format-astro's project: `<prefix>.md`
29
+ * beside a `site.basePath` mount, `<prefix>/index.md` under a base from `site.url`'s path, where
30
+ * Astro serves only below the base). Default: `<prefix>.md`, `/index.md` at the host root.
31
+ */
32
+ indexMirrorHref?: string;
27
33
  /**
28
34
  * When the site has a siteUrl, emitted entry URLs become absolute
29
35
  * (e.g. `https://example.com/docs/x.md`). When omitted, URLs stay
package/dist/llms-txt.js CHANGED
@@ -77,7 +77,7 @@ function mapInputs(siteConfig, nav, pages, options) {
77
77
  const key = item.href ? hrefToSlug(item.href, hrefPrefix) : null;
78
78
  return { label: item.label, ...(key !== null ? { key } : {}), children: toNav(item.children ?? []) };
79
79
  });
80
- return { pages: records, byKey, nav: toNav(nav), url: (key) => absoluteUrl(slugToHref(key, hrefPrefix), siteUrl) };
80
+ return { pages: records, byKey, nav: toNav(nav), url: (key) => absoluteUrl(slugToHref(key, hrefPrefix, options.indexMirrorHref), siteUrl) };
81
81
  }
82
82
  // ── Internals: page lookup + URL math ───────────────────────────
83
83
  function indexPagesBySlug(pages) {
@@ -96,10 +96,10 @@ function hrefToSlug(href, hrefPrefix) {
96
96
  let slug = href.slice(prefix.length).replace(/^\//, "").replace(/\/$/, "");
97
97
  return slug === "" ? "index" : slug;
98
98
  }
99
- function slugToHref(slug, hrefPrefix) {
99
+ function slugToHref(slug, hrefPrefix, indexHref) {
100
100
  const prefix = hrefPrefix.endsWith("/") ? hrefPrefix.slice(0, -1) : hrefPrefix;
101
101
  if (slug === "" || slug === "index")
102
- return indexMirrorHref(prefix);
102
+ return indexHref ?? indexMirrorHref(prefix);
103
103
  return `${prefix}/${slug}.md`;
104
104
  }
105
105
  /**
package/dist/project.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import type { ExportPage, NavItem } from "@dogsbay/types";
2
+ import { type MarkdownStyle } from "@dogsbay/format-dogsbay-md";
2
3
  import { type SiteChrome } from "@dogsbay/site-files";
3
4
  import { type VersionMenu, type ProductModel } from "@dogsbay/site-files";
4
5
  import { type BlogEmitConfig } from "./blog.js";
@@ -102,6 +103,7 @@ export interface AstroProjectOptions {
102
103
  into?: string;
103
104
  mediaOnly?: boolean;
104
105
  root?: string;
106
+ skip?: string[];
105
107
  }[];
106
108
  /**
107
109
  * Per-source asset directories with the URL prefix their content refs
@@ -369,6 +371,17 @@ export interface AstroProjectOptions {
369
371
  * to static `.md` files in `dist/`. Default `true`.
370
372
  */
371
373
  mdMirror?: boolean;
374
+ /**
375
+ * Where links in the Markdown copies (each `.md` and llms-full.txt) go: `html` (default), the
376
+ * page as the site serves it, so a person follows what an agent quotes; `md`, a page's `.md`
377
+ * copy (links to anything without one stay as they are). Absolute with `siteUrl`. Same key and
378
+ * meaning as dogsbay-ssg's `agent.mdMirrorLinks` (plans/md-mirror-links.md).
379
+ */
380
+ mdMirrorLinks?: "html" | "md";
381
+ /** Write llms-full.txt. Default true; off with `mdMirror: false` too (as dogsbay-ssg). */
382
+ llmsFull?: boolean;
383
+ /** How the Markdown copies are written: the site's `style:` (dogsbay.config.yml). */
384
+ markdownStyle?: MarkdownStyle;
372
385
  /**
373
386
  * Granularity views — emit aggregate "single-page" views alongside the
374
387
  * topic-grained pages. Additive: the topic site is unchanged; aggregates
package/dist/project.js CHANGED
@@ -12,7 +12,7 @@ import { fileURLToPath } from "node:url";
12
12
  import { treeToDogsbayMd } from "@dogsbay/format-dogsbay-md";
13
13
  import { treeToAstro } from "./serialize.js";
14
14
  import { apiPageLayout } from "@dogsbay/serialize-core";
15
- import { missingTranslations } from "@dogsbay/site-files";
15
+ import { missingTranslations, rewriteTreeLinks } from "@dogsbay/site-files";
16
16
  import { sanitizeTree } from "@dogsbay/render-html/sanitize";
17
17
  import { generateGlobalCss, themeCss } from "./theme-css.js";
18
18
  /**
@@ -143,8 +143,8 @@ function rewriteTreeHrefs(nodes, prefix) {
143
143
  if (node.inline) {
144
144
  rewriteInlineHrefs(node.inline, prefix);
145
145
  }
146
- // Rewrite hrefs in pre-rendered HTML
147
- if (node.html) {
146
+ // Rewrite hrefs in pre-rendered HTML; a code block's `html` is its code, shown as written
147
+ if (node.html && node.type !== "code" && node.type !== "math-block") {
148
148
  node.html = rewriteHtmlHrefs(node.html, prefix);
149
149
  }
150
150
  // Rewrite href in props (e.g. card, link-button)
@@ -203,7 +203,8 @@ function rewriteTreeImageSrcs(nodes, prefix) {
203
203
  if (node.inline) {
204
204
  rewriteInlineImageSrcs(node.inline, prefix);
205
205
  }
206
- if (node.html) {
206
+ // a code block's `html` is its code, shown as written
207
+ if (node.html && node.type !== "code" && node.type !== "math-block") {
207
208
  node.html = rewriteHtmlImageSrcs(node.html, prefix);
208
209
  }
209
210
  if (node.children) {
@@ -1658,10 +1659,11 @@ export async function emitAstroPages(pages, nav, outputDir, options) {
1658
1659
  const dest = mount.into
1659
1660
  ? join(outputDir, "public", mount.root ?? "_assets", mount.into)
1660
1661
  : join(outputDir, "public", mount.root ?? "_assets");
1662
+ const skipped = new Set((mount.skip ?? []).map((f) => join(mount.dir, f)));
1661
1663
  if (mount.mediaOnly)
1662
- copyMediaTree(mount.dir, dest);
1664
+ copyMediaTree(mount.dir, dest, skipped);
1663
1665
  else
1664
- cpSync(mount.dir, dest, { recursive: true });
1666
+ cpSync(mount.dir, dest, { recursive: true, filter: (src) => !skipped.has(src) });
1665
1667
  }
1666
1668
  }
1667
1669
  let generated = 0;
@@ -1682,6 +1684,7 @@ export async function emitAstroPages(pages, nav, outputDir, options) {
1682
1684
  // combined === basePath (back-compat). For GH Pages project pages
1683
1685
  // and multi-mount Cloudflare, combined adds the urlBase layer.
1684
1686
  const hrefPrefix = combined;
1687
+ const mirrorMd = mirrorSerializer(options, pages);
1685
1688
  // Route-exclusion gate. Two signals, either is enough:
1686
1689
  // - frontmatter `_fragment: true` (loader-stamped — the importer
1687
1690
  // knew this .md was include-only, not a navigable page)
@@ -2294,7 +2297,7 @@ export async function emitAstroPages(pages, nav, outputDir, options) {
2294
2297
  if (options.mdMirror !== false) {
2295
2298
  const mdEndpointPath = join(pagesDir, section ? section : "", `${page.slug}.md.ts`);
2296
2299
  mkdirSync(dirname(mdEndpointPath), { recursive: true });
2297
- const endpointBody = buildMdEndpoint(page, sourceRel);
2300
+ const endpointBody = buildMdEndpoint(page, sourceRel, mirrorMd);
2298
2301
  writeFileSync(mdEndpointPath, endpointBody);
2299
2302
  generatedPaths.add(relative(outputDir, mdEndpointPath));
2300
2303
  // Sibling-level mirror for the index page under a non-empty
@@ -3127,10 +3130,66 @@ const LLMS_TXT_EXTERNAL_RE = /^(?:https?:\/\/|mailto:|tel:|#)/i;
3127
3130
  * frontmatter included, body normalized. Used for both the .md
3128
3131
  * mirror endpoints and the body summary in llms-full.txt.
3129
3132
  */
3130
- function serializePageMd(page) {
3131
- return treeToDogsbayMd(page.tree, {
3132
- frontmatter: page.frontmatter,
3133
- });
3133
+ /**
3134
+ * The URL of a page's Markdown copy. The home page's is `<prefix>.md` beside a `site.basePath`
3135
+ * mount (a sibling endpoint is written there) and `<prefix>/index.md` otherwise: under a base from
3136
+ * `site.url`'s path Astro serves nothing outside the base, and llms.txt named `/repo.md`, a 404.
3137
+ */
3138
+ function mirrorHref(slug, options) {
3139
+ const prefix = combinedPrefix(options).replace(/\/$/, "");
3140
+ if (slug === "" || slug === "index")
3141
+ return basePathSegments(normalizeBasePath(options.basePath)).length ? `${prefix}.md` : `${prefix}/index.md`;
3142
+ return `${prefix}/${slug}.md`;
3143
+ }
3144
+ /** A page with a route (and a `.md` copy): not a fragment, not excluded from routes, not a redirect. */
3145
+ function hasRoute(p, options) {
3146
+ const fragment = p.fragment === true || p.frontmatter?._fragment === true;
3147
+ return !fragment && !makeSlugExcluder(options.excludeFromRoutes)(p.slug) && !(typeof p.redirect === "string" && p.redirect.trim());
3148
+ }
3149
+ /**
3150
+ * Writes a page's Markdown copy (its `.md`, its part of llms-full.txt) in the site's `style:`, its
3151
+ * links as the site serves them: the tree already carries the base path (emitAstroPages), so
3152
+ * here a link becomes absolute with `siteUrl`, and with `mdMirrorLinks: md` a page link names that
3153
+ * page's copy. In llms-full.txt (`where: "full"`) a same-page `#x` names its page. Shared walker:
3154
+ * `rewriteTreeLinks` (@dogsbay/site-files), as dogsbay-ssg's copies.
3155
+ */
3156
+ function mirrorSerializer(options, pages) {
3157
+ const prefix = combinedPrefix(options).replace(/\/$/, "");
3158
+ const site = options.siteUrl && /^https?:\/\//i.test(options.siteUrl) ? parseSiteUrl(options.siteUrl).origin : undefined;
3159
+ const htmlPath = (slug) => (slug === "" || slug === "index" ? `${prefix}/` : `${prefix}/${slug}`);
3160
+ const key = (path) => path.replace(/\/index$/, "").replace(/\/+$/, "") || "/";
3161
+ // Only pages with a route of their own have a copy: not a fragment, a slug excludeFromRoutes
3162
+ // names, or a page that is a `redirect:` (review: `.md` links to them were 404s)
3163
+ const bySite = new Map();
3164
+ for (const p of pages)
3165
+ if (hasRoute(p, options))
3166
+ bySite.set(key(htmlPath(p.slug)), p.slug);
3167
+ const abs = (path) => (site && path.startsWith("/") && !path.startsWith("//") ? `${site}${path}` : path);
3168
+ return (page, where) => {
3169
+ const link = (href) => {
3170
+ // A same-page `#x` names its page in llms-full.txt, and in a copy too when links are absolute
3171
+ // (dogsbay-ssg writes it so: a copy pasted elsewhere still points at the page)
3172
+ if (href.startsWith("#"))
3173
+ return where === "full" || site ? abs(htmlPath(page.slug)) + href : href;
3174
+ if (!href.startsWith("/") || href.startsWith("//"))
3175
+ return href;
3176
+ const at = href.search(/[?#]/);
3177
+ const path = at === -1 ? href : href.slice(0, at);
3178
+ const rest = at === -1 ? "" : href.slice(at);
3179
+ const slug = options.mdMirrorLinks === "md" ? bySite.get(key(path)) : undefined;
3180
+ return abs(slug !== undefined ? mirrorHref(slug, options) : path) + rest;
3181
+ };
3182
+ const restore = rewriteTreeLinks(page.tree, { link, asset: (src) => abs(src) });
3183
+ try {
3184
+ return treeToDogsbayMd(page.tree, {
3185
+ frontmatter: page.frontmatter,
3186
+ ...(options.markdownStyle ?? {}),
3187
+ });
3188
+ }
3189
+ finally {
3190
+ restore();
3191
+ }
3192
+ };
3134
3193
  }
3135
3194
  /**
3136
3195
  * Build the contents of `public/robots.txt`.
@@ -3264,8 +3323,8 @@ function buildMiddlewareSource(config) {
3264
3323
  * markdown body is embedded as a string literal at convert time;
3265
3324
  * Astro prerenders the endpoint into a static `.md` file under `dist/`.
3266
3325
  */
3267
- function buildMdEndpoint(page, sourceRel) {
3268
- const body = serializePageMd(page);
3326
+ function buildMdEndpoint(page, sourceRel, serialize) {
3327
+ const body = serialize(page, "copy");
3269
3328
  return [
3270
3329
  "// AUTO-GENERATED by `dogsbay convert` — do not edit.",
3271
3330
  `// Source: ${sourceRel}`,
@@ -3315,14 +3374,27 @@ function emitLlmsTxtFiles(outputDir, siteName, options, nav, pages) {
3315
3374
  mkdirSync(mountDir, { recursive: true });
3316
3375
  writeFileSync(join(mountDir, "llms.txt"), buildLlmsTxt(siteConfig, nav, pages, {
3317
3376
  hrefPrefix,
3377
+ indexMirrorHref: mirrorHref("index", options),
3318
3378
  aggregates: options.aggregates,
3319
3379
  whenToUse: options.llmsWhenToUse,
3320
3380
  }));
3321
- writeFileSync(join(mountDir, "llms-full.txt"), buildLlmsFullTxt(siteConfig, nav, pages, {
3322
- summary: "body",
3323
- serializePage: serializePageMd,
3324
- hrefPrefix,
3325
- }));
3381
+ // llms-full.txt is every page's copy in one file: off with `llmsFull: false`, and with
3382
+ // `mdMirror: false` (no copies), as in dogsbay-ssg
3383
+ const full = join(mountDir, "llms-full.txt");
3384
+ if (options.llmsFull !== false && options.mdMirror !== false) {
3385
+ const serialize = mirrorSerializer(options, pages);
3386
+ writeFileSync(full,
3387
+ // only pages with a route: the others' links never got the site's prefix, and they have no
3388
+ // page for an agent to cite (review)
3389
+ buildLlmsFullTxt(siteConfig, nav, pages.filter((p) => hasRoute(p, options)), {
3390
+ summary: "body",
3391
+ serializePage: (page) => serialize(page, "full"),
3392
+ hrefPrefix,
3393
+ indexMirrorHref: mirrorHref("index", options),
3394
+ }));
3395
+ }
3396
+ else if (existsSync(full))
3397
+ rmSync(full);
3326
3398
  // Per-section files. `deriveSectionDir` returns a host-absolute
3327
3399
  // path derived from nav hrefs, which since the combined-prefix
3328
3400
  // refactor (commit 132891e) include urlBase + basePath — NOT just
@@ -3363,7 +3435,7 @@ function emitLlmsTxtFiles(outputDir, siteName, options, nav, pages) {
3363
3435
  }
3364
3436
  const sectionPath = join(mountDir, relDir, "llms.txt");
3365
3437
  mkdirSync(dirname(sectionPath), { recursive: true });
3366
- writeFileSync(sectionPath, buildSectionLlmsTxt(siteConfig, group, pages, { hrefPrefix }));
3438
+ writeFileSync(sectionPath, buildSectionLlmsTxt(siteConfig, group, pages, { hrefPrefix, indexMirrorHref: mirrorHref("index", options) }));
3367
3439
  }
3368
3440
  }
3369
3441
  /**
@@ -3592,11 +3664,13 @@ const MOUNT_MEDIA_EXTS = new Set([
3592
3664
  ".svg", ".ico", ".pdf", ".mp4", ".webm",
3593
3665
  ]);
3594
3666
  /** Recursively copy only media files, preserving relative structure. */
3595
- function copyMediaTree(srcDir, destDir) {
3667
+ function copyMediaTree(srcDir, destDir, skip) {
3596
3668
  for (const entry of readdirSync(srcDir)) {
3597
3669
  const full = join(srcDir, entry);
3670
+ if (skip?.has(full))
3671
+ continue;
3598
3672
  if (statSync(full).isDirectory()) {
3599
- copyMediaTree(full, join(destDir, entry));
3673
+ copyMediaTree(full, join(destDir, entry), skip);
3600
3674
  continue;
3601
3675
  }
3602
3676
  const dot = entry.lastIndexOf(".");
package/dist/serialize.js CHANGED
@@ -276,8 +276,18 @@ function nodeToAstroBase(node, ctx) {
276
276
  });
277
277
  case "unordered-list":
278
278
  return wrapBlock("ul", "", node, childrenToAstro(node, ctx));
279
- case "list-item":
280
- return wrapBlock("li", "", node, leafContent(node, ctx));
279
+ case "list-item": {
280
+ // a task item: a disabled checkbox before its text, as render-html draws it
281
+ const c = node.props?.checked;
282
+ if (typeof c !== "boolean")
283
+ return wrapBlock("li", "", node, leafContent(node, ctx));
284
+ // the checkbox labelled by the item's text (axe: a form control needs a label); nested blocks after
285
+ const text = node.inline && node.inline.length ? leafContent({ ...node, children: undefined }, ctx) : "";
286
+ const rest = node.children?.length ? childrenToAstro({ ...node, inline: undefined }, ctx) : "";
287
+ // no text of its own: the checkbox is named "Task"
288
+ const lead = text ? `<label><input type="checkbox" disabled${c ? " checked" : ""} /> ${text}</label>` : `<input type="checkbox" disabled${c ? " checked" : ""} aria-label="Task" />`;
289
+ return wrapBlock("li", "", node, `${lead}${rest}`);
290
+ }
281
291
  case "blockquote":
282
292
  return wrapBlock("blockquote", "border-l-4 border-border pl-4 italic text-muted-foreground", node, childrenToAstro(node, ctx));
283
293
  // A transparent grouping wrapper: renders its children inside a plain
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dogsbay/format-astro",
3
- "version": "0.2.0-beta.153",
3
+ "version": "0.2.0-beta.155",
4
4
  "description": "Astro format plugin for Dogsbay — export TreeNode[] to Astro component markup",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -32,13 +32,13 @@
32
32
  "license": "MIT",
33
33
  "dependencies": {
34
34
  "markdown-it": "^14.3.0",
35
- "@dogsbay/format-dogsbay-md": "0.2.0-beta.153",
36
- "@dogsbay/render-html": "0.2.0-beta.153",
37
- "@dogsbay/icons": "0.2.0-beta.153",
38
- "@dogsbay/serialize-core": "0.2.0-beta.153",
39
- "@dogsbay/site-files": "0.2.0-beta.153",
40
- "@dogsbay/types": "0.2.0-beta.153",
41
- "@dogsbay/ui": "0.2.0-beta.153"
35
+ "@dogsbay/format-dogsbay-md": "0.2.0-beta.155",
36
+ "@dogsbay/icons": "0.2.0-beta.155",
37
+ "@dogsbay/render-html": "0.2.0-beta.155",
38
+ "@dogsbay/serialize-core": "0.2.0-beta.155",
39
+ "@dogsbay/site-files": "0.2.0-beta.155",
40
+ "@dogsbay/types": "0.2.0-beta.155",
41
+ "@dogsbay/ui": "0.2.0-beta.155"
42
42
  },
43
43
  "repository": {
44
44
  "type": "git",