@dogsbay/format-astro 0.2.0-beta.154 → 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";
@@ -370,6 +371,17 @@ export interface AstroProjectOptions {
370
371
  * to static `.md` files in `dist/`. Default `true`.
371
372
  */
372
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;
373
385
  /**
374
386
  * Granularity views — emit aggregate "single-page" views alongside the
375
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) {
@@ -1683,6 +1684,7 @@ export async function emitAstroPages(pages, nav, outputDir, options) {
1683
1684
  // combined === basePath (back-compat). For GH Pages project pages
1684
1685
  // and multi-mount Cloudflare, combined adds the urlBase layer.
1685
1686
  const hrefPrefix = combined;
1687
+ const mirrorMd = mirrorSerializer(options, pages);
1686
1688
  // Route-exclusion gate. Two signals, either is enough:
1687
1689
  // - frontmatter `_fragment: true` (loader-stamped — the importer
1688
1690
  // knew this .md was include-only, not a navigable page)
@@ -2295,7 +2297,7 @@ export async function emitAstroPages(pages, nav, outputDir, options) {
2295
2297
  if (options.mdMirror !== false) {
2296
2298
  const mdEndpointPath = join(pagesDir, section ? section : "", `${page.slug}.md.ts`);
2297
2299
  mkdirSync(dirname(mdEndpointPath), { recursive: true });
2298
- const endpointBody = buildMdEndpoint(page, sourceRel);
2300
+ const endpointBody = buildMdEndpoint(page, sourceRel, mirrorMd);
2299
2301
  writeFileSync(mdEndpointPath, endpointBody);
2300
2302
  generatedPaths.add(relative(outputDir, mdEndpointPath));
2301
2303
  // Sibling-level mirror for the index page under a non-empty
@@ -3128,10 +3130,66 @@ const LLMS_TXT_EXTERNAL_RE = /^(?:https?:\/\/|mailto:|tel:|#)/i;
3128
3130
  * frontmatter included, body normalized. Used for both the .md
3129
3131
  * mirror endpoints and the body summary in llms-full.txt.
3130
3132
  */
3131
- function serializePageMd(page) {
3132
- return treeToDogsbayMd(page.tree, {
3133
- frontmatter: page.frontmatter,
3134
- });
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
+ };
3135
3193
  }
3136
3194
  /**
3137
3195
  * Build the contents of `public/robots.txt`.
@@ -3265,8 +3323,8 @@ function buildMiddlewareSource(config) {
3265
3323
  * markdown body is embedded as a string literal at convert time;
3266
3324
  * Astro prerenders the endpoint into a static `.md` file under `dist/`.
3267
3325
  */
3268
- function buildMdEndpoint(page, sourceRel) {
3269
- const body = serializePageMd(page);
3326
+ function buildMdEndpoint(page, sourceRel, serialize) {
3327
+ const body = serialize(page, "copy");
3270
3328
  return [
3271
3329
  "// AUTO-GENERATED by `dogsbay convert` — do not edit.",
3272
3330
  `// Source: ${sourceRel}`,
@@ -3316,14 +3374,27 @@ function emitLlmsTxtFiles(outputDir, siteName, options, nav, pages) {
3316
3374
  mkdirSync(mountDir, { recursive: true });
3317
3375
  writeFileSync(join(mountDir, "llms.txt"), buildLlmsTxt(siteConfig, nav, pages, {
3318
3376
  hrefPrefix,
3377
+ indexMirrorHref: mirrorHref("index", options),
3319
3378
  aggregates: options.aggregates,
3320
3379
  whenToUse: options.llmsWhenToUse,
3321
3380
  }));
3322
- writeFileSync(join(mountDir, "llms-full.txt"), buildLlmsFullTxt(siteConfig, nav, pages, {
3323
- summary: "body",
3324
- serializePage: serializePageMd,
3325
- hrefPrefix,
3326
- }));
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);
3327
3398
  // Per-section files. `deriveSectionDir` returns a host-absolute
3328
3399
  // path derived from nav hrefs, which since the combined-prefix
3329
3400
  // refactor (commit 132891e) include urlBase + basePath — NOT just
@@ -3364,7 +3435,7 @@ function emitLlmsTxtFiles(outputDir, siteName, options, nav, pages) {
3364
3435
  }
3365
3436
  const sectionPath = join(mountDir, relDir, "llms.txt");
3366
3437
  mkdirSync(dirname(sectionPath), { recursive: true });
3367
- writeFileSync(sectionPath, buildSectionLlmsTxt(siteConfig, group, pages, { hrefPrefix }));
3438
+ writeFileSync(sectionPath, buildSectionLlmsTxt(siteConfig, group, pages, { hrefPrefix, indexMirrorHref: mirrorHref("index", options) }));
3368
3439
  }
3369
3440
  }
3370
3441
  /**
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.154",
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.154",
36
- "@dogsbay/icons": "0.2.0-beta.154",
37
- "@dogsbay/serialize-core": "0.2.0-beta.154",
38
- "@dogsbay/render-html": "0.2.0-beta.154",
39
- "@dogsbay/site-files": "0.2.0-beta.154",
40
- "@dogsbay/types": "0.2.0-beta.154",
41
- "@dogsbay/ui": "0.2.0-beta.154"
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",