nuxt-ai-ready 3.2.0 → 3.3.1
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/chunks/agent-skills.mjs +2 -1
- package/dist/chunks/prerender.mjs +197 -28
- package/dist/cli.mjs +259 -0
- package/dist/module.d.mts +1 -0
- package/dist/module.json +1 -1
- package/dist/module.mjs +2 -1
- package/dist/runtime/llms-txt-files.d.ts +30 -0
- package/dist/runtime/llms-txt-files.js +196 -0
- package/dist/runtime/llms-txt-format.js +5 -4
- package/dist/runtime/llms-txt-utils.d.ts +3 -2
- package/dist/runtime/llms-txt-utils.js +85 -224
- package/dist/runtime/markdown-text.d.ts +2 -0
- package/dist/runtime/markdown-text.js +3 -0
- package/dist/runtime/server/db/drizzle/queries.d.ts +2 -6
- package/dist/runtime/server/db/drizzle/queries.js +30 -10
- package/dist/runtime/server/db/queries.d.ts +5 -6
- package/dist/runtime/server/db/queries.js +40 -9
- package/dist/runtime/server/db/schema/postgres.d.ts +30 -0
- package/dist/runtime/server/db/schema/postgres.js +1 -0
- package/dist/runtime/server/db/schema/sqlite.d.ts +32 -0
- package/dist/runtime/server/db/schema/sqlite.js +1 -0
- package/dist/runtime/server/db/schema-sql.d.ts +1 -1
- package/dist/runtime/server/db/schema-sql.js +2 -1
- package/dist/runtime/server/db/seed.d.ts +8 -0
- package/dist/runtime/server/db/seed.js +16 -0
- package/dist/runtime/server/db/shared.d.ts +3 -3
- package/dist/runtime/server/db/shared.js +19 -1
- package/dist/runtime/server/middleware/markdown.js +1 -1
- package/dist/runtime/server/plugins/sitemap-seeder.js +9 -1
- package/dist/runtime/server/routes/llms-full.txt.get.js +2 -1
- package/dist/runtime/server/routes/llms.txt.get.js +21 -11
- package/dist/runtime/server/routes/sitemap.md.get.js +2 -1
- package/dist/runtime/server/utils/llms-full.js +31 -5
- package/dist/runtime/server/utils/markdown-request.js +2 -5
- package/dist/runtime/server/utils/negotiation-response.js +3 -0
- package/dist/runtime/server/utils/runCron.js +1 -1
- package/dist/runtime/server/utils/sitemap-lastmod.d.ts +4 -0
- package/dist/runtime/server/utils/sitemap-lastmod.js +19 -0
- package/dist/runtime/server/utils/sitemap-md.js +3 -2
- package/dist/runtime/server/utils/sitemap.d.ts +1 -0
- package/dist/runtime/server/utils/sitemap.js +26 -12
- package/dist/runtime/server/utils.js +50 -2
- package/dist/runtime/types.d.ts +13 -1
- package/dist/shared/{nuxt-ai-ready.DH9mSayy.mjs → nuxt-ai-ready.CKXM1jnE.mjs} +40 -12
- package/package.json +3 -3
- package/skills/nuxt-ai-ready/SKILL.md +34 -1
package/dist/module.mjs
CHANGED
|
@@ -7,8 +7,9 @@ import '@nuxt/kit';
|
|
|
7
7
|
import 'defu';
|
|
8
8
|
import 'nuxt-site-config/kit';
|
|
9
9
|
import 'nuxtseo-shared/kit';
|
|
10
|
-
export { m as default } from './shared/nuxt-ai-ready.
|
|
10
|
+
export { m as default } from './shared/nuxt-ai-ready.CKXM1jnE.mjs';
|
|
11
11
|
import './shared/nuxt-ai-ready.B3hAuOLQ.mjs';
|
|
12
|
+
import '../dist/runtime/llms-txt-files.js';
|
|
12
13
|
import '../dist/runtime/route-path.js';
|
|
13
14
|
import '../dist/runtime/server/utils/sitemap-md.js';
|
|
14
15
|
import '../dist/runtime/server/utils/discovery-response.js';
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { LlmsTxtConfig, LlmsTxtSection } from './types.js';
|
|
2
|
+
export interface LlmsTxtPage {
|
|
3
|
+
pathname: string;
|
|
4
|
+
href?: string;
|
|
5
|
+
title?: string;
|
|
6
|
+
description?: string;
|
|
7
|
+
locale?: string;
|
|
8
|
+
}
|
|
9
|
+
export declare function llmsTxtSectionPath(section: LlmsTxtSection): string;
|
|
10
|
+
export type ParsedLlmsTxtConfig = {
|
|
11
|
+
_tag: 'Valid';
|
|
12
|
+
config: LlmsTxtConfig;
|
|
13
|
+
} | {
|
|
14
|
+
_tag: 'Invalid';
|
|
15
|
+
message: string;
|
|
16
|
+
};
|
|
17
|
+
/** Parse the public configuration once, before Nitro registers routes. */
|
|
18
|
+
export declare function parseLlmsTxtConfig(input: unknown): ParsedLlmsTxtConfig;
|
|
19
|
+
interface LlmsTxtFilesInput {
|
|
20
|
+
rootContent?: string;
|
|
21
|
+
header: string;
|
|
22
|
+
config: LlmsTxtConfig;
|
|
23
|
+
sections: LlmsTxtSection[];
|
|
24
|
+
pages: LlmsTxtPage[];
|
|
25
|
+
sitemapPages: ReadonlyMap<string, LlmsTxtPage[]>;
|
|
26
|
+
resolveUrl: (path: string) => string;
|
|
27
|
+
}
|
|
28
|
+
/** A bounded tree of file lists. Every page appears once; every file is reachable from the root. */
|
|
29
|
+
export declare function createLlmsTxtFiles(input: LlmsTxtFilesInput): Map<string, string>;
|
|
30
|
+
export {};
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import { formatLlmsTxtPageLink, normalizeLlmsTxtConfig } from "./llms-txt-format.js";
|
|
2
|
+
const FILE_PATH = /^\/llms\/[a-z0-9][\w-]*\.txt$/i;
|
|
3
|
+
const SITEMAP_NAME = /^[a-z0-9][\w-]*$/i;
|
|
4
|
+
export function llmsTxtSectionPath(section) {
|
|
5
|
+
const name = Array.isArray(section.sitemap) ? section.sitemap[0] : section.sitemap;
|
|
6
|
+
return section.path || `/llms/${name}.txt`;
|
|
7
|
+
}
|
|
8
|
+
export function parseLlmsTxtConfig(input) {
|
|
9
|
+
const invalid = (message) => ({ _tag: "Invalid", message });
|
|
10
|
+
if (!input || typeof input !== "object" || Array.isArray(input))
|
|
11
|
+
return invalid("llmsTxt must be an object or false.");
|
|
12
|
+
const config = input;
|
|
13
|
+
if (config.pages !== void 0 && config.pages !== false && config.pages !== "sitemap" && config.pages !== "all")
|
|
14
|
+
return invalid("llmsTxt.pages must be sitemap, all, or false.");
|
|
15
|
+
if (config.maxLength !== void 0 && (!Number.isSafeInteger(config.maxLength) || config.maxLength < 1024))
|
|
16
|
+
return invalid("llmsTxt.maxLength must be an integer of at least 1024.");
|
|
17
|
+
if (config.sitemaps !== void 0 && (!Array.isArray(config.sitemaps) || config.sitemaps.some((name) => typeof name !== "string" || !SITEMAP_NAME.test(name))))
|
|
18
|
+
return invalid("llmsTxt.sitemaps must contain sitemap names.");
|
|
19
|
+
if (config.sections !== void 0 && !Array.isArray(config.sections))
|
|
20
|
+
return invalid("llmsTxt.sections must be an array.");
|
|
21
|
+
const paths = /* @__PURE__ */ new Set();
|
|
22
|
+
for (const section of config.sections || []) {
|
|
23
|
+
if (!section || typeof section !== "object" || typeof section.title !== "string")
|
|
24
|
+
return invalid("Each llmsTxt section needs a title.");
|
|
25
|
+
if (section.sitemap === void 0) {
|
|
26
|
+
if (section.path !== void 0)
|
|
27
|
+
return invalid("A section path requires a sitemap.");
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
const names = Array.isArray(section.sitemap) ? section.sitemap : [section.sitemap];
|
|
31
|
+
if (!names.length || names.some((name) => typeof name !== "string" || !SITEMAP_NAME.test(name)))
|
|
32
|
+
return invalid("Each section sitemap must contain a sitemap name.");
|
|
33
|
+
const path = llmsTxtSectionPath(section);
|
|
34
|
+
if (!FILE_PATH.test(path))
|
|
35
|
+
return invalid("A section path must use /llms/<name>.txt.");
|
|
36
|
+
if (paths.has(path))
|
|
37
|
+
return invalid(`Duplicate llmsTxt section path: ${path}.`);
|
|
38
|
+
paths.add(path);
|
|
39
|
+
}
|
|
40
|
+
return { _tag: "Valid", config };
|
|
41
|
+
}
|
|
42
|
+
export function createLlmsTxtFiles(input) {
|
|
43
|
+
const files = /* @__PURE__ */ new Map();
|
|
44
|
+
const maxLength = input.config.maxLength ?? 8e4;
|
|
45
|
+
const link = (title, path, description) => formatLlmsTxtPageLink({ pathname: path, href: input.resolveUrl(path), title, description });
|
|
46
|
+
const render = (header, title, lines) => `${header.trim()}
|
|
47
|
+
|
|
48
|
+
## ${title.replace(/\s+/g, " ").trim()}
|
|
49
|
+
|
|
50
|
+
${lines.join("\n")}
|
|
51
|
+
`;
|
|
52
|
+
function tree(path, prefix, header, title, lines, level = 0) {
|
|
53
|
+
const body = render(header, title, lines);
|
|
54
|
+
if (body.length <= maxLength) {
|
|
55
|
+
files.set(path, body);
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
const overhead = render(header, title, []).length;
|
|
59
|
+
const chunks = [];
|
|
60
|
+
let chunk = [];
|
|
61
|
+
let length = overhead;
|
|
62
|
+
for (const line of lines) {
|
|
63
|
+
if (overhead + line.length + 1 > maxLength)
|
|
64
|
+
throw new Error("An llms.txt entry exceeds llmsTxt.maxLength. Shorten the entry or increase the limit.");
|
|
65
|
+
if (length + line.length + 1 > maxLength) {
|
|
66
|
+
chunks.push(chunk);
|
|
67
|
+
chunk = [];
|
|
68
|
+
length = overhead;
|
|
69
|
+
}
|
|
70
|
+
chunk.push(line);
|
|
71
|
+
length += line.length + 1;
|
|
72
|
+
}
|
|
73
|
+
if (chunk.length)
|
|
74
|
+
chunks.push(chunk);
|
|
75
|
+
const links = chunks.map((entries, index) => {
|
|
76
|
+
const child = `/llms/_${prefix}-${level}-${index + 1}.txt`;
|
|
77
|
+
files.set(child, render(header, title, entries));
|
|
78
|
+
return link(`${title} ${index + 1}`, child);
|
|
79
|
+
});
|
|
80
|
+
if (chunks.length >= lines.length && render(header, title, links).length >= body.length)
|
|
81
|
+
throw new Error("llmsTxt.maxLength cannot fit an index. Increase the limit.");
|
|
82
|
+
tree(path, prefix, header, title, links, level + 1);
|
|
83
|
+
}
|
|
84
|
+
const sections = [];
|
|
85
|
+
const assigned = /* @__PURE__ */ new Set();
|
|
86
|
+
for (const [index, section] of input.sections.entries()) {
|
|
87
|
+
if (!section.sitemap) {
|
|
88
|
+
sections.push(section);
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
const names = Array.isArray(section.sitemap) ? section.sitemap : [section.sitemap];
|
|
92
|
+
const pages2 = /* @__PURE__ */ new Map();
|
|
93
|
+
for (const name of names) {
|
|
94
|
+
for (const page of input.sitemapPages.get(name) || []) {
|
|
95
|
+
if (!assigned.has(page.pathname))
|
|
96
|
+
pages2.set(page.pathname, page);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
const entries = [...(section.links || []).map((entry) => formatLlmsTxtPageLink({ pathname: entry.href, ...entry })), ...[...pages2.values()].map(formatLlmsTxtPageLink)];
|
|
100
|
+
if (!entries.length)
|
|
101
|
+
continue;
|
|
102
|
+
for (const pathname of pages2.keys())
|
|
103
|
+
assigned.add(pathname);
|
|
104
|
+
const path = llmsTxtSectionPath(section);
|
|
105
|
+
const description = Array.isArray(section.description) ? section.description.join(" ") : section.description;
|
|
106
|
+
const preamble = normalizeLlmsTxtConfig({ notes: description });
|
|
107
|
+
tree(path, `section-${index + 1}`, `${input.header.trim()}
|
|
108
|
+
|
|
109
|
+
${preamble}
|
|
110
|
+
|
|
111
|
+
${link("llms.txt", "/llms.txt")}`, section.title, entries);
|
|
112
|
+
sections.push({ ...section, sitemap: void 0, path: void 0, links: [{ title: section.title, href: input.resolveUrl(path), description }] });
|
|
113
|
+
}
|
|
114
|
+
const rootPreamble = `${input.header.trim()}
|
|
115
|
+
|
|
116
|
+
${normalizeLlmsTxtConfig({ notes: input.config.notes })}${input.rootContent ? `
|
|
117
|
+
|
|
118
|
+
${input.rootContent}` : ""}`;
|
|
119
|
+
const pageIndexSpace = input.pages.some((page) => !assigned.has(page.pathname)) ? render("", "Pages", [link("Pages", "/llms/_pages.txt")]).length : 1;
|
|
120
|
+
let rootHeader = `${input.header.trim()}
|
|
121
|
+
|
|
122
|
+
${normalizeLlmsTxtConfig({ ...input.config, sections })}${input.rootContent ? `
|
|
123
|
+
|
|
124
|
+
${input.rootContent}` : ""}`;
|
|
125
|
+
if (rootHeader.length + pageIndexSpace > maxLength) {
|
|
126
|
+
const references = sections.flatMap((section, index) => {
|
|
127
|
+
const path = `/llms/_configured-${index + 1}.txt`;
|
|
128
|
+
if (!section.links?.length) {
|
|
129
|
+
const context = normalizeLlmsTxtConfig({ sections: [section] });
|
|
130
|
+
if (!context)
|
|
131
|
+
return [];
|
|
132
|
+
const body = `${input.header.trim()}
|
|
133
|
+
|
|
134
|
+
${context}
|
|
135
|
+
|
|
136
|
+
${link("llms.txt", "/llms.txt")}
|
|
137
|
+
`;
|
|
138
|
+
if (body.length > maxLength)
|
|
139
|
+
throw new Error("An llms.txt entry exceeds llmsTxt.maxLength. Shorten the entry or increase the limit.");
|
|
140
|
+
files.set(path, body);
|
|
141
|
+
return [{ title: section.title, links: [{ title: section.title, href: input.resolveUrl(path) }] }];
|
|
142
|
+
}
|
|
143
|
+
const text = normalizeLlmsTxtConfig({ sections: [{ ...section, description: section.optional ? section.description : void 0 }] });
|
|
144
|
+
const lines = text.split("\n").slice(2);
|
|
145
|
+
const description = Array.isArray(section.description) ? section.description.join(" ") : section.description;
|
|
146
|
+
const preamble = section.optional ? "" : normalizeLlmsTxtConfig({ notes: description });
|
|
147
|
+
tree(path, `configured-${index + 1}`, `${input.header.trim()}
|
|
148
|
+
|
|
149
|
+
${preamble}
|
|
150
|
+
|
|
151
|
+
${link("llms.txt", "/llms.txt")}`, section.optional ? "Optional" : section.title, lines);
|
|
152
|
+
return [{ title: section.title, optional: section.optional, links: [{ title: section.title, href: input.resolveUrl(path), description }] }];
|
|
153
|
+
});
|
|
154
|
+
rootHeader = `${rootPreamble}
|
|
155
|
+
|
|
156
|
+
${normalizeLlmsTxtConfig({ sections: references })}`;
|
|
157
|
+
if (rootHeader.length + pageIndexSpace > maxLength) {
|
|
158
|
+
const indexes = [];
|
|
159
|
+
for (const optional of [false, true]) {
|
|
160
|
+
const selected = references.filter((section) => Boolean(section.optional) === optional);
|
|
161
|
+
if (!selected.length)
|
|
162
|
+
continue;
|
|
163
|
+
const title = optional ? "Optional" : "Sections";
|
|
164
|
+
const path = optional ? "/llms/_optional.txt" : "/llms/_sections.txt";
|
|
165
|
+
const text = normalizeLlmsTxtConfig({ sections: selected.map((section) => ({ ...section, optional: true })) });
|
|
166
|
+
tree(path, optional ? "optional" : "sections", input.header, title, text.split("\n").slice(2));
|
|
167
|
+
indexes.push({ title, optional, links: [{ title, href: input.resolveUrl(path) }] });
|
|
168
|
+
}
|
|
169
|
+
rootHeader = `${rootPreamble}
|
|
170
|
+
|
|
171
|
+
${normalizeLlmsTxtConfig({ sections: indexes })}`;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
const pages = input.pages.filter((page) => !assigned.has(page.pathname)).map(formatLlmsTxtPageLink);
|
|
175
|
+
if (!pages.length && rootHeader.length + 1 <= maxLength) {
|
|
176
|
+
files.set("/llms.txt", `${rootHeader.trim()}
|
|
177
|
+
`);
|
|
178
|
+
} else {
|
|
179
|
+
const inline = render(rootHeader, "Pages", pages);
|
|
180
|
+
if (inline.length <= maxLength) {
|
|
181
|
+
files.set("/llms.txt", inline);
|
|
182
|
+
} else {
|
|
183
|
+
tree("/llms/_pages.txt", "pages", input.header, "Pages", pages);
|
|
184
|
+
const root = `${rootHeader.trim()}
|
|
185
|
+
|
|
186
|
+
## Pages
|
|
187
|
+
|
|
188
|
+
${link("Pages", "/llms/_pages.txt")}
|
|
189
|
+
`;
|
|
190
|
+
if (root.length > maxLength)
|
|
191
|
+
throw new Error("llms.txt sections exceed llmsTxt.maxLength. Shorten the sections or increase the limit.");
|
|
192
|
+
files.set("/llms.txt", root);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
return files;
|
|
196
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { escapeMarkdownHtml } from "./markdown-text.js";
|
|
1
2
|
const RE_INLINE_WHITESPACE = /\s+/g;
|
|
2
3
|
const RE_LINK_TITLE_BRACKET = /[[\]]/g;
|
|
3
4
|
const RE_LINK_HREF_UNSAFE = /[\s()]/g;
|
|
@@ -6,7 +7,7 @@ function normalizeInlineText(value) {
|
|
|
6
7
|
return value.trim().replace(RE_INLINE_WHITESPACE, " ");
|
|
7
8
|
}
|
|
8
9
|
function normalizeLinkTitle(value) {
|
|
9
|
-
return normalizeInlineText(value).replace(RE_LINK_TITLE_BRACKET, (bracket) => bracket === "[" ? "[" : "]");
|
|
10
|
+
return escapeMarkdownHtml(normalizeInlineText(value)).replace(RE_LINK_TITLE_BRACKET, (bracket) => bracket === "[" ? "[" : "]");
|
|
10
11
|
}
|
|
11
12
|
function normalizeLinkHref(value) {
|
|
12
13
|
return value.trim().replace(RE_LINK_HREF_UNSAFE, (character) => {
|
|
@@ -29,7 +30,7 @@ function normalizeLink(link, descriptionPrefixes = []) {
|
|
|
29
30
|
return `- [${normalizeLinkTitle(link.title)}](${normalizeLinkHref(link.href)})${description ? `: ${description}` : ""}`;
|
|
30
31
|
}
|
|
31
32
|
function normalizeSection(section) {
|
|
32
|
-
const parts = [`## ${normalizeInlineText(section.title)}`];
|
|
33
|
+
const parts = [`## ${escapeMarkdownHtml(normalizeInlineText(section.title))}`];
|
|
33
34
|
if (section.links?.length)
|
|
34
35
|
parts.push("", ...section.links.map((link) => normalizeLink(link)));
|
|
35
36
|
return parts.join("\n");
|
|
@@ -46,7 +47,7 @@ function normalizePreamble(config) {
|
|
|
46
47
|
continue;
|
|
47
48
|
const descriptions = normalizePreambleBlocks(section.description);
|
|
48
49
|
if (descriptions.length)
|
|
49
|
-
parts.push([`**${normalizeInlineText(section.title)}:**`, ...descriptions].join("\n\n"));
|
|
50
|
+
parts.push([`**${escapeMarkdownHtml(normalizeInlineText(section.title))}:**`, ...descriptions].join("\n\n"));
|
|
50
51
|
}
|
|
51
52
|
return parts;
|
|
52
53
|
}
|
|
@@ -67,7 +68,7 @@ export function formatLlmsTxtPageLink(page) {
|
|
|
67
68
|
const title = normalizedTitle && normalizedTitle !== page.pathname ? normalizedTitle : page.pathname;
|
|
68
69
|
const href = (page.href || page.pathname).trim();
|
|
69
70
|
const truncatedDescription = description ? `${description.substring(0, 160)}${description.length > 160 ? "..." : ""}` : void 0;
|
|
70
|
-
return normalizeLink({ title, href, description: truncatedDescription });
|
|
71
|
+
return normalizeLink({ title, href, description: truncatedDescription ? escapeMarkdownHtml(truncatedDescription) : void 0 });
|
|
71
72
|
}
|
|
72
73
|
export function normalizeLlmsTxtConfig(config) {
|
|
73
74
|
const required = config.sections?.filter((section) => !section.optional) ?? [];
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { H3Event } from '#nuxtseo/h3';
|
|
2
|
-
|
|
3
|
-
|
|
2
|
+
export { normalizeLlmsTxtConfig } from './llms-txt-format.js';
|
|
3
|
+
/** One discovery snapshot supplies the root and every bounded child file. */
|
|
4
|
+
export declare function buildLlmsTxtFiles(event: H3Event): Promise<Record<string, string>>;
|
|
4
5
|
export declare function buildLlmsTxt(event: H3Event): Promise<string>;
|
|
@@ -1,23 +1,18 @@
|
|
|
1
1
|
import { decodePath } from "ufo";
|
|
2
|
-
import { getRequestHost } from "#nuxtseo/h3";
|
|
2
|
+
import { createError, getRequestHost, getRequestURL } from "#nuxtseo/h3";
|
|
3
3
|
import { useRuntimeConfig } from "#nuxtseo/nitro";
|
|
4
4
|
import { getSiteConfig } from "#site-config/server/composables/getSiteConfig";
|
|
5
5
|
import { withSiteTrailingSlash, withSiteUrl } from "#site-config/server/composables/utils";
|
|
6
|
-
import {
|
|
7
|
-
import { absoluteSiteUrl } from "./markdown-links.js";
|
|
6
|
+
import { createLlmsTxtFiles } from "./llms-txt-files.js";
|
|
8
7
|
import { hasMarkdownTwin, toMarkdownPath } from "./markdown-path.js";
|
|
9
8
|
import { normalizePersistedRoute, toDeployedRoute, toLogicalRoute } from "./route-path.js";
|
|
10
|
-
import { queryPages } from "./server/db/queries.js";
|
|
9
|
+
import { queryPageMetadata, queryPages } from "./server/db/queries.js";
|
|
11
10
|
import { logger } from "./server/logger.js";
|
|
12
|
-
import {
|
|
13
|
-
export { normalizeLlmsTxtConfig };
|
|
11
|
+
import { fetchSitemapUrlGroups, getSitemapsFromConfig } from "./server/utils/sitemap.js";
|
|
12
|
+
export { normalizeLlmsTxtConfig } from "./llms-txt-format.js";
|
|
14
13
|
async function findMarkdownLinkAvailability() {
|
|
15
|
-
if (!import.meta.prerender)
|
|
16
|
-
return {
|
|
17
|
-
runtimeMarkdownAvailable: true,
|
|
18
|
-
paths: /* @__PURE__ */ new Set()
|
|
19
|
-
};
|
|
20
|
-
}
|
|
14
|
+
if (!import.meta.prerender)
|
|
15
|
+
return { runtimeMarkdownAvailable: true, paths: /* @__PURE__ */ new Set() };
|
|
21
16
|
const { readMarkdownLinkAvailabilityFromFilesystem } = await import("#ai-ready-virtual/read-page-data.mjs");
|
|
22
17
|
const availability = await readMarkdownLinkAvailabilityFromFilesystem();
|
|
23
18
|
return {
|
|
@@ -25,231 +20,97 @@ async function findMarkdownLinkAvailability() {
|
|
|
25
20
|
paths: new Set(availability.paths.map(decodePath))
|
|
26
21
|
};
|
|
27
22
|
}
|
|
28
|
-
function
|
|
29
|
-
const segments = url.split("/").filter(Boolean);
|
|
30
|
-
if (segments.length === 0)
|
|
31
|
-
return "/";
|
|
32
|
-
if (depth === 1 || segments.length === 1)
|
|
33
|
-
return `/${segments[0]}`;
|
|
34
|
-
return `/${segments[0]}/${segments[1]}`;
|
|
35
|
-
}
|
|
36
|
-
function getPathSegments(pathname) {
|
|
37
|
-
return pathname.split("/").filter((s) => Boolean(s));
|
|
38
|
-
}
|
|
39
|
-
function analyzePageGroups(pages) {
|
|
40
|
-
const twoSegmentCount = /* @__PURE__ */ new Map();
|
|
41
|
-
const segmentHasNested = /* @__PURE__ */ new Map();
|
|
42
|
-
for (const page of pages) {
|
|
43
|
-
const prefix = getGroupPrefix(page.pathname, 2);
|
|
44
|
-
twoSegmentCount.set(prefix, (twoSegmentCount.get(prefix) || 0) + 1);
|
|
45
|
-
const segments = getPathSegments(page.pathname);
|
|
46
|
-
const firstSegment = segments[0] || "";
|
|
47
|
-
if (!segmentHasNested.has(firstSegment))
|
|
48
|
-
segmentHasNested.set(firstSegment, false);
|
|
49
|
-
if (segments.length > 1)
|
|
50
|
-
segmentHasNested.set(firstSegment, true);
|
|
51
|
-
}
|
|
52
|
-
return { twoSegmentCount, segmentHasNested };
|
|
53
|
-
}
|
|
54
|
-
function getPageGroupKey(pathname, { twoSegmentCount, segmentHasNested }) {
|
|
55
|
-
const segments = getPathSegments(pathname);
|
|
56
|
-
const firstSegment = segments[0] || "";
|
|
57
|
-
const twoSegPrefix = getGroupPrefix(pathname, 2);
|
|
58
|
-
const twoSegCount = twoSegmentCount.get(twoSegPrefix) || 0;
|
|
59
|
-
let groupKey = twoSegCount > 1 ? twoSegPrefix : `/${firstSegment}`;
|
|
60
|
-
if (segments.length <= 1 && !segmentHasNested.get(firstSegment))
|
|
61
|
-
groupKey = "";
|
|
62
|
-
return groupKey;
|
|
63
|
-
}
|
|
64
|
-
function sortPagesByPath(pages) {
|
|
65
|
-
const analysis = analyzePageGroups(pages);
|
|
66
|
-
return pages.sort((a, b) => {
|
|
67
|
-
const segmentsA = getPathSegments(a.pathname);
|
|
68
|
-
const segmentsB = getPathSegments(b.pathname);
|
|
69
|
-
const groupKeyA = getPageGroupKey(a.pathname, analysis);
|
|
70
|
-
const groupKeyB = getPageGroupKey(b.pathname, analysis);
|
|
71
|
-
if (groupKeyA === "" && groupKeyB !== "")
|
|
72
|
-
return -1;
|
|
73
|
-
if (groupKeyA !== "" && groupKeyB === "")
|
|
74
|
-
return 1;
|
|
75
|
-
if (groupKeyA !== groupKeyB)
|
|
76
|
-
return groupKeyA.localeCompare(groupKeyB);
|
|
77
|
-
if (segmentsA.length === 0)
|
|
78
|
-
return -1;
|
|
79
|
-
if (segmentsB.length === 0)
|
|
80
|
-
return 1;
|
|
81
|
-
const minLen = Math.min(segmentsA.length, segmentsB.length);
|
|
82
|
-
for (let i = 0; i < minLen; i++) {
|
|
83
|
-
const cmp = segmentsA[i].localeCompare(segmentsB[i]);
|
|
84
|
-
if (cmp !== 0)
|
|
85
|
-
return cmp;
|
|
86
|
-
}
|
|
87
|
-
return segmentsA.length - segmentsB.length;
|
|
88
|
-
});
|
|
89
|
-
}
|
|
90
|
-
function formatPagesWithGroups(pages) {
|
|
91
|
-
if (pages.length === 0)
|
|
92
|
-
return [];
|
|
93
|
-
const analysis = analyzePageGroups(pages);
|
|
94
|
-
const lines = [];
|
|
95
|
-
let currentGroup = "";
|
|
96
|
-
let segmentGroupIndex = 0;
|
|
97
|
-
let urlsInCurrentGroup = 0;
|
|
98
|
-
for (const page of pages) {
|
|
99
|
-
const groupKey = getPageGroupKey(page.pathname, analysis);
|
|
100
|
-
if (groupKey !== currentGroup) {
|
|
101
|
-
if (urlsInCurrentGroup > 0) {
|
|
102
|
-
const shouldAddBlankLine = segmentGroupIndex === 0 || segmentGroupIndex >= 1 && segmentGroupIndex <= 2 && urlsInCurrentGroup > 1;
|
|
103
|
-
if (shouldAddBlankLine)
|
|
104
|
-
lines.push("");
|
|
105
|
-
}
|
|
106
|
-
currentGroup = groupKey;
|
|
107
|
-
segmentGroupIndex++;
|
|
108
|
-
urlsInCurrentGroup = 0;
|
|
109
|
-
}
|
|
110
|
-
urlsInCurrentGroup++;
|
|
111
|
-
lines.push(formatLlmsTxtPageLink(page));
|
|
112
|
-
}
|
|
113
|
-
return lines;
|
|
114
|
-
}
|
|
115
|
-
export async function buildLlmsTxt(event) {
|
|
23
|
+
export async function buildLlmsTxtFiles(event) {
|
|
116
24
|
const runtimeConfig = useRuntimeConfig(event);
|
|
117
25
|
const aiReadyConfig = runtimeConfig["nuxt-ai-ready"];
|
|
118
|
-
const sitemapConfig = runtimeConfig.sitemap;
|
|
119
26
|
const siteConfig = getSiteConfig(event);
|
|
120
|
-
const
|
|
121
|
-
const i18n = aiReadyConfig.i18n;
|
|
122
|
-
const i18nRuntime = i18n ? { _tag: "enabled", config: i18n, ...await import("./llms-txt-i18n.js") } : { _tag: "disabled" };
|
|
123
|
-
const i18nContext = { host: getRequestHost(event, { xForwardedHost: true }) };
|
|
27
|
+
const config = aiReadyConfig.llmsTxt;
|
|
124
28
|
const baseURL = runtimeConfig.app.baseURL;
|
|
125
|
-
const resolvePath = (path) => withSiteTrailingSlash(event, toDeployedRoute(path, baseURL));
|
|
126
29
|
const resolveUrl = (path) => withSiteUrl(event, toDeployedRoute(path, baseURL), { withBase: true });
|
|
30
|
+
const resolvePageUrl = (path) => resolveUrl(withSiteTrailingSlash(event, path));
|
|
127
31
|
const canonicalSiteUrl = siteConfig.url ? resolveUrl("/") : void 0;
|
|
128
|
-
const
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
if (canonicalSiteUrl)
|
|
135
|
-
parts.push(`
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
})) ?? [];
|
|
143
|
-
if (sections[0]?.links) {
|
|
144
|
-
const sitemapRoutes = sitemapConfig?.sitemaps ? Object.values(sitemapConfig.sitemaps).map((s) => s.sitemapName) : ["sitemap.xml"];
|
|
145
|
-
for (const name of sitemapRoutes) {
|
|
146
|
-
sections[0].links.push({
|
|
147
|
-
title: name,
|
|
148
|
-
href: resolveUrl(`/${name}`),
|
|
149
|
-
description: "XML sitemap for search engines and crawlers."
|
|
150
|
-
});
|
|
32
|
+
const i18n = aiReadyConfig.i18n;
|
|
33
|
+
const i18nRuntime = i18n ? { _tag: "enabled", config: i18n, ...await import("./llms-txt-i18n.js") } : { _tag: "disabled" };
|
|
34
|
+
const i18nContext = { host: getRequestHost(event, { xForwardedHost: true }) };
|
|
35
|
+
const parts = [`# ${(siteConfig.name || canonicalSiteUrl || "Site").replace(/\s+/g, " ").trim()}`];
|
|
36
|
+
if (siteConfig.description)
|
|
37
|
+
parts.push("", `> ${siteConfig.description.replace(/\s+/g, " ").trim()}`);
|
|
38
|
+
if (canonicalSiteUrl)
|
|
39
|
+
parts.push("", `Canonical Origin: ${canonicalSiteUrl}`);
|
|
40
|
+
const sections = config.sections?.map((section) => ({ ...section, links: section.links ? [...section.links] : void 0 })) ?? [];
|
|
41
|
+
const resources = sections.find((section) => section.title === "LLM Resources" && !section.sitemap);
|
|
42
|
+
if (resources) {
|
|
43
|
+
resources.links ||= [];
|
|
44
|
+
for (const sitemap of getSitemapsFromConfig(event).filter((sitemap2) => config.sitemaps === void 0 || config.sitemaps.includes(sitemap2.name))) {
|
|
45
|
+
resources.links.push({ title: sitemap.route.split("/").pop() || `${sitemap.name}.xml`, href: resolveUrl(toLogicalRoute(sitemap.route, baseURL)), description: "XML sitemap for search engines and crawlers." });
|
|
151
46
|
}
|
|
152
|
-
|
|
153
|
-
title: "
|
|
154
|
-
|
|
155
|
-
description: "Crawler rules and permissions."
|
|
156
|
-
});
|
|
47
|
+
if (!getSitemapsFromConfig(event).length && (config.sitemaps === void 0 || config.sitemaps.includes("sitemap")))
|
|
48
|
+
resources.links.push({ title: "sitemap.xml", href: resolveUrl("/sitemap.xml"), description: "XML sitemap for search engines and crawlers." });
|
|
49
|
+
resources.links.push({ title: "robots.txt", href: resolveUrl("/robots.txt"), description: "Crawler rules and permissions." });
|
|
157
50
|
}
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
51
|
+
if (config.pages === false && !sections.some((section) => section.sitemap))
|
|
52
|
+
return Object.fromEntries(createLlmsTxtFiles({ header: parts.join("\n"), config, sections, pages: [], sitemapPages: /* @__PURE__ */ new Map(), resolveUrl }));
|
|
53
|
+
const groups = await fetchSitemapUrlGroups(event, config.sitemaps);
|
|
54
|
+
const allowedOrigin = canonicalSiteUrl ? new URL(canonicalSiteUrl).origin : void 0;
|
|
55
|
+
const requestOrigin = getRequestURL(event, { xForwardedHost: true, xForwardedProto: true }).origin;
|
|
56
|
+
const sitemapPaths = /* @__PURE__ */ new Map();
|
|
57
|
+
for (const [name, urls] of groups) {
|
|
58
|
+
const paths = /* @__PURE__ */ new Set();
|
|
59
|
+
for (const entry of urls) {
|
|
60
|
+
const url = URL.canParse(entry.loc, canonicalSiteUrl || "http://localhost") ? new URL(entry.loc, canonicalSiteUrl || "http://localhost") : void 0;
|
|
61
|
+
if (!url || url.search || url.hash || allowedOrigin && url.origin !== allowedOrigin && url.origin !== requestOrigin)
|
|
62
|
+
continue;
|
|
63
|
+
const path = toLogicalRoute(url.pathname, baseURL);
|
|
64
|
+
paths.add(path);
|
|
65
|
+
}
|
|
66
|
+
sitemapPaths.set(name, paths);
|
|
162
67
|
}
|
|
163
|
-
const
|
|
164
|
-
let
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
]);
|
|
172
|
-
} catch (err) {
|
|
173
|
-
logger.warn(
|
|
174
|
-
`[ai-ready] Database unavailable for llms.txt, falling back to sitemap-only pages: ${err instanceof Error ? err.message : String(err)}`
|
|
175
|
-
);
|
|
68
|
+
const admitted = new Set([...sitemapPaths.values()].flatMap((paths) => [...paths]));
|
|
69
|
+
let stored = [];
|
|
70
|
+
if (aiReadyConfig.database?._tag !== "Disabled") {
|
|
71
|
+
const read = config.pages === "all" ? Promise.all([queryPages(event), queryPages(event, { where: { hasError: true } })]).then((results) => results.flat()) : queryPageMetadata(event, [...admitted].flatMap((path) => [path, toDeployedRoute(path, baseURL)]));
|
|
72
|
+
stored = await read.catch((error) => {
|
|
73
|
+
logger.warn(`[ai-ready] Database unavailable for llms.txt. Using sitemap metadata: ${error instanceof Error ? error.message : String(error)}`);
|
|
74
|
+
return [];
|
|
75
|
+
});
|
|
176
76
|
}
|
|
177
|
-
const
|
|
178
|
-
const
|
|
179
|
-
const
|
|
180
|
-
const
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
77
|
+
const errorPaths = new Set(stored.filter((page) => page.isError).map((page) => normalizePersistedRoute(page.route, admitted, baseURL)));
|
|
78
|
+
const metadata = new Map(stored.map((page) => [normalizePersistedRoute(page.route, admitted, baseURL), page]));
|
|
79
|
+
const availability = config.markdownLinks ? await findMarkdownLinkAvailability() : void 0;
|
|
80
|
+
const href = (path) => {
|
|
81
|
+
const markdownPath = toMarkdownPath(path);
|
|
82
|
+
if (availability && (availability.paths.has(decodePath(toDeployedRoute(markdownPath, baseURL))) || availability.runtimeMarkdownAvailable && hasMarkdownTwin(path)))
|
|
83
|
+
return resolveUrl(markdownPath);
|
|
84
|
+
return resolvePageUrl(path);
|
|
85
|
+
};
|
|
86
|
+
const pageMap = /* @__PURE__ */ new Map();
|
|
87
|
+
const candidates = config.pages === "all" ? /* @__PURE__ */ new Set([...admitted, ...metadata.keys()]) : admitted;
|
|
88
|
+
for (const pathname of [...candidates].sort()) {
|
|
89
|
+
if (errorPaths.has(pathname))
|
|
186
90
|
continue;
|
|
187
|
-
|
|
188
|
-
|
|
91
|
+
const page = metadata.get(pathname);
|
|
92
|
+
const locale = page?.locale || (i18nRuntime._tag === "enabled" ? i18nRuntime.resolveLocaleFromRoute(pathname, i18nRuntime.config, i18nContext).locale : void 0);
|
|
93
|
+
pageMap.set(pathname, { pathname, href: href(pathname), title: page?.title, description: page?.description, locale });
|
|
189
94
|
}
|
|
190
|
-
|
|
191
|
-
const other = [];
|
|
192
|
-
for (const pathname of sitemapPaths) {
|
|
193
|
-
if (!seenPaths.has(pathname) && !errorSet.has(pathname)) {
|
|
194
|
-
const locale = i18nRuntime._tag === "enabled" ? i18nRuntime.resolveLocaleFromRoute(pathname, i18nRuntime.config, i18nContext).locale : void 0;
|
|
195
|
-
other.push({ pathname, locale });
|
|
196
|
-
seenPaths.add(pathname);
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
let resolvePageHref = resolvePath;
|
|
200
|
-
if (llmsTxtConfig.markdownLinks) {
|
|
201
|
-
const markdownLinkAvailability = await findMarkdownLinkAvailability();
|
|
202
|
-
resolvePageHref = (pathname) => {
|
|
203
|
-
const deployedPathname = resolvePath(pathname);
|
|
204
|
-
const markdownPath = resolvePath(toMarkdownPath(pathname));
|
|
205
|
-
if (markdownLinkAvailability.paths.has(decodePath(markdownPath)) || markdownLinkAvailability.runtimeMarkdownAvailable && hasMarkdownTwin(pathname)) {
|
|
206
|
-
return markdownPath;
|
|
207
|
-
}
|
|
208
|
-
return deployedPathname;
|
|
209
|
-
};
|
|
210
|
-
}
|
|
211
|
-
const resolvePagePath = resolvePageHref;
|
|
212
|
-
resolvePageHref = (pathname) => absoluteSiteUrl(resolvePagePath(pathname), canonicalSiteUrl);
|
|
213
|
-
for (const page of [...prerendered, ...other])
|
|
214
|
-
page.href = resolvePageHref(page.pathname);
|
|
95
|
+
let rootContent;
|
|
215
96
|
if (i18nRuntime._tag === "enabled") {
|
|
216
|
-
const
|
|
217
|
-
for (const
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
97
|
+
const counts = /* @__PURE__ */ new Map();
|
|
98
|
+
for (const page of pageMap.values())
|
|
99
|
+
counts.set(page.locale, (counts.get(page.locale) || 0) + 1);
|
|
100
|
+
rootContent = i18nRuntime.formatAvailableLanguagesSection(i18nRuntime.config, counts, href, i18nContext).join("\n");
|
|
101
|
+
for (const [path, page] of pageMap) {
|
|
102
|
+
if (page.locale !== i18nRuntime.config.defaultLocale)
|
|
103
|
+
pageMap.delete(path);
|
|
221
104
|
}
|
|
222
|
-
parts.push(...i18nRuntime.formatAvailableLanguagesSection(i18nRuntime.config, pageCounts, resolvePageHref, i18nContext));
|
|
223
|
-
parts.push("");
|
|
224
105
|
}
|
|
225
|
-
const
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
const
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
if (sortedPrerendered.length > 0 && sortedOther.length > 0) {
|
|
236
|
-
parts.push(`## Prerendered Pages${devModeHint}
|
|
237
|
-
`);
|
|
238
|
-
parts.push(...formatPagesWithGroups(sortedPrerendered));
|
|
239
|
-
parts.push("");
|
|
240
|
-
parts.push("## Other Pages\n");
|
|
241
|
-
parts.push(...formatPagesWithGroups(sortedOther));
|
|
242
|
-
parts.push("");
|
|
243
|
-
} else if (sortedPrerendered.length > 0) {
|
|
244
|
-
parts.push(`## Pages${devModeHint}
|
|
245
|
-
`);
|
|
246
|
-
parts.push(...formatPagesWithGroups(sortedPrerendered));
|
|
247
|
-
parts.push("");
|
|
248
|
-
} else if (sortedOther.length > 0) {
|
|
249
|
-
parts.push(`## Pages${devModeHint}
|
|
250
|
-
`);
|
|
251
|
-
parts.push(...formatPagesWithGroups(sortedOther));
|
|
252
|
-
parts.push("");
|
|
253
|
-
}
|
|
254
|
-
return parts.join("\n");
|
|
106
|
+
const sitemapPages = new Map([...sitemapPaths].map(([name, paths]) => [name, [...paths].flatMap((path) => pageMap.has(path) ? [pageMap.get(path)] : [])]));
|
|
107
|
+
return Object.fromEntries(createLlmsTxtFiles({ header: parts.join("\n"), config, sections, rootContent, pages: config.pages === false ? [] : [...pageMap.values()], sitemapPages, resolveUrl }));
|
|
108
|
+
}
|
|
109
|
+
export async function buildLlmsTxt(event) {
|
|
110
|
+
const files = await buildLlmsTxtFiles(event);
|
|
111
|
+
const path = toLogicalRoute(event.path.split("?")[0], useRuntimeConfig(event).app.baseURL);
|
|
112
|
+
const content = files[path];
|
|
113
|
+
if (content === void 0)
|
|
114
|
+
throw createError({ statusCode: 404, statusMessage: "llms.txt file not found" });
|
|
115
|
+
return content;
|
|
255
116
|
}
|