nuxt-ai-ready 3.1.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.
Files changed (30) hide show
  1. package/dist/chunks/agent-skills.mjs +2 -1
  2. package/dist/chunks/prerender.mjs +196 -29
  3. package/dist/module.d.mts +1 -0
  4. package/dist/module.json +1 -1
  5. package/dist/module.mjs +2 -1
  6. package/dist/runtime/llms-txt-directive.d.ts +15 -0
  7. package/dist/runtime/llms-txt-directive.js +23 -0
  8. package/dist/runtime/llms-txt-files.d.ts +30 -0
  9. package/dist/runtime/llms-txt-files.js +141 -0
  10. package/dist/runtime/llms-txt-utils.d.ts +3 -2
  11. package/dist/runtime/llms-txt-utils.js +85 -221
  12. package/dist/runtime/markdown-links.d.ts +10 -0
  13. package/dist/runtime/markdown-links.js +52 -0
  14. package/dist/runtime/server/db/queries.d.ts +3 -0
  15. package/dist/runtime/server/db/queries.js +28 -2
  16. package/dist/runtime/server/middleware/markdown.js +9 -2
  17. package/dist/runtime/server/middleware/markdown.prerender.js +2 -1
  18. package/dist/runtime/server/plugins/llms-txt-directive-html.d.ts +3 -0
  19. package/dist/runtime/server/plugins/llms-txt-directive-html.js +8 -0
  20. package/dist/runtime/server/routes/llms-full.txt.get.js +2 -1
  21. package/dist/runtime/server/routes/llms.txt.get.js +19 -11
  22. package/dist/runtime/server/routes/sitemap.md.get.js +6 -3
  23. package/dist/runtime/server/utils/markdown-request.js +2 -5
  24. package/dist/runtime/server/utils/sitemap.d.ts +1 -0
  25. package/dist/runtime/server/utils/sitemap.js +22 -10
  26. package/dist/runtime/server/utils.js +2 -1
  27. package/dist/runtime/types.d.ts +30 -2
  28. package/dist/shared/{nuxt-ai-ready.Dmvy7CIJ.mjs → nuxt-ai-ready.BwnSMqRn.mjs} +94 -28
  29. package/package.json +1 -1
  30. package/skills/nuxt-ai-ready/SKILL.md +37 -2
@@ -1,22 +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 { formatLlmsTxtPageLink, normalizeLlmsTxtConfig } from "./llms-txt-format.js";
6
+ import { createLlmsTxtFiles } from "./llms-txt-files.js";
7
7
  import { hasMarkdownTwin, toMarkdownPath } from "./markdown-path.js";
8
8
  import { normalizePersistedRoute, toDeployedRoute, toLogicalRoute } from "./route-path.js";
9
- import { queryPages } from "./server/db/queries.js";
9
+ import { queryPageMetadata, queryPages } from "./server/db/queries.js";
10
10
  import { logger } from "./server/logger.js";
11
- import { fetchSitemapUrls } from "./server/utils/sitemap.js";
12
- export { normalizeLlmsTxtConfig };
11
+ import { fetchSitemapUrlGroups, getSitemapsFromConfig } from "./server/utils/sitemap.js";
12
+ export { normalizeLlmsTxtConfig } from "./llms-txt-format.js";
13
13
  async function findMarkdownLinkAvailability() {
14
- if (!import.meta.prerender) {
15
- return {
16
- runtimeMarkdownAvailable: true,
17
- paths: /* @__PURE__ */ new Set()
18
- };
19
- }
14
+ if (!import.meta.prerender)
15
+ return { runtimeMarkdownAvailable: true, paths: /* @__PURE__ */ new Set() };
20
16
  const { readMarkdownLinkAvailabilityFromFilesystem } = await import("#ai-ready-virtual/read-page-data.mjs");
21
17
  const availability = await readMarkdownLinkAvailabilityFromFilesystem();
22
18
  return {
@@ -24,229 +20,97 @@ async function findMarkdownLinkAvailability() {
24
20
  paths: new Set(availability.paths.map(decodePath))
25
21
  };
26
22
  }
27
- function getGroupPrefix(url, depth) {
28
- const segments = url.split("/").filter(Boolean);
29
- if (segments.length === 0)
30
- return "/";
31
- if (depth === 1 || segments.length === 1)
32
- return `/${segments[0]}`;
33
- return `/${segments[0]}/${segments[1]}`;
34
- }
35
- function getPathSegments(pathname) {
36
- return pathname.split("/").filter((s) => Boolean(s));
37
- }
38
- function analyzePageGroups(pages) {
39
- const twoSegmentCount = /* @__PURE__ */ new Map();
40
- const segmentHasNested = /* @__PURE__ */ new Map();
41
- for (const page of pages) {
42
- const prefix = getGroupPrefix(page.pathname, 2);
43
- twoSegmentCount.set(prefix, (twoSegmentCount.get(prefix) || 0) + 1);
44
- const segments = getPathSegments(page.pathname);
45
- const firstSegment = segments[0] || "";
46
- if (!segmentHasNested.has(firstSegment))
47
- segmentHasNested.set(firstSegment, false);
48
- if (segments.length > 1)
49
- segmentHasNested.set(firstSegment, true);
50
- }
51
- return { twoSegmentCount, segmentHasNested };
52
- }
53
- function getPageGroupKey(pathname, { twoSegmentCount, segmentHasNested }) {
54
- const segments = getPathSegments(pathname);
55
- const firstSegment = segments[0] || "";
56
- const twoSegPrefix = getGroupPrefix(pathname, 2);
57
- const twoSegCount = twoSegmentCount.get(twoSegPrefix) || 0;
58
- let groupKey = twoSegCount > 1 ? twoSegPrefix : `/${firstSegment}`;
59
- if (segments.length <= 1 && !segmentHasNested.get(firstSegment))
60
- groupKey = "";
61
- return groupKey;
62
- }
63
- function sortPagesByPath(pages) {
64
- const analysis = analyzePageGroups(pages);
65
- return pages.sort((a, b) => {
66
- const segmentsA = getPathSegments(a.pathname);
67
- const segmentsB = getPathSegments(b.pathname);
68
- const groupKeyA = getPageGroupKey(a.pathname, analysis);
69
- const groupKeyB = getPageGroupKey(b.pathname, analysis);
70
- if (groupKeyA === "" && groupKeyB !== "")
71
- return -1;
72
- if (groupKeyA !== "" && groupKeyB === "")
73
- return 1;
74
- if (groupKeyA !== groupKeyB)
75
- return groupKeyA.localeCompare(groupKeyB);
76
- if (segmentsA.length === 0)
77
- return -1;
78
- if (segmentsB.length === 0)
79
- return 1;
80
- const minLen = Math.min(segmentsA.length, segmentsB.length);
81
- for (let i = 0; i < minLen; i++) {
82
- const cmp = segmentsA[i].localeCompare(segmentsB[i]);
83
- if (cmp !== 0)
84
- return cmp;
85
- }
86
- return segmentsA.length - segmentsB.length;
87
- });
88
- }
89
- function formatPagesWithGroups(pages) {
90
- if (pages.length === 0)
91
- return [];
92
- const analysis = analyzePageGroups(pages);
93
- const lines = [];
94
- let currentGroup = "";
95
- let segmentGroupIndex = 0;
96
- let urlsInCurrentGroup = 0;
97
- for (const page of pages) {
98
- const groupKey = getPageGroupKey(page.pathname, analysis);
99
- if (groupKey !== currentGroup) {
100
- if (urlsInCurrentGroup > 0) {
101
- const shouldAddBlankLine = segmentGroupIndex === 0 || segmentGroupIndex >= 1 && segmentGroupIndex <= 2 && urlsInCurrentGroup > 1;
102
- if (shouldAddBlankLine)
103
- lines.push("");
104
- }
105
- currentGroup = groupKey;
106
- segmentGroupIndex++;
107
- urlsInCurrentGroup = 0;
108
- }
109
- urlsInCurrentGroup++;
110
- lines.push(formatLlmsTxtPageLink(page));
111
- }
112
- return lines;
113
- }
114
- export async function buildLlmsTxt(event) {
23
+ export async function buildLlmsTxtFiles(event) {
115
24
  const runtimeConfig = useRuntimeConfig(event);
116
25
  const aiReadyConfig = runtimeConfig["nuxt-ai-ready"];
117
- const sitemapConfig = runtimeConfig.sitemap;
118
26
  const siteConfig = getSiteConfig(event);
119
- const llmsTxtConfig = aiReadyConfig.llmsTxt;
120
- const i18n = aiReadyConfig.i18n;
121
- const i18nRuntime = i18n ? { _tag: "enabled", config: i18n, ...await import("./llms-txt-i18n.js") } : { _tag: "disabled" };
122
- const i18nContext = { host: getRequestHost(event, { xForwardedHost: true }) };
27
+ const config = aiReadyConfig.llmsTxt;
123
28
  const baseURL = runtimeConfig.app.baseURL;
124
- const resolvePath = (path) => withSiteTrailingSlash(event, toDeployedRoute(path, baseURL));
125
29
  const resolveUrl = (path) => withSiteUrl(event, toDeployedRoute(path, baseURL), { withBase: true });
30
+ const resolvePageUrl = (path) => resolveUrl(withSiteTrailingSlash(event, path));
126
31
  const canonicalSiteUrl = siteConfig.url ? resolveUrl("/") : void 0;
127
- const parts = [];
128
- parts.push(`# ${siteConfig.name || canonicalSiteUrl}`);
129
- if (siteConfig.description) {
130
- parts.push(`
131
- > ${siteConfig.description}`);
132
- }
133
- if (canonicalSiteUrl) {
134
- parts.push(`
135
- Canonical Origin: ${canonicalSiteUrl}`);
136
- }
137
- parts.push("");
138
- const sections = llmsTxtConfig.sections?.map((section) => ({
139
- ...section,
140
- links: section.links ? [...section.links] : void 0
141
- })) ?? [];
142
- if (sections[0]?.links) {
143
- const sitemapRoutes = sitemapConfig?.sitemaps ? Object.values(sitemapConfig.sitemaps).map((s) => s.sitemapName) : ["sitemap.xml"];
144
- for (const name of sitemapRoutes) {
145
- sections[0].links.push({
146
- title: name,
147
- href: resolveUrl(`/${name}`),
148
- description: "XML sitemap for search engines and crawlers."
149
- });
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." });
150
46
  }
151
- sections[0].links.push({
152
- title: "robots.txt",
153
- href: resolveUrl("/robots.txt"),
154
- description: "Crawler rules and permissions."
155
- });
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." });
156
50
  }
157
- const normalizedContent = normalizeLlmsTxtConfig({ ...llmsTxtConfig, sections });
158
- if (normalizedContent) {
159
- parts.push(normalizedContent);
160
- parts.push("");
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);
161
67
  }
162
- const urlsPromise = fetchSitemapUrls(event);
163
- let pages = [];
164
- let errorPages = [];
165
- try {
166
- ;
167
- [pages, errorPages] = await Promise.all([
168
- queryPages(event),
169
- queryPages(event, { where: { hasError: true } })
170
- ]);
171
- } catch (err) {
172
- logger.warn(
173
- `[ai-ready] Database unavailable for llms.txt, falling back to sitemap-only pages: ${err instanceof Error ? err.message : String(err)}`
174
- );
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
+ });
175
76
  }
176
- const urls = await urlsPromise;
177
- const sitemapPaths = urls.map((url) => toLogicalRoute(url.loc, baseURL));
178
- const sitemapPathSet = new Set(sitemapPaths);
179
- const seenPaths = /* @__PURE__ */ new Set();
180
- const errorSet = new Set(errorPages.map((p) => normalizePersistedRoute(p.route, sitemapPathSet, baseURL)));
181
- const prerendered = [];
182
- for (const page of pages) {
183
- const pathname = normalizePersistedRoute(page.route, sitemapPathSet, baseURL);
184
- if (seenPaths.has(pathname))
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))
185
90
  continue;
186
- seenPaths.add(pathname);
187
- prerendered.push({ pathname, title: page.title, description: page.description, locale: page.locale });
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 });
188
94
  }
189
- const devModeHint = import.meta.dev && prerendered.length === 0 ? " (dev mode - run `nuxi generate` for page titles)" : "";
190
- const other = [];
191
- for (const pathname of sitemapPaths) {
192
- if (!seenPaths.has(pathname) && !errorSet.has(pathname)) {
193
- const locale = i18nRuntime._tag === "enabled" ? i18nRuntime.resolveLocaleFromRoute(pathname, i18nRuntime.config, i18nContext).locale : void 0;
194
- other.push({ pathname, locale });
195
- seenPaths.add(pathname);
196
- }
197
- }
198
- let resolvePageHref = resolvePath;
199
- if (llmsTxtConfig.markdownLinks) {
200
- const markdownLinkAvailability = await findMarkdownLinkAvailability();
201
- resolvePageHref = (pathname) => {
202
- const deployedPathname = resolvePath(pathname);
203
- const markdownPath = resolvePath(toMarkdownPath(pathname));
204
- if (markdownLinkAvailability.paths.has(decodePath(markdownPath)) || markdownLinkAvailability.runtimeMarkdownAvailable && hasMarkdownTwin(pathname)) {
205
- return markdownPath;
206
- }
207
- return deployedPathname;
208
- };
209
- }
210
- for (const page of [...prerendered, ...other])
211
- page.href = resolvePageHref(page.pathname);
95
+ let rootContent;
212
96
  if (i18nRuntime._tag === "enabled") {
213
- const pageCounts = /* @__PURE__ */ new Map();
214
- for (const locale of i18nRuntime.config.locales) pageCounts.set(locale.code, 0);
215
- for (const p of [...prerendered, ...other]) {
216
- const code = p.locale || i18nRuntime.resolveLocaleFromRoute(p.pathname, i18nRuntime.config, i18nContext).locale;
217
- pageCounts.set(code, (pageCounts.get(code) ?? 0) + 1);
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);
218
104
  }
219
- parts.push(...i18nRuntime.formatAvailableLanguagesSection(i18nRuntime.config, pageCounts, resolvePageHref, i18nContext));
220
- parts.push("");
221
105
  }
222
- const isDefaultLocale = (item) => {
223
- if (i18nRuntime._tag === "disabled")
224
- return true;
225
- const code = item.locale || i18nRuntime.resolveLocaleFromRoute(item.pathname, i18nRuntime.config, i18nContext).locale;
226
- return code === i18nRuntime.config.defaultLocale;
227
- };
228
- const filteredPrerendered = i18nRuntime._tag === "enabled" ? prerendered.filter(isDefaultLocale) : prerendered;
229
- const filteredOther = i18nRuntime._tag === "enabled" ? other.filter(isDefaultLocale) : other;
230
- const sortedPrerendered = sortPagesByPath(filteredPrerendered);
231
- const sortedOther = sortPagesByPath(filteredOther);
232
- if (sortedPrerendered.length > 0 && sortedOther.length > 0) {
233
- parts.push(`## Prerendered Pages${devModeHint}
234
- `);
235
- parts.push(...formatPagesWithGroups(sortedPrerendered));
236
- parts.push("");
237
- parts.push("## Other Pages\n");
238
- parts.push(...formatPagesWithGroups(sortedOther));
239
- parts.push("");
240
- } else if (sortedPrerendered.length > 0) {
241
- parts.push(`## Pages${devModeHint}
242
- `);
243
- parts.push(...formatPagesWithGroups(sortedPrerendered));
244
- parts.push("");
245
- } else if (sortedOther.length > 0) {
246
- parts.push(`## Pages${devModeHint}
247
- `);
248
- parts.push(...formatPagesWithGroups(sortedOther));
249
- parts.push("");
250
- }
251
- 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;
252
116
  }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Resolve root-relative link and image destinations against a site origin.
3
+ *
4
+ * A Markdown page read on its own, by an agent or from `llms-full.txt`, has
5
+ * no page URL to resolve `/docs/api` against, so such a link loses its
6
+ * target. Fenced code and inline code spans keep their text.
7
+ */
8
+ export declare function absoluteMarkdownLinks(markdown: string, siteUrl: string): string;
9
+ /** A deployed path on the site's origin, or the path itself when no site URL is known. */
10
+ export declare function absoluteSiteUrl(path: string, siteUrl: string | undefined): string;
@@ -0,0 +1,52 @@
1
+ const RE_FENCE = /^ {0,3}(`{3,}|~{3,})/;
2
+ const RE_INLINE_ROOT_LINK = /\]\((<?)\/(?!\/)/g;
3
+ const RE_REFERENCE_ROOT_LINK = /^( {0,3}\[[^\]]+\]:[\t ]*<?)\/(?!\/)/;
4
+ function closesFence(line, fence) {
5
+ const trimmed = line.trim();
6
+ return trimmed.length >= fence.length && trimmed[0] === fence[0] && [...trimmed].every((char) => char === fence[0]);
7
+ }
8
+ function backtickRun(line, start) {
9
+ let end = start;
10
+ while (line[end] === "`")
11
+ end++;
12
+ return end - start;
13
+ }
14
+ function rewriteOutsideCodeSpans(line, rewrite) {
15
+ let output = "";
16
+ let last = 0;
17
+ let index = line.indexOf("`");
18
+ while (index !== -1) {
19
+ const length = backtickRun(line, index);
20
+ let close = line.indexOf("`", index + length);
21
+ while (close !== -1 && backtickRun(line, close) !== length)
22
+ close = line.indexOf("`", close + backtickRun(line, close));
23
+ if (close === -1) {
24
+ index = line.indexOf("`", index + length);
25
+ continue;
26
+ }
27
+ output += rewrite(line.slice(last, index)) + line.slice(index, close + length);
28
+ last = close + length;
29
+ index = line.indexOf("`", last);
30
+ }
31
+ return output + rewrite(line.slice(last));
32
+ }
33
+ export function absoluteMarkdownLinks(markdown, siteUrl) {
34
+ const origin = new URL(siteUrl).origin;
35
+ let fence;
36
+ return markdown.split(/(?<=\n)/).map((line) => {
37
+ if (fence) {
38
+ if (closesFence(line, fence))
39
+ fence = void 0;
40
+ return line;
41
+ }
42
+ const opening = line.match(RE_FENCE);
43
+ if (opening) {
44
+ fence = opening[1];
45
+ return line;
46
+ }
47
+ return rewriteOutsideCodeSpans(line, (text) => text.replace(RE_REFERENCE_ROOT_LINK, `$1${origin}/`).replace(RE_INLINE_ROOT_LINK, `]($1${origin}/`));
48
+ }).join("");
49
+ }
50
+ export function absoluteSiteUrl(path, siteUrl) {
51
+ return siteUrl ? new URL(path, new URL(siteUrl).origin).href : path;
52
+ }
@@ -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 = excludeErrors ? pages.filter((p) => !errorRoutes.has(p.route)) : pages;
98
- return filtered.map((p) => ({
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 = [];
@@ -1,6 +1,8 @@
1
1
  import { createError, defineEventHandler, getRequestURL, setHeader, setResponseStatus } from "#nuxtseo/h3";
2
2
  import { useNitroApp, useRuntimeConfig } from "#nuxtseo/nitro";
3
3
  import { resolveLocaleAlternateUrl } from "../../i18n-url.js";
4
+ import { insertLlmsTxtDirective } from "../../llms-txt-directive.js";
5
+ import { absoluteMarkdownLinks } from "../../markdown-links.js";
4
6
  import { logger } from "../logger.js";
5
7
  import { computeLocaleAlternates, resolveLocaleFromRoute } from "../utils/i18n.js";
6
8
  import { resolveMarkdownRedirect } from "../utils/markdown-redirect.js";
@@ -18,7 +20,7 @@ function notFoundMarkdown(ctx, canonicalUrl, build) {
18
20
  ``,
19
21
  `- [Sitemap](${resolveUrl("/sitemap.xml")})`,
20
22
  `- [llms.txt](${resolveUrl("/llms.txt")})`,
21
- `- [llms-full.txt](${resolveUrl("/llms-full.txt")})`,
23
+ ...config.llmsFullTxt === false ? [] : [`- [llms-full.txt](${resolveUrl("/llms-full.txt")})`],
22
24
  ``
23
25
  ].join("\n");
24
26
  const i18n = config.i18n;
@@ -49,7 +51,12 @@ export default defineEventHandler(async (event) => {
49
51
  const ctx = buildNegotiationContext(event, decision.path);
50
52
  const { path, config, resolvePath, resolveUrl, routeContext } = ctx;
51
53
  const canonicalUrl = resolveUrl(path);
52
- const finalizeMarkdown = (markdown) => config.sitemapMd === false ? markdown : appendSitemapSection(markdown, resolvePath(SITEMAP_MD_ROUTE));
54
+ const finalizeMarkdown = (markdown) => {
55
+ const withSitemap = config.sitemapMd === false ? markdown : appendSitemapSection(markdown, resolvePath(SITEMAP_MD_ROUTE));
56
+ const llmsTxt = config.llmsTxt;
57
+ const withDirective = llmsTxt?.directive?.markdown === false ? withSitemap : insertLlmsTxtDirective(withSitemap, resolveUrl("/llms.txt"));
58
+ return absoluteMarkdownLinks(withDirective, canonicalUrl);
59
+ };
53
60
  const [
54
61
  { tryGetContentMarkdown },
55
62
  { fetchRawWithEvent },
@@ -1,6 +1,7 @@
1
1
  import { createError, defineEventHandler } from "#nuxtseo/h3";
2
2
  import { useRuntimeConfig } from "#nuxtseo/nitro";
3
3
  import { withSiteUrl } from "#site-config/server/composables/utils";
4
+ import { absoluteMarkdownLinks } from "../../markdown-links.js";
4
5
  import { toDeployedRoute } from "../../route-path.js";
5
6
  import { logger } from "../logger.js";
6
7
  import { convertHtmlToMarkdown, extractLastUpdated } from "../utils.js";
@@ -52,7 +53,7 @@ export default defineEventHandler(async (event) => {
52
53
  last_updated: contentPage.updatedAt
53
54
  });
54
55
  const markdown = `${frontmatter}
55
- ${contentPage.markdown}`;
56
+ ${absoluteMarkdownLinks(contentPage.markdown, canonicalUrl)}`;
56
57
  const headings = extractHeadingsFromMarkdown(contentPage.markdown);
57
58
  const keywords2 = extractKeywords(contentPage.markdown, "");
58
59
  return JSON.stringify({
@@ -0,0 +1,3 @@
1
+ /** Registered only with `llmsTxt.directive.html: true`. */
2
+ declare const _default: any;
3
+ export default _default;
@@ -0,0 +1,8 @@
1
+ import { defineNitroPlugin, useRuntimeConfig } from "#nuxtseo/nitro";
2
+ import { llmsTxtDirectiveHtml } from "../../llms-txt-directive.js";
3
+ import { toDeployedRoute } from "../../route-path.js";
4
+ export default defineNitroPlugin((nitroApp) => {
5
+ nitroApp.hooks.hook("render:html", (html, { event }) => {
6
+ html.bodyPrepend.unshift(llmsTxtDirectiveHtml(toDeployedRoute("/llms.txt", useRuntimeConfig(event).app.baseURL)));
7
+ });
8
+ });
@@ -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 { buildLlmsTxt } from "../../llms-txt-utils.js";
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(buildLlmsTxt, {
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
- const runtimeConfig = useRuntimeConfig(event)["nuxt-ai-ready"];
21
- const cacheSeconds = runtimeConfig.llmsTxtCacheSeconds;
22
- const cacheEnabled = !import.meta.dev && cacheSeconds > 0;
23
- const content = cacheEnabled ? await getBuildLlmsTxtCached(cacheSeconds)(event) : await buildLlmsTxt(event);
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,7 +1,8 @@
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";
5
+ import { absoluteSiteUrl } from "../../markdown-links.js";
5
6
  import { toMarkdownPath } from "../../markdown-path.js";
6
7
  import { toDeployedRoute } from "../../route-path.js";
7
8
  import { queryPages } from "../db/queries.js";
@@ -17,11 +18,12 @@ async function buildSitemapMarkdown(event) {
17
18
  );
18
19
  }
19
20
  const baseURL = useRuntimeConfig(event).app.baseURL;
21
+ const siteConfig = getSiteConfig(event);
20
22
  return buildSitemapMd(
21
23
  pages.map((page) => ({ route: page.route, title: page.title, updatedAt: page.updatedAt })),
22
24
  {
23
- siteName: getSiteConfig(event).name,
24
- resolveHref: (route) => toDeployedRoute(toMarkdownPath(route), baseURL)
25
+ siteName: siteConfig.name,
26
+ resolveHref: (route) => absoluteSiteUrl(toDeployedRoute(toMarkdownPath(route), baseURL), siteConfig.url)
25
27
  }
26
28
  );
27
29
  }
@@ -40,6 +42,7 @@ function getBuildSitemapMdCached(maxAge) {
40
42
  return cached;
41
43
  }
42
44
  export default eventHandler(async (event) => {
45
+ assertMethod(event, "GET", true);
43
46
  const fullRuntimeConfig = useRuntimeConfig(event);
44
47
  const runtimeConfig = fullRuntimeConfig["nuxt-ai-ready"];
45
48
  const cacheSeconds = runtimeConfig.llmsTxtCacheSeconds ?? 600;