@dogsbay/format-astro 0.2.0-beta.13 → 0.2.0-beta.131
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 +323 -0
- package/dist/browser.d.ts +13 -0
- package/dist/browser.js +13 -0
- package/dist/canonical.d.ts +33 -0
- package/dist/canonical.js +106 -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 -5
- package/dist/index.js +34 -4
- package/dist/lead.d.ts +1 -39
- package/dist/lead.js +3 -38
- package/dist/llms-txt.d.ts +41 -11
- package/dist/llms-txt.js +82 -274
- package/dist/plugins.d.ts +0 -1
- package/dist/plugins.js +4 -3
- package/dist/project.d.ts +377 -13
- package/dist/project.js +2239 -649
- package/dist/redirects.d.ts +115 -0
- package/dist/redirects.js +223 -0
- package/dist/serialize.d.ts +14 -1
- package/dist/serialize.js +821 -254
- package/dist/sitemap.d.ts +107 -0
- package/dist/sitemap.js +126 -0
- package/dist/taxonomy.d.ts +48 -2
- package/dist/taxonomy.js +46 -12
- package/dist/theme-css.d.ts +33 -0
- package/dist/theme-css.js +683 -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 +16 -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/taxonomy.d.ts.map +0 -1
- package/dist/taxonomy.js.map +0 -1
package/dist/lead.d.ts
CHANGED
|
@@ -1,39 +1 @@
|
|
|
1
|
-
|
|
2
|
-
* Detect whether a page's body tree already provides a level-1
|
|
3
|
-
* heading and / or a leading paragraph.
|
|
4
|
-
*
|
|
5
|
-
* `format-astro` uses this to decide whether to auto-inject the
|
|
6
|
-
* frontmatter `title` and `description` at the top of `<main>` —
|
|
7
|
-
* we never overwrite writer-authored content, only fill in what
|
|
8
|
-
* the body doesn't already cover.
|
|
9
|
-
*
|
|
10
|
-
* See plans/auto-lede.md.
|
|
11
|
-
*/
|
|
12
|
-
import type { TreeNode } from "@dogsbay/types";
|
|
13
|
-
export interface LeadingNodeInfo {
|
|
14
|
-
/**
|
|
15
|
-
* `true` when `tree[0]` is a level-1 heading. Heading depth
|
|
16
|
-
* lives at either `node.depth` (some importers) or
|
|
17
|
-
* `node.props.level` (others); both shapes are recognized.
|
|
18
|
-
*/
|
|
19
|
-
hasH1: boolean;
|
|
20
|
-
/**
|
|
21
|
-
* `true` when the body's first non-heading node is a paragraph.
|
|
22
|
-
* Used to decide whether the description should be auto-rendered
|
|
23
|
-
* as a lede `<p>` — if the writer already wrote a lede
|
|
24
|
-
* paragraph, we leave it alone.
|
|
25
|
-
*
|
|
26
|
-
* "First non-heading node" means: `tree[1]` when `tree[0]` is a
|
|
27
|
-
* heading; otherwise `tree[0]`. So `# Title\nLede paragraph`
|
|
28
|
-
* yields `{ hasH1: true, hasLede: true }`.
|
|
29
|
-
*/
|
|
30
|
-
hasLede: boolean;
|
|
31
|
-
}
|
|
32
|
-
/**
|
|
33
|
-
* Inspect the leading nodes of a page tree.
|
|
34
|
-
*
|
|
35
|
-
* Defensive against empty trees and missing properties — returns
|
|
36
|
-
* `false` for both flags when the tree is empty.
|
|
37
|
-
*/
|
|
38
|
-
export declare function detectLeadingNodes(tree: TreeNode[] | undefined): LeadingNodeInfo;
|
|
39
|
-
//# sourceMappingURL=lead.d.ts.map
|
|
1
|
+
export { detectLeadingNodes, deriveDescription, type LeadingNodeInfo } from "@dogsbay/serialize-core";
|
package/dist/lead.js
CHANGED
|
@@ -1,38 +1,3 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
* Defensive against empty trees and missing properties — returns
|
|
5
|
-
* `false` for both flags when the tree is empty.
|
|
6
|
-
*/
|
|
7
|
-
export function detectLeadingNodes(tree) {
|
|
8
|
-
if (!tree || tree.length === 0) {
|
|
9
|
-
return { hasH1: false, hasLede: false };
|
|
10
|
-
}
|
|
11
|
-
const first = tree[0];
|
|
12
|
-
const firstIsH1 = isH1(first);
|
|
13
|
-
if (firstIsH1) {
|
|
14
|
-
const second = tree[1];
|
|
15
|
-
return { hasH1: true, hasLede: isParagraph(second) };
|
|
16
|
-
}
|
|
17
|
-
return { hasH1: false, hasLede: isParagraph(first) };
|
|
18
|
-
}
|
|
19
|
-
function isH1(node) {
|
|
20
|
-
if (!node || node.type !== "heading")
|
|
21
|
-
return false;
|
|
22
|
-
// Some importers (dogsbay-md, mkdocs) attach depth as a top-level
|
|
23
|
-
// field; others (Starlight via mdx-jsx) use props.level. Both
|
|
24
|
-
// conventions are valid — check both.
|
|
25
|
-
const depth = node.depth;
|
|
26
|
-
if (typeof depth === "number")
|
|
27
|
-
return depth === 1;
|
|
28
|
-
const level = node.props?.level;
|
|
29
|
-
if (typeof level === "number")
|
|
30
|
-
return level === 1;
|
|
31
|
-
return false;
|
|
32
|
-
}
|
|
33
|
-
function isParagraph(node) {
|
|
34
|
-
if (!node)
|
|
35
|
-
return false;
|
|
36
|
-
return node.type === "paragraph";
|
|
37
|
-
}
|
|
38
|
-
//# sourceMappingURL=lead.js.map
|
|
1
|
+
// Moved to @dogsbay/serialize-core (src/lead.ts), so @dogsbay/site-check can use
|
|
2
|
+
// these without depending on format-astro, which brings @dogsbay/ui and astro.
|
|
3
|
+
export { detectLeadingNodes, deriveDescription } from "@dogsbay/serialize-core";
|
package/dist/llms-txt.d.ts
CHANGED
|
@@ -14,7 +14,9 @@
|
|
|
14
14
|
* dependency-free (only @dogsbay/types). Callers wire the actual
|
|
15
15
|
* `treeToDogsbayMd` serializer when they want full-body bundles.
|
|
16
16
|
*/
|
|
17
|
-
import type { ExportPage, NavItem, SiteConfig
|
|
17
|
+
import type { ExportPage, NavItem, SiteConfig } from "@dogsbay/types";
|
|
18
|
+
import { extractFirstParagraph } from "@dogsbay/serialize-core";
|
|
19
|
+
export { extractFirstParagraph };
|
|
18
20
|
export interface BuildLlmsTxtOptions {
|
|
19
21
|
/**
|
|
20
22
|
* Used to derive page slugs from nav hrefs. Defaults to
|
|
@@ -43,6 +45,41 @@ export interface BuildLlmsTxtOptions {
|
|
|
43
45
|
* orphan emission entirely (matches pre-decoupling behavior).
|
|
44
46
|
*/
|
|
45
47
|
otherPagesLabel?: string | false;
|
|
48
|
+
/**
|
|
49
|
+
* Other sites on this host to link at the end of the index — the
|
|
50
|
+
* `llms.txt` half of `site.aggregates`.
|
|
51
|
+
*
|
|
52
|
+
* Emitted as links, NOT inlined. Inlining would give an agent
|
|
53
|
+
* everything in one fetch but go stale on the aggregated site's next
|
|
54
|
+
* build, and this file is generated by a DIFFERENT build that has no
|
|
55
|
+
* way to know that happened.
|
|
56
|
+
*/
|
|
57
|
+
aggregates?: AggregateEntry[];
|
|
58
|
+
/** Heading for the aggregates section. Default "Other sites". */
|
|
59
|
+
aggregatesLabel?: string;
|
|
60
|
+
/**
|
|
61
|
+
* Free-form guidance placed directly under the site description:
|
|
62
|
+
* what this site is good for and when an agent should reach for it.
|
|
63
|
+
*
|
|
64
|
+
* The llms.txt spec gives you a title, a blockquote and a link
|
|
65
|
+
* index — all of which describe what EXISTS. None of them say when
|
|
66
|
+
* the site is the right answer, which is the question an agent is
|
|
67
|
+
* actually holding. An audit put it as "generic marketing copy does
|
|
68
|
+
* not read as guidance", and that is fair: this is worth writing
|
|
69
|
+
* only if it names concrete jobs.
|
|
70
|
+
*
|
|
71
|
+
* Emitted verbatim as markdown so an author can use a list, and
|
|
72
|
+
* omitted entirely when unset rather than filled with a default —
|
|
73
|
+
* an invented "when to use" is worse than none.
|
|
74
|
+
*/
|
|
75
|
+
whenToUse?: string;
|
|
76
|
+
}
|
|
77
|
+
/** One entry in the aggregates section. See `site.aggregates`. */
|
|
78
|
+
export interface AggregateEntry {
|
|
79
|
+
name: string;
|
|
80
|
+
description?: string;
|
|
81
|
+
url?: string;
|
|
82
|
+
llmsTxt?: string;
|
|
46
83
|
}
|
|
47
84
|
export interface BuildLlmsFullTxtOptions extends BuildLlmsTxtOptions {
|
|
48
85
|
/**
|
|
@@ -68,7 +105,7 @@ export interface BuildLlmsFullTxtOptions extends BuildLlmsTxtOptions {
|
|
|
68
105
|
* - Pages reachable in `pages` but absent from `nav` appended under
|
|
69
106
|
* a final `## {otherPagesLabel}` section. Set `otherPagesLabel`
|
|
70
107
|
* to `false` to suppress (e.g. when nav is the publish list).
|
|
71
|
-
* - External hrefs are skipped.
|
|
108
|
+
* - External hrefs, and hrefs outside `hrefPrefix`, are skipped.
|
|
72
109
|
*/
|
|
73
110
|
export declare function buildLlmsTxt(siteConfig: Pick<SiteConfig, "siteName" | "description" | "siteUrl">, nav: NavItem[], pages: ExportPage[], options?: BuildLlmsTxtOptions): string;
|
|
74
111
|
/**
|
|
@@ -78,15 +115,8 @@ export declare function buildLlmsTxt(siteConfig: Pick<SiteConfig, "siteName" | "
|
|
|
78
115
|
*/
|
|
79
116
|
export declare function buildSectionLlmsTxt(siteConfig: Pick<SiteConfig, "siteName" | "siteUrl">, group: NavItem, pages: ExportPage[], options?: BuildLlmsTxtOptions): string;
|
|
80
117
|
/**
|
|
81
|
-
* Bundle every page
|
|
82
|
-
* nav
|
|
118
|
+
* Bundle every page into a single document: nav order, then pages outside
|
|
119
|
+
* the nav by slug. Each page is delimited by a `# Title` header and `URL:`
|
|
83
120
|
* line, then either the full body (default) or just a description.
|
|
84
121
|
*/
|
|
85
122
|
export declare function buildLlmsFullTxt(siteConfig: Pick<SiteConfig, "siteName" | "description" | "siteUrl">, nav: NavItem[], pages: ExportPage[], options?: BuildLlmsFullTxtOptions): string;
|
|
86
|
-
/**
|
|
87
|
-
* Extract plain text of the first paragraph in a page tree. Cross-shape
|
|
88
|
-
* tolerant: handles `node.inline` (dogsbay-md parser) and
|
|
89
|
-
* `node.children[{type: "prose", inline}]` (Starlight importer).
|
|
90
|
-
*/
|
|
91
|
-
export declare function extractFirstParagraph(tree: TreeNode[]): string;
|
|
92
|
-
//# sourceMappingURL=llms-txt.d.ts.map
|
package/dist/llms-txt.js
CHANGED
|
@@ -1,9 +1,16 @@
|
|
|
1
|
+
import { extractFirstParagraph } from "@dogsbay/serialize-core";
|
|
1
2
|
import { DEFAULT_BASE_PATH } from "./base-path.js";
|
|
3
|
+
import { DESCRIPTION_LIMIT, buildLlmsFullTxt as buildLlmsFullTxtFile, buildLlmsTxt as buildLlmsTxtFile, buildSectionLlmsTxt as buildSectionLlmsTxtFile, llmsFullOrder, truncate, } from "@dogsbay/site-files";
|
|
4
|
+
// Moved to serialize-core so @dogsbay/site-check need not depend on format-astro
|
|
5
|
+
export { extractFirstParagraph };
|
|
2
6
|
const EXTERNAL_HREF_RE = /^(?:https?:\/\/|mailto:|tel:|#)/i;
|
|
3
|
-
const DEFAULT_DESCRIPTION_LIMIT =
|
|
4
|
-
const DEFAULT_TOP_LEVEL_LABEL = "Documentation";
|
|
5
|
-
const DEFAULT_OTHER_PAGES_LABEL = "Other pages";
|
|
7
|
+
const DEFAULT_DESCRIPTION_LIMIT = DESCRIPTION_LIMIT;
|
|
6
8
|
// ── Public builders ─────────────────────────────────────────────
|
|
9
|
+
//
|
|
10
|
+
// The files themselves are written by @dogsbay/site-files, shared with
|
|
11
|
+
// dogsbay-ssg. This module maps ExportPage and NavItem onto its records:
|
|
12
|
+
// keys are slugs ("index" for the home page), URLs are `<prefix>/<slug>.md`,
|
|
13
|
+
// descriptions come from front matter or the first paragraph.
|
|
7
14
|
/**
|
|
8
15
|
* Build the top-level llms.txt index for a site.
|
|
9
16
|
*
|
|
@@ -14,73 +21,18 @@ const DEFAULT_OTHER_PAGES_LABEL = "Other pages";
|
|
|
14
21
|
* - Pages reachable in `pages` but absent from `nav` appended under
|
|
15
22
|
* a final `## {otherPagesLabel}` section. Set `otherPagesLabel`
|
|
16
23
|
* to `false` to suppress (e.g. when nav is the publish list).
|
|
17
|
-
* - External hrefs are skipped.
|
|
24
|
+
* - External hrefs, and hrefs outside `hrefPrefix`, are skipped.
|
|
18
25
|
*/
|
|
19
26
|
export function buildLlmsTxt(siteConfig, nav, pages, options = {}) {
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
: options.otherPagesLabel
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
const out = [];
|
|
30
|
-
out.push(`# ${siteConfig.siteName}`);
|
|
31
|
-
out.push("");
|
|
32
|
-
if (siteConfig.description) {
|
|
33
|
-
out.push(`> ${siteConfig.description}`);
|
|
34
|
-
out.push("");
|
|
35
|
-
}
|
|
36
|
-
const topLevelLeaves = [];
|
|
37
|
-
const groupSections = [];
|
|
38
|
-
for (const item of nav) {
|
|
39
|
-
if (item.children && item.children.length > 0) {
|
|
40
|
-
const lines = [];
|
|
41
|
-
lines.push(`## ${item.label}`);
|
|
42
|
-
lines.push("");
|
|
43
|
-
// Group's own landing page first, if it has one
|
|
44
|
-
if (item.href && !EXTERNAL_HREF_RE.test(item.href)) {
|
|
45
|
-
const entry = renderEntry(item, slugIndex, hrefPrefix, siteUrl, descLimit, emittedSlugs);
|
|
46
|
-
if (entry)
|
|
47
|
-
lines.push(entry);
|
|
48
|
-
}
|
|
49
|
-
collectEntries(item.children, slugIndex, hrefPrefix, siteUrl, descLimit, lines, emittedSlugs);
|
|
50
|
-
lines.push("");
|
|
51
|
-
groupSections.push(lines);
|
|
52
|
-
}
|
|
53
|
-
else if (item.href && !EXTERNAL_HREF_RE.test(item.href)) {
|
|
54
|
-
topLevelLeaves.push(item);
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
if (topLevelLeaves.length > 0) {
|
|
58
|
-
out.push(`## ${topLevelLabel}`);
|
|
59
|
-
out.push("");
|
|
60
|
-
for (const leaf of topLevelLeaves) {
|
|
61
|
-
const entry = renderEntry(leaf, slugIndex, hrefPrefix, siteUrl, descLimit, emittedSlugs);
|
|
62
|
-
if (entry)
|
|
63
|
-
out.push(entry);
|
|
64
|
-
}
|
|
65
|
-
out.push("");
|
|
66
|
-
}
|
|
67
|
-
for (const section of groupSections) {
|
|
68
|
-
out.push(...section);
|
|
69
|
-
}
|
|
70
|
-
// Orphan pass — pages that exist in `pages` but were never hit by
|
|
71
|
-
// the nav walk. Emit so agents see the full surface even when nav
|
|
72
|
-
// curates a subset (the common case: a sidebar trims internal /
|
|
73
|
-
// sub-utility pages, but agents still want to find them).
|
|
74
|
-
if (otherPagesLabel !== false) {
|
|
75
|
-
const orphans = collectOrphanEntries(pages, emittedSlugs, hrefPrefix, siteUrl, descLimit);
|
|
76
|
-
if (orphans.length > 0) {
|
|
77
|
-
out.push(`## ${otherPagesLabel}`);
|
|
78
|
-
out.push("");
|
|
79
|
-
out.push(...orphans);
|
|
80
|
-
out.push("");
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
return out.join("\n").replace(/\n{3,}/g, "\n\n").trimEnd() + "\n";
|
|
27
|
+
const m = mapInputs(siteConfig, nav, pages, options);
|
|
28
|
+
return buildLlmsTxtFile({ name: siteConfig.siteName, description: siteConfig.description, whenToUse: options.whenToUse, aggregates: options.aggregates }, m.nav, m.pages, {
|
|
29
|
+
url: m.url,
|
|
30
|
+
// A nav entry whose page is missing is still linked, by its label.
|
|
31
|
+
missing: "link",
|
|
32
|
+
topLevelLabel: options.topLevelLabel,
|
|
33
|
+
otherPagesLabel: options.otherPagesLabel,
|
|
34
|
+
aggregatesLabel: options.aggregatesLabel,
|
|
35
|
+
});
|
|
84
36
|
}
|
|
85
37
|
/**
|
|
86
38
|
* Per-section llms.txt — same shape as the root index, scoped to one
|
|
@@ -88,149 +40,44 @@ export function buildLlmsTxt(siteConfig, nav, pages, options = {}) {
|
|
|
88
40
|
* can pull only the section they care about.
|
|
89
41
|
*/
|
|
90
42
|
export function buildSectionLlmsTxt(siteConfig, group, pages, options = {}) {
|
|
91
|
-
const
|
|
92
|
-
|
|
93
|
-
const descLimit = options.descriptionLimit ?? DEFAULT_DESCRIPTION_LIMIT;
|
|
94
|
-
const slugIndex = indexPagesBySlug(pages);
|
|
95
|
-
const out = [];
|
|
96
|
-
out.push(`# ${siteConfig.siteName} — ${group.label}`);
|
|
97
|
-
out.push("");
|
|
98
|
-
out.push(`## ${group.label}`);
|
|
99
|
-
out.push("");
|
|
100
|
-
if (group.href && !EXTERNAL_HREF_RE.test(group.href)) {
|
|
101
|
-
const entry = renderEntry(group, slugIndex, hrefPrefix, siteUrl, descLimit);
|
|
102
|
-
if (entry)
|
|
103
|
-
out.push(entry);
|
|
104
|
-
}
|
|
105
|
-
if (group.children) {
|
|
106
|
-
collectEntries(group.children, slugIndex, hrefPrefix, siteUrl, descLimit, out);
|
|
107
|
-
}
|
|
108
|
-
return out.join("\n").replace(/\n{3,}/g, "\n\n").trimEnd() + "\n";
|
|
43
|
+
const m = mapInputs(siteConfig, [group], pages, options);
|
|
44
|
+
return buildSectionLlmsTxtFile({ name: siteConfig.siteName }, m.nav[0], m.pages, { url: m.url, missing: "link" });
|
|
109
45
|
}
|
|
110
46
|
/**
|
|
111
|
-
* Bundle every page
|
|
112
|
-
* nav
|
|
47
|
+
* Bundle every page into a single document: nav order, then pages outside
|
|
48
|
+
* the nav by slug. Each page is delimited by a `# Title` header and `URL:`
|
|
113
49
|
* line, then either the full body (default) or just a description.
|
|
114
50
|
*/
|
|
115
51
|
export function buildLlmsFullTxt(siteConfig, nav, pages, options = {}) {
|
|
116
|
-
const hrefPrefix = options.hrefPrefix ?? DEFAULT_BASE_PATH;
|
|
117
|
-
const siteUrl = options.siteUrl ?? siteConfig.siteUrl;
|
|
118
52
|
const summary = options.summary ?? (options.serializePage ? "body" : "description");
|
|
119
53
|
if (summary === "body" && !options.serializePage) {
|
|
120
54
|
throw new Error(`buildLlmsFullTxt: summary "body" requires options.serializePage`);
|
|
121
55
|
}
|
|
122
|
-
const
|
|
123
|
-
const
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
const seen = new Set(navOrder);
|
|
129
|
-
const orphanSlugs = pages
|
|
130
|
-
.map((p) => p.slug || "index")
|
|
131
|
-
.filter((s) => !seen.has(s))
|
|
132
|
-
.sort((a, b) => a.localeCompare(b));
|
|
133
|
-
const allSlugs = [...navOrder, ...orphanSlugs];
|
|
134
|
-
const out = [];
|
|
135
|
-
out.push(`# ${siteConfig.siteName}`);
|
|
136
|
-
out.push("");
|
|
137
|
-
if (siteConfig.description) {
|
|
138
|
-
out.push(`> ${siteConfig.description}`);
|
|
139
|
-
out.push("");
|
|
140
|
-
}
|
|
141
|
-
let firstPage = true;
|
|
142
|
-
for (const slug of allSlugs) {
|
|
143
|
-
const page = slugIndex.get(slug);
|
|
144
|
-
if (!page)
|
|
145
|
-
continue;
|
|
146
|
-
if (!firstPage) {
|
|
147
|
-
out.push("---");
|
|
148
|
-
out.push("");
|
|
149
|
-
}
|
|
150
|
-
firstPage = false;
|
|
151
|
-
out.push(`# ${page.title}`);
|
|
152
|
-
out.push("");
|
|
153
|
-
out.push(`URL: ${absoluteUrl(slugToHref(slug, hrefPrefix), siteUrl)}`);
|
|
154
|
-
out.push("");
|
|
155
|
-
if (summary === "body") {
|
|
156
|
-
out.push(options.serializePage(page).trimEnd());
|
|
157
|
-
}
|
|
158
|
-
else {
|
|
159
|
-
const desc = getPageDescription(page, options.descriptionLimit ?? DEFAULT_DESCRIPTION_LIMIT);
|
|
160
|
-
out.push(desc || page.title);
|
|
161
|
-
}
|
|
162
|
-
out.push("");
|
|
163
|
-
}
|
|
164
|
-
return out.join("\n").replace(/\n{3,}/g, "\n\n").trimEnd() + "\n";
|
|
165
|
-
}
|
|
166
|
-
// ── Internals: nav walking + entry rendering ────────────────────
|
|
167
|
-
function collectEntries(items, slugIndex, hrefPrefix, siteUrl, descLimit, out, emitted) {
|
|
168
|
-
for (const item of items) {
|
|
169
|
-
if (item.href && !EXTERNAL_HREF_RE.test(item.href)) {
|
|
170
|
-
const entry = renderEntry(item, slugIndex, hrefPrefix, siteUrl, descLimit, emitted);
|
|
171
|
-
if (entry)
|
|
172
|
-
out.push(entry);
|
|
173
|
-
}
|
|
174
|
-
if (item.children) {
|
|
175
|
-
collectEntries(item.children, slugIndex, hrefPrefix, siteUrl, descLimit, out, emitted);
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
function renderEntry(item, slugIndex, hrefPrefix, siteUrl, descLimit, emitted) {
|
|
180
|
-
if (!item.href)
|
|
181
|
-
return null;
|
|
182
|
-
const slug = hrefToSlug(item.href, hrefPrefix);
|
|
183
|
-
if (slug === null)
|
|
184
|
-
return null;
|
|
185
|
-
const page = slugIndex.get(slug);
|
|
186
|
-
const label = item.label || page?.title || slug;
|
|
187
|
-
const description = page ? getPageDescription(page, descLimit) : "";
|
|
188
|
-
const url = absoluteUrl(`${item.href.replace(/\/$/, "")}.md`, siteUrl);
|
|
189
|
-
if (emitted && page)
|
|
190
|
-
emitted.add(slug);
|
|
191
|
-
return description
|
|
192
|
-
? `- [${label}](${url}): ${description}`
|
|
193
|
-
: `- [${label}](${url})`;
|
|
194
|
-
}
|
|
195
|
-
/**
|
|
196
|
-
* Render entries for pages that exist but weren't reached during the
|
|
197
|
-
* nav walk. Sorted alphabetically by slug for stable output (nav
|
|
198
|
-
* order isn't meaningful for these — they're not in nav).
|
|
199
|
-
*/
|
|
200
|
-
function collectOrphanEntries(pages, emitted, hrefPrefix, siteUrl, descLimit) {
|
|
201
|
-
const orphans = [];
|
|
202
|
-
const sorted = pages
|
|
203
|
-
.filter((p) => !emitted.has(p.slug || "index"))
|
|
204
|
-
.sort((a, b) => (a.slug || "index").localeCompare(b.slug || "index"));
|
|
205
|
-
for (const page of sorted) {
|
|
206
|
-
const slug = page.slug || "index";
|
|
207
|
-
const label = page.title || slug;
|
|
208
|
-
const description = getPageDescription(page, descLimit);
|
|
209
|
-
const url = absoluteUrl(slugToHref(slug, hrefPrefix), siteUrl);
|
|
210
|
-
orphans.push(description
|
|
211
|
-
? `- [${label}](${url}): ${description}`
|
|
212
|
-
: `- [${label}](${url})`);
|
|
213
|
-
}
|
|
214
|
-
return orphans;
|
|
215
|
-
}
|
|
216
|
-
function flattenNavToSlugs(nav, hrefPrefix) {
|
|
217
|
-
const slugs = [];
|
|
218
|
-
const seen = new Set();
|
|
219
|
-
const walk = (items) => {
|
|
220
|
-
for (const item of items) {
|
|
221
|
-
if (item.href && !EXTERNAL_HREF_RE.test(item.href)) {
|
|
222
|
-
const slug = hrefToSlug(item.href, hrefPrefix);
|
|
223
|
-
if (slug !== null && !seen.has(slug)) {
|
|
224
|
-
seen.add(slug);
|
|
225
|
-
slugs.push(slug);
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
if (item.children)
|
|
229
|
-
walk(item.children);
|
|
230
|
-
}
|
|
56
|
+
const m = mapInputs(siteConfig, nav, pages, options);
|
|
57
|
+
const load = (key) => {
|
|
58
|
+
const page = m.byKey.get(key);
|
|
59
|
+
if (summary === "body")
|
|
60
|
+
return options.serializePage(page);
|
|
61
|
+
return m.pages.get(key).description || page.title;
|
|
231
62
|
};
|
|
232
|
-
|
|
233
|
-
|
|
63
|
+
return buildLlmsFullTxtFile({ name: siteConfig.siteName, description: siteConfig.description, whenToUse: options.whenToUse }, llmsFullOrder(m.nav, m.pages), m.pages, m.url, load);
|
|
64
|
+
}
|
|
65
|
+
/** ExportPage and NavItem as @dogsbay/site-files records. */
|
|
66
|
+
function mapInputs(siteConfig, nav, pages, options) {
|
|
67
|
+
const hrefPrefix = options.hrefPrefix ?? DEFAULT_BASE_PATH;
|
|
68
|
+
const siteUrl = options.siteUrl ?? siteConfig.siteUrl;
|
|
69
|
+
const limit = options.descriptionLimit ?? DEFAULT_DESCRIPTION_LIMIT;
|
|
70
|
+
const byKey = indexPagesBySlug(pages);
|
|
71
|
+
const records = new Map();
|
|
72
|
+
for (const [key, page] of byKey) {
|
|
73
|
+
const description = getPageDescription(page, limit);
|
|
74
|
+
records.set(key, { title: page.title, ...(description ? { description } : {}) });
|
|
75
|
+
}
|
|
76
|
+
const toNav = (items) => items.map((item) => {
|
|
77
|
+
const key = item.href ? hrefToSlug(item.href, hrefPrefix) : null;
|
|
78
|
+
return { label: item.label, ...(key !== null ? { key } : {}), children: toNav(item.children ?? []) };
|
|
79
|
+
});
|
|
80
|
+
return { pages: records, byKey, nav: toNav(nav), url: (key) => absoluteUrl(slugToHref(key, hrefPrefix), siteUrl) };
|
|
234
81
|
}
|
|
235
82
|
// ── Internals: page lookup + URL math ───────────────────────────
|
|
236
83
|
function indexPagesBySlug(pages) {
|
|
@@ -252,14 +99,45 @@ function hrefToSlug(href, hrefPrefix) {
|
|
|
252
99
|
function slugToHref(slug, hrefPrefix) {
|
|
253
100
|
const prefix = hrefPrefix.endsWith("/") ? hrefPrefix.slice(0, -1) : hrefPrefix;
|
|
254
101
|
if (slug === "" || slug === "index")
|
|
255
|
-
return
|
|
102
|
+
return indexMirrorHref(prefix);
|
|
256
103
|
return `${prefix}/${slug}.md`;
|
|
257
104
|
}
|
|
105
|
+
/**
|
|
106
|
+
* The site index's markdown mirror.
|
|
107
|
+
*
|
|
108
|
+
* At a mount this is `<prefix>.md` — verified against the deployed
|
|
109
|
+
* blog, where `/blog.md` serves and `/blog/index.md` 404s.
|
|
110
|
+
*
|
|
111
|
+
* At the HOST ROOT the prefix is empty, so that form degenerates to
|
|
112
|
+
* `.md`: a relative path that resolves against whatever directory the
|
|
113
|
+
* reader happens to be in, and absolutises to `https://host/.md`. The
|
|
114
|
+
* route Astro actually emits there is `dist/index.md`.
|
|
115
|
+
*
|
|
116
|
+
* The bug was invisible on every site we had, because a mounted site
|
|
117
|
+
* takes the other branch and the one host-root site had no index entry
|
|
118
|
+
* in its llms.txt.
|
|
119
|
+
*/
|
|
120
|
+
function indexMirrorHref(prefix) {
|
|
121
|
+
return prefix ? `${prefix}.md` : "/index.md";
|
|
122
|
+
}
|
|
258
123
|
function absoluteUrl(path, siteUrl) {
|
|
259
124
|
if (!siteUrl)
|
|
260
125
|
return path;
|
|
261
|
-
|
|
262
|
-
|
|
126
|
+
// Strip the path component from siteUrl. With the two-prefix
|
|
127
|
+
// model (urlBase from site.url's path drives Astro's `base`,
|
|
128
|
+
// basePath stays as filesystem prefix), `path` already carries
|
|
129
|
+
// the combined prefix. Concatenating siteUrl-with-path would
|
|
130
|
+
// double-count. Use only the origin.
|
|
131
|
+
// See plans/astro-base-from-site-url.md.
|
|
132
|
+
let origin;
|
|
133
|
+
try {
|
|
134
|
+
const u = new URL(siteUrl);
|
|
135
|
+
origin = `${u.protocol}//${u.host}`;
|
|
136
|
+
}
|
|
137
|
+
catch {
|
|
138
|
+
origin = siteUrl.replace(/\/$/, "");
|
|
139
|
+
}
|
|
140
|
+
return path.startsWith("/") ? `${origin}${path}` : `${origin}/${path}`;
|
|
263
141
|
}
|
|
264
142
|
// ── Internals: description + plain-text extraction ──────────────
|
|
265
143
|
function getPageDescription(page, limit) {
|
|
@@ -270,73 +148,3 @@ function getPageDescription(page, limit) {
|
|
|
270
148
|
const firstPara = extractFirstParagraph(page.tree);
|
|
271
149
|
return firstPara ? truncate(firstPara, limit) : "";
|
|
272
150
|
}
|
|
273
|
-
/**
|
|
274
|
-
* Extract plain text of the first paragraph in a page tree. Cross-shape
|
|
275
|
-
* tolerant: handles `node.inline` (dogsbay-md parser) and
|
|
276
|
-
* `node.children[{type: "prose", inline}]` (Starlight importer).
|
|
277
|
-
*/
|
|
278
|
-
export function extractFirstParagraph(tree) {
|
|
279
|
-
for (const node of tree) {
|
|
280
|
-
if (node.type !== "paragraph")
|
|
281
|
-
continue;
|
|
282
|
-
const inline = node.inline ?? findProseInline(node.children);
|
|
283
|
-
if (inline)
|
|
284
|
-
return inlineToPlainText(inline).trim();
|
|
285
|
-
}
|
|
286
|
-
return "";
|
|
287
|
-
}
|
|
288
|
-
function findProseInline(children) {
|
|
289
|
-
if (!children)
|
|
290
|
-
return undefined;
|
|
291
|
-
for (const child of children) {
|
|
292
|
-
if (child.type === "prose" && child.inline)
|
|
293
|
-
return child.inline;
|
|
294
|
-
}
|
|
295
|
-
return undefined;
|
|
296
|
-
}
|
|
297
|
-
function inlineToPlainText(inline) {
|
|
298
|
-
let out = "";
|
|
299
|
-
for (const node of inline) {
|
|
300
|
-
switch (node.type) {
|
|
301
|
-
case "text":
|
|
302
|
-
out += node.text;
|
|
303
|
-
break;
|
|
304
|
-
case "code":
|
|
305
|
-
out += node.text;
|
|
306
|
-
break;
|
|
307
|
-
case "link":
|
|
308
|
-
case "highlight":
|
|
309
|
-
out += inlineToPlainText(node.children);
|
|
310
|
-
break;
|
|
311
|
-
case "kbd":
|
|
312
|
-
out += node.keys.join("+");
|
|
313
|
-
break;
|
|
314
|
-
case "math":
|
|
315
|
-
out += node.latex;
|
|
316
|
-
break;
|
|
317
|
-
case "image":
|
|
318
|
-
if (node.alt)
|
|
319
|
-
out += node.alt;
|
|
320
|
-
break;
|
|
321
|
-
case "break":
|
|
322
|
-
out += " ";
|
|
323
|
-
break;
|
|
324
|
-
case "html-inline":
|
|
325
|
-
case "footnote-ref":
|
|
326
|
-
case "icon":
|
|
327
|
-
// No useful plain text; skip.
|
|
328
|
-
break;
|
|
329
|
-
}
|
|
330
|
-
}
|
|
331
|
-
return out;
|
|
332
|
-
}
|
|
333
|
-
function truncate(text, limit) {
|
|
334
|
-
const collapsed = text.replace(/\s+/g, " ").trim();
|
|
335
|
-
if (collapsed.length <= limit)
|
|
336
|
-
return collapsed;
|
|
337
|
-
const slice = collapsed.slice(0, limit);
|
|
338
|
-
const lastSpace = slice.lastIndexOf(" ");
|
|
339
|
-
const cut = lastSpace > limit * 0.5 ? slice.slice(0, lastSpace) : slice;
|
|
340
|
-
return `${cut.trimEnd()}…`;
|
|
341
|
-
}
|
|
342
|
-
//# sourceMappingURL=llms-txt.js.map
|
package/dist/plugins.d.ts
CHANGED
|
@@ -37,4 +37,3 @@ export interface EmitPluginRuntimeOptions {
|
|
|
37
37
|
export declare function emitPluginRuntime(options: EmitPluginRuntimeOptions): void;
|
|
38
38
|
/** Test-only: clear emitted plugin runtime files. */
|
|
39
39
|
export declare function _clearPluginRuntime(outputDir: string): void;
|
|
40
|
-
//# sourceMappingURL=plugins.d.ts.map
|
package/dist/plugins.js
CHANGED
|
@@ -37,7 +37,9 @@
|
|
|
37
37
|
*
|
|
38
38
|
* See plans/plugin-api.md.
|
|
39
39
|
*/
|
|
40
|
-
import {
|
|
40
|
+
import { readFileSync, existsSync, mkdirSync, rmSync } from "node:fs";
|
|
41
|
+
// Unchanged files are not rewritten, so `site dev` reloads only what changed
|
|
42
|
+
import { writeFileSync, copyFileSync } from "./write-if-changed.js";
|
|
41
43
|
import { dirname, join, basename, extname } from "node:path";
|
|
42
44
|
/**
|
|
43
45
|
* Slots the runtime emits stack files for. Each entry is always
|
|
@@ -46,7 +48,7 @@ import { dirname, join, basename, extname } from "node:path";
|
|
|
46
48
|
* requires both an entry here and a usage site in format-astro's
|
|
47
49
|
* page emitter.
|
|
48
50
|
*/
|
|
49
|
-
const KNOWN_SLOTS = ["MarkdownContent"];
|
|
51
|
+
const KNOWN_SLOTS = ["MarkdownContent", "RightRail"];
|
|
50
52
|
const PLUGIN_RUNTIME_REL = "src/lib/plugin-runtime.ts";
|
|
51
53
|
const PLUGIN_CONFIG_DIR_REL = "src/lib/plugin-config";
|
|
52
54
|
const PLUGIN_STYLES_DIR_REL = "src/styles/plugins";
|
|
@@ -336,4 +338,3 @@ export function _clearPluginRuntime(outputDir) {
|
|
|
336
338
|
rmSync(join(outputDir, PLUGIN_CONFIG_DIR_REL), { recursive: true, force: true });
|
|
337
339
|
rmSync(join(outputDir, PLUGIN_STYLES_DIR_REL), { recursive: true, force: true });
|
|
338
340
|
}
|
|
339
|
-
//# sourceMappingURL=plugins.js.map
|