@dogsbay/site-files 0.0.0-stage → 0.2.0-beta.117

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.
@@ -0,0 +1,84 @@
1
+ /**
2
+ * Which URL a page of a versioned site names as its canonical, and whether
3
+ * it goes in the sitemap (dogsbay-ssg plans/canonical-versions.md, step 2).
4
+ * Both pipelines call this per page and turn the result into their own
5
+ * URLs: a page of an older version points at the same page in the default
6
+ * version, so search engines index the current docs.
7
+ *
8
+ * | Page | Canonical | Sitemap |
9
+ * |---|---|---|
10
+ * | in the default version | itself | yes |
11
+ * | older, and the default version has the page | that page | no |
12
+ * | older, and a redirect in the default version covers its path | the redirect's target | no |
13
+ * | older, only in its version | itself | yes |
14
+ * | in a `noindex: true` version | itself (robots noindex) | no |
15
+ * | in a `canonical: self` version | itself | yes |
16
+ *
17
+ * `sitemap: true | false` on a version overrides the last column (with
18
+ * `noindex`, true is refused at config load). A page's own front matter
19
+ * `canonicalUrl` (an absolute URL) wins over all of it; a redirect is
20
+ * followed only when it ends on a page of the default version or on
21
+ * another site.
22
+ */
23
+ /** Per-version settings (`content.versions[]`). */
24
+ export interface VersionSeo {
25
+ /** `self`: every page is its own canonical. Default: the canonical-to-latest rule. */
26
+ canonical?: "self";
27
+ noindex?: boolean;
28
+ /** Override whether the version's pages are listed in the sitemap. */
29
+ sitemap?: boolean;
30
+ }
31
+ export interface CanonicalQuery {
32
+ /** The page is in the default version (or the site has no version axis). */
33
+ isDefault: boolean;
34
+ /** The page's path inside its version (`guide/install`, `index`). */
35
+ path: string;
36
+ /** Its version's settings. */
37
+ version?: VersionSeo;
38
+ /** The page's front matter `canonicalUrl`, when set. Only an absolute URL counts. */
39
+ frontmatter?: string;
40
+ /** The page's own absolute URL, when known: a `canonicalUrl` equal to it is the page itself. */
41
+ selfUrl?: string;
42
+ /** Whether the default version (same language) has a live page at this path. */
43
+ defaultHas: (path: string) => boolean;
44
+ /**
45
+ * Where a redirect in the default version sends this path, chains
46
+ * collapsed: a path inside the version, a site path with a leading slash
47
+ * (outside the version: not followed), or a URL. Undefined when none.
48
+ */
49
+ defaultRedirect?: (path: string) => string | undefined;
50
+ }
51
+ export type CanonicalTarget =
52
+ /** The page itself. */
53
+ {
54
+ kind: "self";
55
+ }
56
+ /** A page of the default version, by its path inside it. */
57
+ | {
58
+ kind: "default";
59
+ path: string;
60
+ }
61
+ /** An absolute URL, as written. */
62
+ | {
63
+ kind: "url";
64
+ url: string;
65
+ };
66
+ export interface CanonicalDecision {
67
+ target: CanonicalTarget;
68
+ /** List the page in the sitemap. */
69
+ sitemap: boolean;
70
+ /** Mark the page `noindex`. */
71
+ noindex: boolean;
72
+ /** Why the page's `canonicalUrl` was ignored, for the build's warnings. */
73
+ warning?: string;
74
+ }
75
+ export declare function decideCanonical(q: CanonicalQuery): CanonicalDecision;
76
+ /** Check every version's settings, and that the default version is not `noindex` (older pages would point at noindex pages). */
77
+ export declare function checkVersionsSeo(versions: Array<{
78
+ id: string;
79
+ } & Record<string, unknown>>, defaultVersion: string | undefined): string | undefined;
80
+ /**
81
+ * Check one version's settings; returns an error message or undefined.
82
+ * `noindex` with `sitemap: true` lists pages search engines are told to skip.
83
+ */
84
+ export declare function checkVersionSeo(id: string, v: Record<string, unknown>): string | undefined;
@@ -0,0 +1,82 @@
1
+ /**
2
+ * Which URL a page of a versioned site names as its canonical, and whether
3
+ * it goes in the sitemap (dogsbay-ssg plans/canonical-versions.md, step 2).
4
+ * Both pipelines call this per page and turn the result into their own
5
+ * URLs: a page of an older version points at the same page in the default
6
+ * version, so search engines index the current docs.
7
+ *
8
+ * | Page | Canonical | Sitemap |
9
+ * |---|---|---|
10
+ * | in the default version | itself | yes |
11
+ * | older, and the default version has the page | that page | no |
12
+ * | older, and a redirect in the default version covers its path | the redirect's target | no |
13
+ * | older, only in its version | itself | yes |
14
+ * | in a `noindex: true` version | itself (robots noindex) | no |
15
+ * | in a `canonical: self` version | itself | yes |
16
+ *
17
+ * `sitemap: true | false` on a version overrides the last column (with
18
+ * `noindex`, true is refused at config load). A page's own front matter
19
+ * `canonicalUrl` (an absolute URL) wins over all of it; a redirect is
20
+ * followed only when it ends on a page of the default version or on
21
+ * another site.
22
+ */
23
+ const URL_RE = /^[a-z][a-z0-9+.-]*:/i;
24
+ const ABSOLUTE = /^https?:\/\//i;
25
+ const same = (a, b) => a.replace(/\/+$/, "") === b.replace(/\/+$/, "");
26
+ export function decideCanonical(q) {
27
+ const v = q.version ?? {};
28
+ const self = (warning) => (v.noindex ? { target: { kind: "self" }, sitemap: false, noindex: true } : { target: { kind: "self" }, sitemap: v.sitemap ?? true, noindex: false, ...(warning ? { warning } : {}) });
29
+ const override = q.frontmatter?.trim();
30
+ let warning;
31
+ if (override) {
32
+ if (!ABSOLUTE.test(override)) {
33
+ warning = `canonicalUrl "${override}" is not an absolute URL (https://…); ignored`;
34
+ }
35
+ else if (q.selfUrl && same(override, q.selfUrl)) {
36
+ return self();
37
+ }
38
+ else {
39
+ // A page that names another page as canonical is not listed, unless its version says so.
40
+ return { target: { kind: "url", url: override }, sitemap: v.sitemap ?? false, noindex: !!v.noindex };
41
+ }
42
+ }
43
+ if (v.noindex || q.isDefault || v.canonical === "self")
44
+ return self(warning);
45
+ const moved = (target) => ({ target, sitemap: v.sitemap ?? false, noindex: false, ...(warning ? { warning } : {}) });
46
+ if (q.defaultHas(q.path))
47
+ return moved({ kind: "default", path: q.path });
48
+ const r = q.defaultRedirect?.(q.path);
49
+ // Followed only to a page of the default version, or to another site: a redirect out of the
50
+ // version (to an archived copy) leaves the page on itself, and in the sitemap.
51
+ if (r !== undefined && URL_RE.test(r))
52
+ return moved({ kind: "url", url: r });
53
+ if (r !== undefined && !r.startsWith("/") && r !== q.path && q.defaultHas(r))
54
+ return moved({ kind: "default", path: r });
55
+ return self(warning);
56
+ }
57
+ /** Check every version's settings, and that the default version is not `noindex` (older pages would point at noindex pages). */
58
+ export function checkVersionsSeo(versions, defaultVersion) {
59
+ for (const v of versions) {
60
+ const bad = checkVersionSeo(v.id, v);
61
+ if (bad)
62
+ return bad;
63
+ if (v.id === defaultVersion && v.noindex === true)
64
+ return `versions: "${v.id}" is the defaultVersion and has noindex: true; older versions' pages name it as their canonical`;
65
+ }
66
+ return undefined;
67
+ }
68
+ /**
69
+ * Check one version's settings; returns an error message or undefined.
70
+ * `noindex` with `sitemap: true` lists pages search engines are told to skip.
71
+ */
72
+ export function checkVersionSeo(id, v) {
73
+ if (v.canonical !== undefined && v.canonical !== "self")
74
+ return `versions: "${id}".canonical must be "self" (or left out for the canonical-to-latest rule); got ${JSON.stringify(v.canonical)}`;
75
+ if (v.noindex !== undefined && typeof v.noindex !== "boolean")
76
+ return `versions: "${id}".noindex must be true or false`;
77
+ if (v.sitemap !== undefined && typeof v.sitemap !== "boolean")
78
+ return `versions: "${id}".sitemap must be true or false`;
79
+ if (v.noindex === true && v.sitemap === true)
80
+ return `versions: "${id}" has noindex: true and sitemap: true; a sitemap should not list pages search engines are told to skip`;
81
+ return undefined;
82
+ }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * `hreflang` alternates for a page that exists in several languages: every
3
+ * copy lists all of them (itself included) and `x-default`, the default
4
+ * language's copy. Built the same way by dogsbay-ssg and the Astro pipeline;
5
+ * each renders `<link rel="alternate" hreflang=… href=…>`.
6
+ */
7
+ export interface HreflangLink {
8
+ hreflang: string;
9
+ href: string;
10
+ }
11
+ /**
12
+ * The links for one page, from its copies by language (absolute URLs).
13
+ * Fewer than two languages: none. Sorted by language, `x-default` last, when
14
+ * the default language has the page.
15
+ */
16
+ export declare function buildHreflangLinks(alternates: {
17
+ lang: string;
18
+ url: string;
19
+ }[], defaultLang?: string): HreflangLink[];
@@ -0,0 +1,18 @@
1
+ /**
2
+ * The links for one page, from its copies by language (absolute URLs).
3
+ * Fewer than two languages: none. Sorted by language, `x-default` last, when
4
+ * the default language has the page.
5
+ */
6
+ export function buildHreflangLinks(alternates, defaultLang) {
7
+ const byLang = new Map();
8
+ for (const a of alternates)
9
+ if (a.lang && a.url && !byLang.has(a.lang))
10
+ byLang.set(a.lang, a.url);
11
+ if (byLang.size < 2)
12
+ return [];
13
+ const links = [...byLang].sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0)).map(([hreflang, href]) => ({ hreflang, href }));
14
+ const def = defaultLang !== undefined ? byLang.get(defaultLang) : undefined;
15
+ if (def)
16
+ links.push({ hreflang: "x-default", href: def });
17
+ return links;
18
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * @dogsbay/site-files — the files a docs site ships beside its pages, built
3
+ * the same way by the Astro pipeline (format-astro) and dogsbay-ssg. Pure and
4
+ * Worker-safe: strings in, strings out. See plans/site-files-package.md.
5
+ */
6
+ export * from "./json-ld.js";
7
+ export * from "./text.js";
8
+ export * from "./robots.js";
9
+ export * from "./sitemap.js";
10
+ export * from "./llms.js";
11
+ export * from "./redirects.js";
12
+ export * from "./canonical.js";
13
+ export * from "./hreflang.js";
14
+ export * from "./translations.js";
package/dist/index.js ADDED
@@ -0,0 +1,14 @@
1
+ /**
2
+ * @dogsbay/site-files — the files a docs site ships beside its pages, built
3
+ * the same way by the Astro pipeline (format-astro) and dogsbay-ssg. Pure and
4
+ * Worker-safe: strings in, strings out. See plans/site-files-package.md.
5
+ */
6
+ export * from "./json-ld.js";
7
+ export * from "./text.js";
8
+ export * from "./robots.js";
9
+ export * from "./sitemap.js";
10
+ export * from "./llms.js";
11
+ export * from "./redirects.js";
12
+ export * from "./canonical.js";
13
+ export * from "./hreflang.js";
14
+ export * from "./translations.js";
@@ -0,0 +1,105 @@
1
+ /**
2
+ * JSON-LD helpers — pure functions consumed by DocsLayout and dogsbay-ssg to
3
+ * decide which Schema.org `@type` to emit and to normalize the
4
+ * `customJsonLd` prop shape.
5
+ *
6
+ * The 100daysofdocs SEO audit flagged that DocsLayout was emitting
7
+ * `Article` for every page regardless of intent — tutorials and
8
+ * how-to pages should render as `HowTo` for SERP rich results.
9
+ * This module owns the type selection so it's testable without
10
+ * spinning up an Astro renderer.
11
+ */
12
+ /**
13
+ * Map a `meta.type` value (the open-string `pageType` prop on
14
+ * DocsLayout) to a Schema.org `@type`.
15
+ *
16
+ * The mapping intentionally accepts a few common spelling
17
+ * variants (`"how-to"` / `"howto"`, `"reference"` / `"ref"` /
18
+ * `"api"`) so writers don't have to learn one canonical spelling
19
+ * to get the right structured data. Anything else falls back to
20
+ * `Article`, which is Google's "general docs page" assumption.
21
+ */
22
+ export declare function jsonLdTypeFor(pageType: string | undefined): string;
23
+ /**
24
+ * Normalize the `customJsonLd` prop into an array. DocsLayout
25
+ * accepts either a single object (most common case — one extra
26
+ * Course / Organization block) or an array (sites that layer
27
+ * several blocks per page).
28
+ *
29
+ * Returns an empty array for `undefined` so the template can
30
+ * iterate uniformly without conditional rendering.
31
+ */
32
+ export declare function normalizeCustomJsonLd(raw: Record<string, unknown> | Record<string, unknown>[] | undefined): Record<string, unknown>[];
33
+ export interface BuildArticleJsonLdOptions {
34
+ /** Already-resolved Schema.org `@type` (output of jsonLdTypeFor). */
35
+ type: string;
36
+ title: string;
37
+ /** Site name — used as the `provider` Organization for Course. */
38
+ siteName: string;
39
+ keywords: string[];
40
+ description?: string;
41
+ image?: string;
42
+ url?: string;
43
+ /**
44
+ * Page headings — used to synthesize `step[]` for HowTo. Each
45
+ * H2 (or H3 if no H2s exist) becomes a HowToStep with a deep
46
+ * link to the heading id.
47
+ */
48
+ headings?: ReadonlyArray<{
49
+ depth: number;
50
+ slug: string;
51
+ text: string;
52
+ }>;
53
+ }
54
+ /**
55
+ * Build the JSON-LD payload for the page's primary structured-data
56
+ * block, shaped per `@type`. The earlier implementation emitted
57
+ * Article-shaped fields (`headline`) regardless of @type, which
58
+ * meant HowTo / Course pages failed Google's Rich Results
59
+ * requirements (HowTo needs `name` + `step`, Course needs `name` +
60
+ * `description` + `provider`). See
61
+ * `packages/site-check/src/rules/seo/json-ld-required-fields.ts`
62
+ * for the validator that catches this.
63
+ *
64
+ * Schema.org's `name` is a Thing-level field accepted by every
65
+ * @type, so we always emit it. `headline` is added on top for
66
+ * Article-family types (Article, TechArticle) because Google's
67
+ * Rich Results validator specifically requires it there. HowTo
68
+ * gets a synthesized `step[]` from the page's H2 headings (each
69
+ * H2 = one procedure step); Course gets a `provider` Organization
70
+ * built from `siteName`.
71
+ */
72
+ export declare function buildArticleJsonLd(opts: BuildArticleJsonLdOptions): Record<string, unknown>;
73
+ /**
74
+ * Identity structured data for a page that is not an article — a
75
+ * homepage, a landing page, a section index.
76
+ *
77
+ * Exists because the article block is gated on `ogType === "article"`,
78
+ * so those pages emitted NO structured data at all. An external agent
79
+ * audit reported "No JSON-LD structured data found on homepage", which
80
+ * was accurate: the one page most likely to be an agent's entry point
81
+ * was the one describing itself the least.
82
+ *
83
+ * `WebSite` rather than `Organization` or `SoftwareApplication` on
84
+ * purpose. Those are claims about what the site's OWNER is, and
85
+ * guessing wrong is worse than saying less — a docs site for a product
86
+ * is not itself the product. Owners wanting a stronger identity type
87
+ * add it through `customJsonLd`, which layers on top of this.
88
+ */
89
+ export declare function buildWebSiteJsonLd(opts: {
90
+ siteName: string;
91
+ title: string;
92
+ description?: string;
93
+ url?: string;
94
+ }): Record<string, unknown>;
95
+ /**
96
+ * JSON-LD `BreadcrumbList` for a page: the nav trail from the top level down
97
+ * to the page, with absolute URLs. An entry without a URL (a section with no
98
+ * page of its own) is left out, since search engines require `item` on every
99
+ * entry but the last; positions count the entries kept. Fewer than two
100
+ * entries say nothing a breadcrumb adds: undefined.
101
+ */
102
+ export declare function buildBreadcrumbJsonLd(items: {
103
+ name: string;
104
+ url?: string;
105
+ }[]): Record<string, unknown> | undefined;
@@ -0,0 +1,161 @@
1
+ /**
2
+ * JSON-LD helpers — pure functions consumed by DocsLayout and dogsbay-ssg to
3
+ * decide which Schema.org `@type` to emit and to normalize the
4
+ * `customJsonLd` prop shape.
5
+ *
6
+ * The 100daysofdocs SEO audit flagged that DocsLayout was emitting
7
+ * `Article` for every page regardless of intent — tutorials and
8
+ * how-to pages should render as `HowTo` for SERP rich results.
9
+ * This module owns the type selection so it's testable without
10
+ * spinning up an Astro renderer.
11
+ */
12
+ /**
13
+ * Map a `meta.type` value (the open-string `pageType` prop on
14
+ * DocsLayout) to a Schema.org `@type`.
15
+ *
16
+ * The mapping intentionally accepts a few common spelling
17
+ * variants (`"how-to"` / `"howto"`, `"reference"` / `"ref"` /
18
+ * `"api"`) so writers don't have to learn one canonical spelling
19
+ * to get the right structured data. Anything else falls back to
20
+ * `Article`, which is Google's "general docs page" assumption.
21
+ */
22
+ export function jsonLdTypeFor(pageType) {
23
+ switch (pageType) {
24
+ case "tutorial":
25
+ case "how-to":
26
+ case "howto":
27
+ return "HowTo";
28
+ case "reference":
29
+ case "ref":
30
+ case "api":
31
+ return "TechArticle";
32
+ case "course":
33
+ return "Course";
34
+ default:
35
+ return "Article";
36
+ }
37
+ }
38
+ /**
39
+ * Normalize the `customJsonLd` prop into an array. DocsLayout
40
+ * accepts either a single object (most common case — one extra
41
+ * Course / Organization block) or an array (sites that layer
42
+ * several blocks per page).
43
+ *
44
+ * Returns an empty array for `undefined` so the template can
45
+ * iterate uniformly without conditional rendering.
46
+ */
47
+ export function normalizeCustomJsonLd(raw) {
48
+ if (!raw)
49
+ return [];
50
+ if (Array.isArray(raw))
51
+ return raw;
52
+ return [raw];
53
+ }
54
+ /**
55
+ * Build the JSON-LD payload for the page's primary structured-data
56
+ * block, shaped per `@type`. The earlier implementation emitted
57
+ * Article-shaped fields (`headline`) regardless of @type, which
58
+ * meant HowTo / Course pages failed Google's Rich Results
59
+ * requirements (HowTo needs `name` + `step`, Course needs `name` +
60
+ * `description` + `provider`). See
61
+ * `packages/site-check/src/rules/seo/json-ld-required-fields.ts`
62
+ * for the validator that catches this.
63
+ *
64
+ * Schema.org's `name` is a Thing-level field accepted by every
65
+ * @type, so we always emit it. `headline` is added on top for
66
+ * Article-family types (Article, TechArticle) because Google's
67
+ * Rich Results validator specifically requires it there. HowTo
68
+ * gets a synthesized `step[]` from the page's H2 headings (each
69
+ * H2 = one procedure step); Course gets a `provider` Organization
70
+ * built from `siteName`.
71
+ */
72
+ export function buildArticleJsonLd(opts) {
73
+ const { type, title, siteName, keywords, description, image, url, headings } = opts;
74
+ const block = {
75
+ "@context": "https://schema.org",
76
+ "@type": type,
77
+ name: title,
78
+ };
79
+ // Only when there are any. An empty `keywords: ""` asserts the page
80
+ // has no topics, which is a different claim from staying silent.
81
+ if (keywords.length > 0)
82
+ block.keywords = keywords.join(", ");
83
+ if (type === "Article" || type === "TechArticle") {
84
+ block.headline = title;
85
+ }
86
+ if (description)
87
+ block.description = description;
88
+ if (image)
89
+ block.image = image;
90
+ if (url)
91
+ block.url = url;
92
+ if (type === "HowTo") {
93
+ let stepHeadings = (headings ?? []).filter((h) => h.depth === 2);
94
+ if (stepHeadings.length === 0) {
95
+ stepHeadings = (headings ?? []).filter((h) => h.depth === 3);
96
+ }
97
+ block.step = stepHeadings.map((h) => ({
98
+ "@type": "HowToStep",
99
+ name: h.text,
100
+ url: url ? `${url}#${h.slug}` : `#${h.slug}`,
101
+ }));
102
+ }
103
+ if (type === "Course") {
104
+ block.provider = { "@type": "Organization", name: siteName };
105
+ if (!block.description)
106
+ block.description = title;
107
+ }
108
+ return block;
109
+ }
110
+ /**
111
+ * Identity structured data for a page that is not an article — a
112
+ * homepage, a landing page, a section index.
113
+ *
114
+ * Exists because the article block is gated on `ogType === "article"`,
115
+ * so those pages emitted NO structured data at all. An external agent
116
+ * audit reported "No JSON-LD structured data found on homepage", which
117
+ * was accurate: the one page most likely to be an agent's entry point
118
+ * was the one describing itself the least.
119
+ *
120
+ * `WebSite` rather than `Organization` or `SoftwareApplication` on
121
+ * purpose. Those are claims about what the site's OWNER is, and
122
+ * guessing wrong is worse than saying less — a docs site for a product
123
+ * is not itself the product. Owners wanting a stronger identity type
124
+ * add it through `customJsonLd`, which layers on top of this.
125
+ */
126
+ export function buildWebSiteJsonLd(opts) {
127
+ const block = {
128
+ "@context": "https://schema.org",
129
+ "@type": "WebSite",
130
+ name: opts.siteName,
131
+ };
132
+ if (opts.title && opts.title !== opts.siteName)
133
+ block.headline = opts.title;
134
+ if (opts.description)
135
+ block.description = opts.description;
136
+ if (opts.url)
137
+ block.url = opts.url;
138
+ return block;
139
+ }
140
+ /**
141
+ * JSON-LD `BreadcrumbList` for a page: the nav trail from the top level down
142
+ * to the page, with absolute URLs. An entry without a URL (a section with no
143
+ * page of its own) is left out, since search engines require `item` on every
144
+ * entry but the last; positions count the entries kept. Fewer than two
145
+ * entries say nothing a breadcrumb adds: undefined.
146
+ */
147
+ export function buildBreadcrumbJsonLd(items) {
148
+ const kept = items.filter((it, i) => it.url || i === items.length - 1);
149
+ if (kept.length < 2)
150
+ return undefined;
151
+ return {
152
+ "@context": "https://schema.org",
153
+ "@type": "BreadcrumbList",
154
+ itemListElement: kept.map((it, i) => ({
155
+ "@type": "ListItem",
156
+ position: i + 1,
157
+ name: it.name,
158
+ ...(it.url ? { item: it.url } : {}),
159
+ })),
160
+ };
161
+ }
package/dist/llms.d.ts ADDED
@@ -0,0 +1,72 @@
1
+ /**
2
+ * llms.txt, the per-section llms.txt files and llms-full.txt (llmstxt.org).
3
+ *
4
+ * The pipelines pass small records, not their page models: each page's key
5
+ * (its slug), title and description (already cut with `truncate`), and the
6
+ * nav as keys. They also pass `url(key)`, the link an entry gets: Astro's
7
+ * `/docs/x.md` (absolute from the origin with `site.url`), dogsbay-ssg's
8
+ * `x.md` relative to the file or absolute. Page bodies are never held here:
9
+ * llms-full.txt reads each through `load(key)` and streams.
10
+ */
11
+ export interface LlmsSite {
12
+ name: string;
13
+ description?: string;
14
+ /** "When to use this": what the site is good for. Omitted when unset. */
15
+ whenToUse?: string;
16
+ /** Other sites on this host, linked at the end of llms.txt (`site.aggregates`). */
17
+ aggregates?: LlmsAggregate[];
18
+ }
19
+ export interface LlmsAggregate {
20
+ name: string;
21
+ description?: string;
22
+ url?: string;
23
+ llmsTxt?: string;
24
+ }
25
+ export interface LlmsPage {
26
+ title: string;
27
+ /** Shown after the link, as given (cut it with `truncate`). */
28
+ description?: string;
29
+ }
30
+ export interface LlmsNav {
31
+ label: string;
32
+ /** The page's key; absent for a label-only group. */
33
+ key?: string;
34
+ /** Appended to the entry's URL (`#section`). */
35
+ fragment?: string;
36
+ children: LlmsNav[];
37
+ }
38
+ export interface LlmsOptions {
39
+ /** The URL of a page's entry. */
40
+ url(key: string): string;
41
+ /** A nav entry whose key has no page: `"skip"` it (dogsbay-ssg) or `"link"` it by its label (Astro). Default `"skip"`. */
42
+ missing?: "skip" | "link";
43
+ /** Escape `[` and `]` in labels, which would end the link text early (dogsbay-ssg). Default false. */
44
+ escapeLabels?: boolean;
45
+ /** Heading for top-level pages outside a group. Default "Documentation". */
46
+ topLevelLabel?: string;
47
+ /** Heading for pages not in the nav, or false to leave them out. Default "Other pages". */
48
+ otherPagesLabel?: string | false;
49
+ /** Heading for `site.aggregates`. Default "Other sites". */
50
+ aggregatesLabel?: string;
51
+ }
52
+ type Pages = ReadonlyMap<string, LlmsPage>;
53
+ /**
54
+ * The site's llms.txt: the title, description and "When to use this", then
55
+ * top-level pages under "Documentation", one section per top-level nav group,
56
+ * pages outside the nav under "Other pages" (sorted by key), and other sites.
57
+ */
58
+ export declare function buildLlmsTxt(site: LlmsSite, nav: LlmsNav[], pages: Pages, o: LlmsOptions): string;
59
+ /** One top-level nav group's own llms.txt. */
60
+ export declare function buildSectionLlmsTxt(site: Pick<LlmsSite, "name">, group: LlmsNav, pages: Pages, o: LlmsOptions): string;
61
+ /** Page keys in nav order, then pages outside the nav sorted by key: the llms-full.txt order. */
62
+ export declare function llmsFullOrder(nav: LlmsNav[], pages: Pages): string[];
63
+ /**
64
+ * llms-full.txt, one chunk per page so the file is never held whole: the
65
+ * header, then for each page `# Title`, `URL:` and its Markdown (from
66
+ * `load`), separated by `---`. Joined, the chunks equal `buildLlmsFullTxt`.
67
+ * `site.whenToUse` is written when given (neither pipeline passes it).
68
+ */
69
+ export declare function llmsFullChunks(site: LlmsSite, order: string[], pages: Pages, url: (key: string) => string, load: (key: string) => string | Promise<string>): AsyncGenerator<string>;
70
+ /** llms-full.txt as one string, for a caller that has every page at hand. */
71
+ export declare function buildLlmsFullTxt(site: LlmsSite, order: string[], pages: Pages, url: (key: string) => string, load: (key: string) => string): string;
72
+ export {};