nuxt-ai-ready 3.2.0 → 3.3.0
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 +189 -27
- 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 +141 -0
- package/dist/runtime/llms-txt-utils.d.ts +3 -2
- package/dist/runtime/llms-txt-utils.js +85 -224
- package/dist/runtime/server/db/queries.d.ts +3 -0
- package/dist/runtime/server/db/queries.js +28 -2
- package/dist/runtime/server/middleware/markdown.js +1 -1
- package/dist/runtime/server/routes/llms-full.txt.get.js +2 -1
- package/dist/runtime/server/routes/llms.txt.get.js +19 -11
- package/dist/runtime/server/routes/sitemap.md.get.js +2 -1
- package/dist/runtime/server/utils/markdown-request.js +2 -5
- package/dist/runtime/server/utils/sitemap.d.ts +1 -0
- package/dist/runtime/server/utils/sitemap.js +22 -10
- package/dist/runtime/types.d.ts +13 -1
- package/dist/shared/{nuxt-ai-ready.DH9mSayy.mjs → nuxt-ai-ready.BwnSMqRn.mjs} +41 -12
- package/package.json +1 -1
- package/skills/nuxt-ai-ready/SKILL.md +34 -1
|
@@ -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
|
}
|
|
@@ -34,6 +34,9 @@ export interface PageEntry {
|
|
|
34
34
|
export interface PageData extends PageEntry {
|
|
35
35
|
markdown: string;
|
|
36
36
|
}
|
|
37
|
+
export type PageMetadata = Pick<PageEntry, 'route' | 'title' | 'description' | 'isError' | 'locale'>;
|
|
38
|
+
/** Read only admitted page metadata. Each query fits D1's 100 parameter limit. */
|
|
39
|
+
export declare function queryPageMetadata(event: AiReadyDatabaseEvent | undefined, routes: readonly string[]): Promise<PageMetadata[]>;
|
|
37
40
|
export interface SearchResult {
|
|
38
41
|
route: string;
|
|
39
42
|
title: string;
|
|
@@ -80,6 +80,7 @@ async function getPrerenderDb() {
|
|
|
80
80
|
const isErrorQuery = sql.includes("is_error = 1") || params.includes(1) && sql.includes("is_error");
|
|
81
81
|
const excludeErrors = sql.includes("is_error = 0");
|
|
82
82
|
const includeMarkdown = wantsMarkdown(sql);
|
|
83
|
+
const admittedRoutes = sql.includes("WHERE route IN (") ? new Set(params) : void 0;
|
|
83
84
|
if (isErrorQuery) {
|
|
84
85
|
return [...errorRoutes].map((route) => ({
|
|
85
86
|
route,
|
|
@@ -94,8 +95,8 @@ async function getPrerenderDb() {
|
|
|
94
95
|
locale: ""
|
|
95
96
|
}));
|
|
96
97
|
}
|
|
97
|
-
const filtered =
|
|
98
|
-
|
|
98
|
+
const filtered = pages.filter((p) => (!excludeErrors || !errorRoutes.has(p.route)) && (!admittedRoutes || admittedRoutes.has(p.route)));
|
|
99
|
+
const rows = filtered.map((p) => ({
|
|
99
100
|
route: p.route,
|
|
100
101
|
title: p.title,
|
|
101
102
|
description: p.description,
|
|
@@ -107,6 +108,13 @@ async function getPrerenderDb() {
|
|
|
107
108
|
indexed: 1,
|
|
108
109
|
locale: p.locale || ""
|
|
109
110
|
}));
|
|
111
|
+
if (admittedRoutes && !excludeErrors) {
|
|
112
|
+
for (const route of errorRoutes) {
|
|
113
|
+
if (admittedRoutes.has(route))
|
|
114
|
+
rows.push({ route, title: "", description: "", headings: [], keywords: "[]", updated_at: "", is_error: 1, indexed: 0, locale: "" });
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return rows;
|
|
110
118
|
},
|
|
111
119
|
first: async (sql, params = []) => {
|
|
112
120
|
if (sql.includes("WHERE route = ?")) {
|
|
@@ -136,6 +144,24 @@ async function getPrerenderDb() {
|
|
|
136
144
|
}
|
|
137
145
|
};
|
|
138
146
|
}
|
|
147
|
+
export async function queryPageMetadata(event, routes) {
|
|
148
|
+
if (!routes.length)
|
|
149
|
+
return [];
|
|
150
|
+
const db = await getDb(event);
|
|
151
|
+
if (!db)
|
|
152
|
+
return [];
|
|
153
|
+
const unique = [...new Set(routes)];
|
|
154
|
+
const pages = [];
|
|
155
|
+
for (let i = 0; i < unique.length; i += 100) {
|
|
156
|
+
const batch = unique.slice(i, i + 100);
|
|
157
|
+
const rows = await db.all(
|
|
158
|
+
`SELECT route, title, description, is_error, locale FROM ai_ready_pages WHERE route IN (${batch.map(() => "?").join(", ")})`,
|
|
159
|
+
batch
|
|
160
|
+
);
|
|
161
|
+
pages.push(...rows.map((row) => ({ route: row.route, title: row.title, description: row.description, isError: row.is_error === 1, locale: row.locale || "" })));
|
|
162
|
+
}
|
|
163
|
+
return pages;
|
|
164
|
+
}
|
|
139
165
|
function buildWhereClause(where) {
|
|
140
166
|
const conditions = [];
|
|
141
167
|
const params = [];
|
|
@@ -20,7 +20,7 @@ function notFoundMarkdown(ctx, canonicalUrl, build) {
|
|
|
20
20
|
``,
|
|
21
21
|
`- [Sitemap](${resolveUrl("/sitemap.xml")})`,
|
|
22
22
|
`- [llms.txt](${resolveUrl("/llms.txt")})`,
|
|
23
|
-
`- [llms-full.txt](${resolveUrl("/llms-full.txt")})
|
|
23
|
+
...config.llmsFullTxt === false ? [] : [`- [llms-full.txt](${resolveUrl("/llms-full.txt")})`],
|
|
24
24
|
``
|
|
25
25
|
].join("\n");
|
|
26
26
|
const i18n = config.i18n;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { eventHandler, sendIterable, setHeader, setResponseHeader } from "#nuxtseo/h3";
|
|
1
|
+
import { assertMethod, eventHandler, sendIterable, setHeader, setResponseHeader } from "#nuxtseo/h3";
|
|
2
2
|
import { useRuntimeConfig } from "#nuxtseo/nitro";
|
|
3
3
|
import { getSiteConfig } from "#site-config/server/composables";
|
|
4
4
|
import { withSiteUrl } from "#site-config/server/composables/utils";
|
|
@@ -7,6 +7,7 @@ import { toDeployedRoute } from "../../route-path.js";
|
|
|
7
7
|
import { countPages, streamPages } from "../db/queries.js";
|
|
8
8
|
import { buildLlmsFullTxtHeader, formatPageForLlmsFullTxt } from "../utils/llms-full.js";
|
|
9
9
|
export default eventHandler(async (event) => {
|
|
10
|
+
assertMethod(event, "GET", true);
|
|
10
11
|
if (import.meta.prerender) {
|
|
11
12
|
setHeader(event, "Content-Type", "text/plain; charset=utf-8");
|
|
12
13
|
return "# llms-full.txt\n\nThis file is generated during prerender.";
|
|
@@ -1,29 +1,37 @@
|
|
|
1
|
-
import { eventHandler, setHeader } from "#nuxtseo/h3";
|
|
1
|
+
import { assertMethod, createError, eventHandler, getRequestURL, setHeader } from "#nuxtseo/h3";
|
|
2
2
|
import { defineCachedFunction, useRuntimeConfig } from "#nuxtseo/nitro";
|
|
3
3
|
import { publicCacheControl } from "../../cache-control.js";
|
|
4
|
-
import {
|
|
4
|
+
import { buildLlmsTxtFiles } from "../../llms-txt-utils.js";
|
|
5
|
+
import { toLogicalRoute } from "../../route-path.js";
|
|
5
6
|
const cachedBuilders = /* @__PURE__ */ new Map();
|
|
6
7
|
function getBuildLlmsTxtCached(maxAge) {
|
|
7
8
|
let cached = cachedBuilders.get(maxAge);
|
|
8
9
|
if (!cached) {
|
|
9
|
-
cached = defineCachedFunction(
|
|
10
|
-
name: "llms-txt",
|
|
10
|
+
cached = defineCachedFunction(buildLlmsTxtFiles, {
|
|
11
|
+
name: "llms-txt-files",
|
|
11
12
|
group: "ai-ready",
|
|
12
13
|
maxAge,
|
|
13
|
-
swr: true
|
|
14
|
+
swr: true,
|
|
15
|
+
// Share one discovery snapshot between root and child files, separated by origin.
|
|
16
|
+
getKey: (event) => getRequestURL(event, { xForwardedHost: true, xForwardedProto: true }).origin
|
|
14
17
|
});
|
|
15
18
|
cachedBuilders.set(maxAge, cached);
|
|
16
19
|
}
|
|
17
20
|
return cached;
|
|
18
21
|
}
|
|
19
22
|
export default eventHandler(async (event) => {
|
|
20
|
-
|
|
21
|
-
const
|
|
22
|
-
const
|
|
23
|
-
const
|
|
23
|
+
assertMethod(event, "GET", true);
|
|
24
|
+
const runtimeConfig = useRuntimeConfig(event);
|
|
25
|
+
const config = runtimeConfig["nuxt-ai-ready"];
|
|
26
|
+
const cacheSeconds = config.llmsTxtCacheSeconds;
|
|
27
|
+
const cacheEnabled = !import.meta.dev && !import.meta.prerender && cacheSeconds > 0;
|
|
28
|
+
const files = cacheEnabled ? await getBuildLlmsTxtCached(cacheSeconds)(event) : await buildLlmsTxtFiles(event);
|
|
29
|
+
const path = toLogicalRoute(event.path.split("?")[0], runtimeConfig.app.baseURL);
|
|
30
|
+
const content = files[path];
|
|
31
|
+
if (content === void 0)
|
|
32
|
+
throw createError({ statusCode: 404, statusMessage: "llms.txt file not found" });
|
|
24
33
|
setHeader(event, "Content-Type", "text/plain; charset=utf-8");
|
|
25
|
-
if (cacheEnabled)
|
|
34
|
+
if (cacheEnabled)
|
|
26
35
|
setHeader(event, "Cache-Control", publicCacheControl(cacheSeconds, 3600));
|
|
27
|
-
}
|
|
28
36
|
return content;
|
|
29
37
|
});
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { eventHandler, setHeader } from "#nuxtseo/h3";
|
|
1
|
+
import { assertMethod, eventHandler, setHeader } from "#nuxtseo/h3";
|
|
2
2
|
import { defineCachedFunction, useRuntimeConfig } from "#nuxtseo/nitro";
|
|
3
3
|
import { getSiteConfig } from "#site-config/server/composables";
|
|
4
4
|
import { publicCacheControl } from "../../cache-control.js";
|
|
@@ -42,6 +42,7 @@ function getBuildSitemapMdCached(maxAge) {
|
|
|
42
42
|
return cached;
|
|
43
43
|
}
|
|
44
44
|
export default eventHandler(async (event) => {
|
|
45
|
+
assertMethod(event, "GET", true);
|
|
45
46
|
const fullRuntimeConfig = useRuntimeConfig(event);
|
|
46
47
|
const runtimeConfig = fullRuntimeConfig["nuxt-ai-ready"];
|
|
47
48
|
const cacheSeconds = runtimeConfig.llmsTxtCacheSeconds ?? 600;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { negotiateContent, parseAcceptHeader } from "@mdream/js/negotiate";
|
|
2
2
|
import { getBotInfo } from "@nuxtjs/robots/util";
|
|
3
3
|
import { getHeaders } from "#nuxtseo/h3";
|
|
4
|
-
import { isReservedPath } from "../../markdown-path.js";
|
|
4
|
+
import { hasMarkdownTwin, isReservedPath } from "../../markdown-path.js";
|
|
5
5
|
const RE_MD_EXT = /\.md$/;
|
|
6
6
|
export function toMarkdownRequest(event, isPrerender = !!import.meta.prerender) {
|
|
7
7
|
return { path: event.path, headers: getHeaders(event), isPrerender };
|
|
@@ -38,10 +38,7 @@ export function getRequestRenderInfo(request, mode = { _tag: "runtime", contentN
|
|
|
38
38
|
const isExplicit = originalPath.endsWith(".md");
|
|
39
39
|
if (isPrerender && !isExplicit)
|
|
40
40
|
return null;
|
|
41
|
-
|
|
42
|
-
const hasExtension = lastSegment.includes(".");
|
|
43
|
-
const extension = hasExtension ? lastSegment.substring(lastSegment.lastIndexOf(".")) : "";
|
|
44
|
-
if (hasExtension && extension !== ".md")
|
|
41
|
+
if (!isExplicit && !hasMarkdownTwin(originalPath))
|
|
45
42
|
return null;
|
|
46
43
|
const negotiation = isPrerender ? "markdown" : mode.contentNegotiation ? negotiateRequestRepresentation(request, mode.botNegotiation) : "html";
|
|
47
44
|
if (isExplicit) {
|
|
@@ -53,5 +53,6 @@ export declare function fetchSitemapByRoute(event: H3Event | undefined, route: s
|
|
|
53
53
|
* Fetch all URLs from all sitemaps (or single sitemap if not multi-sitemap)
|
|
54
54
|
* Used for backwards compatibility and llms.txt generation
|
|
55
55
|
*/
|
|
56
|
+
export declare function fetchSitemapUrlGroups(event: H3Event, names?: readonly string[]): Promise<Map<string, SitemapUrl[]>>;
|
|
56
57
|
export declare function fetchSitemapUrls(event: H3Event): Promise<SitemapUrl[]>;
|
|
57
58
|
export {};
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { createSitemapReader } from "sitemapd";
|
|
2
2
|
import { parseSitemap } from "sitemapd/parse";
|
|
3
|
+
import staticSitemapConfig from "#ai-ready-virtual/sitemap-config.mjs";
|
|
4
|
+
import { getRequestURL } from "#nuxtseo/h3";
|
|
3
5
|
import { useRuntimeConfig } from "#nuxtseo/nitro";
|
|
4
6
|
import { logger } from "../logger.js";
|
|
5
7
|
import { getCfEnv, hasAssets } from "./cloudflare.js";
|
|
@@ -19,17 +21,18 @@ const SITEMAP_READER_LIMITS = {
|
|
|
19
21
|
const MAX_SITEMAP_CRAWL_ROUNDS = 10;
|
|
20
22
|
export function getSitemapsFromConfig(event) {
|
|
21
23
|
const runtimeConfig = useRuntimeConfig(event);
|
|
22
|
-
const sitemapConfig = runtimeConfig.sitemap;
|
|
24
|
+
const sitemapConfig = { ...staticSitemapConfig, ...runtimeConfig.sitemap };
|
|
23
25
|
if (!sitemapConfig?.sitemaps)
|
|
24
26
|
return [];
|
|
25
27
|
const sitemaps = [];
|
|
26
28
|
for (const [key, sitemap] of Object.entries(sitemapConfig.sitemaps)) {
|
|
27
29
|
if (key === "index")
|
|
28
30
|
continue;
|
|
29
|
-
|
|
31
|
+
const route = sitemap._route || sitemap.route || (key === "chunks" ? sitemapConfig.sitemaps.index?._route || sitemapConfig.sitemaps.index?.route : void 0);
|
|
32
|
+
if (route) {
|
|
30
33
|
sitemaps.push({
|
|
31
|
-
name: sitemap.sitemapName || key,
|
|
32
|
-
route:
|
|
34
|
+
name: (sitemap.sitemapName || key).replace(/\.xml$/, ""),
|
|
35
|
+
route: route.startsWith("/") ? route : `/${route}`
|
|
33
36
|
});
|
|
34
37
|
}
|
|
35
38
|
}
|
|
@@ -305,6 +308,7 @@ export async function crawlSitemapByRoute(event, input) {
|
|
|
305
308
|
const usePublicAsset = config.sitemapPrerendered && hasAssets(event);
|
|
306
309
|
const configuredSiteUrl = createUniversalContext(event).siteUrl ?? SITEMAP_FALLBACK_ORIGIN;
|
|
307
310
|
const siteUrl = parseAbsoluteUrl(configuredSiteUrl);
|
|
311
|
+
const requestOrigin = import.meta.dev && event ? getRequestURL(event, { xForwardedHost: true, xForwardedProto: true }).origin : void 0;
|
|
308
312
|
if (siteUrl._tag === "error") {
|
|
309
313
|
return {
|
|
310
314
|
_tag: "failed",
|
|
@@ -346,7 +350,7 @@ export async function crawlSitemapByRoute(event, input) {
|
|
|
346
350
|
const target = parseAbsoluteUrl(request.url);
|
|
347
351
|
if (target._tag === "error")
|
|
348
352
|
return { _tag: "deny", reason: `Invalid sitemap target: ${request.url}` };
|
|
349
|
-
if (target.value.origin !== siteUrl.value.origin) {
|
|
353
|
+
if (target.value.origin !== siteUrl.value.origin && target.value.origin !== requestOrigin) {
|
|
350
354
|
return {
|
|
351
355
|
_tag: "deny",
|
|
352
356
|
reason: `Sitemap target origin ${target.value.origin} differs from ${siteUrl.value.origin}`
|
|
@@ -433,17 +437,21 @@ export async function fetchSitemapByRoute(event, route) {
|
|
|
433
437
|
const result = await crawlSitemapByRoute(event, { route });
|
|
434
438
|
return result._tag === "complete" ? { urls: result.urls } : { urls: result.urls, error: result.error };
|
|
435
439
|
}
|
|
436
|
-
export async function
|
|
440
|
+
export async function fetchSitemapUrlGroups(event, names) {
|
|
437
441
|
const sitemaps = getSitemapsFromConfig(event);
|
|
442
|
+
const groups = /* @__PURE__ */ new Map();
|
|
438
443
|
if (sitemaps.length > 0) {
|
|
439
444
|
logger.debug(`[sitemap] Multi-sitemap mode: ${sitemaps.length} sitemaps`);
|
|
440
|
-
const allUrls = [];
|
|
441
445
|
for (const sitemap of sitemaps) {
|
|
446
|
+
if (names !== void 0 && !names.includes(sitemap.name))
|
|
447
|
+
continue;
|
|
442
448
|
const { urls: urls2 } = await fetchSitemapByRoute(event, sitemap.route);
|
|
443
|
-
|
|
449
|
+
groups.set(sitemap.name, [...groups.get(sitemap.name) || [], ...urls2]);
|
|
444
450
|
}
|
|
445
|
-
return
|
|
451
|
+
return groups;
|
|
446
452
|
}
|
|
453
|
+
if (names !== void 0 && !names.includes("sitemap"))
|
|
454
|
+
return groups;
|
|
447
455
|
logger.debug(`[sitemap] Single sitemap mode: /sitemap.xml`);
|
|
448
456
|
const { urls, error } = await fetchSitemapByRoute(event, "/sitemap.xml");
|
|
449
457
|
if (error) {
|
|
@@ -451,5 +459,9 @@ export async function fetchSitemapUrls(event) {
|
|
|
451
459
|
} else if (urls.length === 0) {
|
|
452
460
|
logger.warn("Sitemap is empty - add routes to sitemap or configure sitemap.sources in nuxt.config");
|
|
453
461
|
}
|
|
454
|
-
|
|
462
|
+
groups.set("sitemap", urls);
|
|
463
|
+
return groups;
|
|
464
|
+
}
|
|
465
|
+
export async function fetchSitemapUrls(event) {
|
|
466
|
+
return [...(await fetchSitemapUrlGroups(event)).values()].flat();
|
|
455
467
|
}
|
package/dist/runtime/types.d.ts
CHANGED
|
@@ -185,7 +185,9 @@ export interface ModuleOptions {
|
|
|
185
185
|
/**
|
|
186
186
|
* Structured llms.txt configuration
|
|
187
187
|
*/
|
|
188
|
-
llmsTxt?: LlmsTxtConfig;
|
|
188
|
+
llmsTxt?: LlmsTxtConfig | false;
|
|
189
|
+
/** Generate and serve llms-full.txt. Set false to omit the export and its discovery link. */
|
|
190
|
+
llmsFullTxt?: boolean;
|
|
189
191
|
/**
|
|
190
192
|
* Generate a Markdown sitemap at `/sitemap.md` from indexed pages and end
|
|
191
193
|
* every Markdown page with a `## Sitemap` section linking to it.
|
|
@@ -512,6 +514,10 @@ export interface LlmsTxtLink {
|
|
|
512
514
|
* Section in llms.txt
|
|
513
515
|
*/
|
|
514
516
|
export interface LlmsTxtSection {
|
|
517
|
+
/** Generate a separate file from these sitemap names. Chunked sitemaps retain their configured name. */
|
|
518
|
+
sitemap?: string | string[];
|
|
519
|
+
/** Generated file path. Defaults to /llms/<sitemap>.txt. */
|
|
520
|
+
path?: string;
|
|
515
521
|
/** The title of the section */
|
|
516
522
|
title: string;
|
|
517
523
|
/** Description rendered in the heading-free preamble, or in each optional link note (can be array for multiple paragraphs) */
|
|
@@ -525,6 +531,12 @@ export interface LlmsTxtSection {
|
|
|
525
531
|
* Structured llms.txt configuration
|
|
526
532
|
*/
|
|
527
533
|
export interface LlmsTxtConfig {
|
|
534
|
+
/** Page admission. Sitemap URLs are authoritative by default. Use all to include stored pages outside the sitemap. */
|
|
535
|
+
pages?: 'sitemap' | 'all' | false;
|
|
536
|
+
/** Only read these sitemap names. An empty array disables sitemap page discovery. */
|
|
537
|
+
sitemaps?: string[];
|
|
538
|
+
/** Maximum characters per generated file. Large lists become linked files. Minimum 1024. Default 80000. */
|
|
539
|
+
maxLength?: number;
|
|
528
540
|
/**
|
|
529
541
|
* Rewrite automatically generated page links to their Markdown representation
|
|
530
542
|
* when the final static output contains the file or the deployment retains
|