nuxt-ai-ready 1.5.5 → 1.5.7
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/module.json +1 -1
- package/dist/module.mjs +12 -10
- package/dist/runtime/llms-txt-format.d.ts +12 -1
- package/dist/runtime/llms-txt-format.js +91 -30
- package/dist/runtime/llms-txt-utils.js +40 -35
- package/dist/runtime/route-path.d.ts +10 -0
- package/dist/runtime/route-path.js +16 -0
- package/dist/runtime/server/db/queries.js +9 -9
- package/dist/runtime/server/middleware/markdown.js +16 -12
- package/dist/runtime/server/middleware/markdown.prerender.js +6 -3
- package/dist/runtime/server/routes/llms-full.txt.get.js +7 -2
- package/dist/runtime/server/utils/cloudflare.d.ts +1 -1
- package/dist/runtime/server/utils/cloudflare.js +3 -1
- package/dist/runtime/server/utils/indexnow-shared.js +3 -2
- package/dist/runtime/server/utils/link-header.js +4 -2
- package/dist/runtime/server/utils/llms-full.js +2 -2
- package/dist/runtime/server/utils/sitemap.js +45 -39
- package/dist/runtime/types.d.ts +3 -3
- package/package.json +13 -13
package/dist/module.json
CHANGED
package/dist/module.mjs
CHANGED
|
@@ -9,8 +9,9 @@ import { resolveNuxtContentVersion } from 'nuxtseo-shared/kit';
|
|
|
9
9
|
import { readPackageJSON, resolvePackageJSON } from 'pkg-types';
|
|
10
10
|
import { parseSitemapXml } from '@nuxtjs/sitemap/utils';
|
|
11
11
|
import { colorize } from 'consola/utils';
|
|
12
|
-
import { withBase } from 'ufo';
|
|
12
|
+
import { withLeadingSlash, withBase } from 'ufo';
|
|
13
13
|
import { normalizePagePath, toMarkdownPath } from '../dist/runtime/markdown-path.js';
|
|
14
|
+
import { toLogicalRoute, toDeployedRoute } from '../dist/runtime/route-path.js';
|
|
14
15
|
import { initSchema, computeContentHash, insertPage, queryAllPages, exportDbDump } from '../dist/runtime/server/db/shared.js';
|
|
15
16
|
import { comparePageHashes, submitToIndexNowShared } from '../dist/runtime/server/utils/indexnow-shared.js';
|
|
16
17
|
import { buildLlmsFullTxtHeader, formatPageForLlmsFullTxt } from '../dist/runtime/server/utils/llms-full.js';
|
|
@@ -25,7 +26,7 @@ const RE_HTML_MD_EXT = /\.(html|md)$/;
|
|
|
25
26
|
const RE_INDEX_SUFFIX = /\/index$/;
|
|
26
27
|
const RE_MD_EXT = /\.md$/;
|
|
27
28
|
async function fetchPreviousMeta(siteUrl, indexNow) {
|
|
28
|
-
const metaUrl =
|
|
29
|
+
const metaUrl = withBase("/__ai-ready/pages.meta.json", siteUrl);
|
|
29
30
|
logger.info(`Fetching previous build meta from ${metaUrl}`);
|
|
30
31
|
const controller = new AbortController();
|
|
31
32
|
const timeoutId = setTimeout(() => controller.abort(), BUILD_FETCH_TIMEOUT);
|
|
@@ -41,7 +42,7 @@ async function fetchPreviousMeta(siteUrl, indexNow) {
|
|
|
41
42
|
}
|
|
42
43
|
logger.info(`Previous build: ${prevMeta.pageCount} pages (buildId: ${prevMeta.buildId})`);
|
|
43
44
|
if (indexNow) {
|
|
44
|
-
const keyUrl =
|
|
45
|
+
const keyUrl = withBase(`/${indexNow}.txt`, siteUrl);
|
|
45
46
|
const keyLive = await fetch(keyUrl, { signal: AbortSignal.timeout(5e3) }).then((r) => r.ok).catch(() => false);
|
|
46
47
|
if (!keyLive) {
|
|
47
48
|
logger.info("IndexNow key file not live yet - IndexNow submission will be skipped");
|
|
@@ -161,7 +162,7 @@ async function processMarkdownRoute(state, nuxt, route, parsed, lastmod, options
|
|
|
161
162
|
async function processSitemapEntry(state, nuxt, nitro, entry) {
|
|
162
163
|
const loc = typeof entry === "string" ? entry : entry.loc;
|
|
163
164
|
const lastmod = typeof entry === "string" ? void 0 : entry.lastmod;
|
|
164
|
-
const route =
|
|
165
|
+
const route = toLogicalRoute(loc, nitro.options.baseURL);
|
|
165
166
|
if (route.split("/").some((segment) => segment.startsWith("_"))) {
|
|
166
167
|
return { crawled: false, skipped: true };
|
|
167
168
|
}
|
|
@@ -169,7 +170,7 @@ async function processSitemapEntry(state, nuxt, nitro, entry) {
|
|
|
169
170
|
return { crawled: false, skipped: true };
|
|
170
171
|
}
|
|
171
172
|
const mdRoute = toMarkdownPath(route);
|
|
172
|
-
const mdUrl =
|
|
173
|
+
const mdUrl = toDeployedRoute(mdRoute, nitro.options.baseURL);
|
|
173
174
|
logger.debug(`Fetching markdown for ${route} \u2192 ${mdUrl}`);
|
|
174
175
|
const res = await globalThis.$fetch(mdUrl, {
|
|
175
176
|
headers: { "x-nitro-prerender": mdRoute },
|
|
@@ -292,7 +293,8 @@ function setupPrerenderHandler(options, dbPath, siteInfo, llmsTxtConfig, indexNo
|
|
|
292
293
|
let initPromise = null;
|
|
293
294
|
nitro.hooks.hook("prerender:generate", async (route) => {
|
|
294
295
|
if (route.error) {
|
|
295
|
-
const
|
|
296
|
+
const routePath = route.fileName || route.route;
|
|
297
|
+
const pageRoute2 = withLeadingSlash(routePath).replace(RE_HTML_MD_EXT, "").replace(RE_INDEX_SUFFIX, "").replace(RE_HTML_MD_EXT, "") || "/";
|
|
296
298
|
state.errorRoutes.add(pageRoute2);
|
|
297
299
|
logger.debug(`Detected error page: ${pageRoute2}`);
|
|
298
300
|
return;
|
|
@@ -614,7 +616,7 @@ const module$1 = defineNuxtModule({
|
|
|
614
616
|
version: ">=6.0.0"
|
|
615
617
|
},
|
|
616
618
|
"@nuxtjs/sitemap": {
|
|
617
|
-
version: ">=8.
|
|
619
|
+
version: ">=8.3.0"
|
|
618
620
|
},
|
|
619
621
|
"nuxt-site-config": {
|
|
620
622
|
version: ">=3.2"
|
|
@@ -727,7 +729,7 @@ const module$1 = defineNuxtModule({
|
|
|
727
729
|
}
|
|
728
730
|
registerTypeTemplates();
|
|
729
731
|
const defaultLlmsTxtSections = [];
|
|
730
|
-
const llmsFullRoute = withSiteUrl("llms-full.txt");
|
|
732
|
+
const llmsFullRoute = withSiteUrl("llms-full.txt", { withBase: true });
|
|
731
733
|
defaultLlmsTxtSections.push({
|
|
732
734
|
title: "LLM Resources",
|
|
733
735
|
links: [
|
|
@@ -750,7 +752,7 @@ const module$1 = defineNuxtModule({
|
|
|
750
752
|
});
|
|
751
753
|
const mcpLink = {
|
|
752
754
|
title: "MCP",
|
|
753
|
-
href: withSiteUrl(nuxt.options.mcp !== false && nuxt.options.mcp?.route || "/mcp"),
|
|
755
|
+
href: withSiteUrl(nuxt.options.mcp !== false && nuxt.options.mcp?.route || "/mcp", { withBase: true }),
|
|
754
756
|
description: "Model Context Protocol server endpoint for AI agent integration."
|
|
755
757
|
};
|
|
756
758
|
if (defaultLlmsTxtSections[0]) {
|
|
@@ -1038,7 +1040,7 @@ export async function lookupContentPage(event, path) {
|
|
|
1038
1040
|
const siteConfig = useSiteConfig();
|
|
1039
1041
|
setupPrerenderHandler(config, buildDbPath, {
|
|
1040
1042
|
name: siteConfig.name,
|
|
1041
|
-
url: siteConfig.url,
|
|
1043
|
+
url: siteConfig.url ? withSiteUrl("/", { withBase: true }) : void 0,
|
|
1042
1044
|
description: siteConfig.description
|
|
1043
1045
|
}, mergedLlmsTxt, indexNow, { ftsTokenizer, i18n: i18nConfig });
|
|
1044
1046
|
}
|
|
@@ -1,8 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Pure formatting functions for llms.txt - no runtime dependencies
|
|
3
3
|
*/
|
|
4
|
+
import type { RuntimeI18nConfig } from './server/utils/i18n.js';
|
|
4
5
|
import type { LlmsTxtConfig } from './types.js';
|
|
6
|
+
export declare function formatAvailableLanguagesSection(i18n: RuntimeI18nConfig, pageCounts: Map<string, number>, resolveHref?: (pathname: string) => string): string[];
|
|
7
|
+
interface LlmsTxtPageLink {
|
|
8
|
+
pathname: string;
|
|
9
|
+
href?: string;
|
|
10
|
+
title?: string;
|
|
11
|
+
description?: string;
|
|
12
|
+
}
|
|
13
|
+
export declare function formatLlmsTxtPageLink(page: LlmsTxtPageLink): string;
|
|
5
14
|
/**
|
|
6
|
-
* Normalize llms.txt structured configuration to
|
|
15
|
+
* Normalize llms.txt structured configuration to the llmstxt.org Markdown format.
|
|
16
|
+
* Preamble prose must appear before the first H2; every H2 body is a file list.
|
|
7
17
|
*/
|
|
8
18
|
export declare function normalizeLlmsTxtConfig(config: LlmsTxtConfig): string;
|
|
19
|
+
export {};
|
|
@@ -1,39 +1,100 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
1
|
+
import { localePath } from "./server/utils/i18n.js";
|
|
2
|
+
const RE_INLINE_WHITESPACE = /\s+/g;
|
|
3
|
+
const RE_LINK_TITLE_BRACKET = /[[\]]/g;
|
|
4
|
+
const RE_LINK_HREF_UNSAFE = /[\s()]/g;
|
|
5
|
+
const RE_PREAMBLE_ATX_HEADING = /^( {0,3})(#{1,6})(?=\s)/gm;
|
|
6
|
+
function normalizeInlineText(value) {
|
|
7
|
+
return value.trim().replace(RE_INLINE_WHITESPACE, " ");
|
|
7
8
|
}
|
|
8
|
-
function
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
9
|
+
function normalizeLinkTitle(value) {
|
|
10
|
+
return normalizeInlineText(value).replace(RE_LINK_TITLE_BRACKET, (bracket) => bracket === "[" ? "[" : "]");
|
|
11
|
+
}
|
|
12
|
+
function normalizeLinkHref(value) {
|
|
13
|
+
return value.trim().replace(RE_LINK_HREF_UNSAFE, (character) => {
|
|
14
|
+
if (character === "(")
|
|
15
|
+
return "%28";
|
|
16
|
+
if (character === ")")
|
|
17
|
+
return "%29";
|
|
18
|
+
return encodeURIComponent(character);
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
function normalizeDescriptions(value) {
|
|
22
|
+
return (Array.isArray(value) ? value : value ? [value] : []).filter((description) => description.trim()).map(normalizeInlineText);
|
|
23
|
+
}
|
|
24
|
+
function normalizePreambleBlocks(value) {
|
|
25
|
+
return (Array.isArray(value) ? value : value ? [value] : []).filter((block) => block.trim()).map((block) => block.trim().replace(RE_PREAMBLE_ATX_HEADING, "$1\\$2"));
|
|
26
|
+
}
|
|
27
|
+
function normalizeLink(link, descriptionPrefixes = []) {
|
|
28
|
+
const descriptions = [...descriptionPrefixes, link.description].filter((value) => Boolean(value?.trim())).map(normalizeInlineText);
|
|
29
|
+
const description = descriptions.join("; ");
|
|
30
|
+
return `- [${normalizeLinkTitle(link.title)}](${normalizeLinkHref(link.href)})${description ? `: ${description}` : ""}`;
|
|
31
|
+
}
|
|
32
|
+
function normalizeSection(section) {
|
|
33
|
+
const parts = [`## ${normalizeInlineText(section.title)}`];
|
|
18
34
|
if (section.links?.length)
|
|
19
|
-
parts.push(...section.links.map(normalizeLink));
|
|
35
|
+
parts.push("", ...section.links.map((link) => normalizeLink(link)));
|
|
20
36
|
return parts.join("\n");
|
|
21
37
|
}
|
|
22
|
-
|
|
38
|
+
function normalizePreamble(config) {
|
|
23
39
|
const parts = [];
|
|
24
|
-
const required = config.sections?.filter((s) => !s.optional) ?? [];
|
|
25
|
-
const optional = config.sections?.filter((s) => s.optional) ?? [];
|
|
26
|
-
if (required.length)
|
|
27
|
-
parts.push(...required.map((s) => normalizeSection(s)));
|
|
28
|
-
if (optional.length) {
|
|
29
|
-
parts.push("## Optional");
|
|
30
|
-
parts.push(...optional.map((s) => normalizeSection(s, 3)));
|
|
31
|
-
}
|
|
32
40
|
if (config.notes) {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
41
|
+
const notes = normalizePreambleBlocks(config.notes);
|
|
42
|
+
if (notes.length)
|
|
43
|
+
parts.push(["**Notes:**", ...notes].join("\n\n"));
|
|
44
|
+
}
|
|
45
|
+
for (const section of config.sections?.filter((section2) => !section2.optional) ?? []) {
|
|
46
|
+
if (!section.description)
|
|
47
|
+
continue;
|
|
48
|
+
const descriptions = normalizePreambleBlocks(section.description);
|
|
49
|
+
if (descriptions.length)
|
|
50
|
+
parts.push([`**${normalizeInlineText(section.title)}:**`, ...descriptions].join("\n\n"));
|
|
37
51
|
}
|
|
52
|
+
return parts;
|
|
53
|
+
}
|
|
54
|
+
function normalizeOptionalSections(sections) {
|
|
55
|
+
const links = sections.flatMap(
|
|
56
|
+
(section) => section.links?.map((link) => normalizeLink(link, [section.title, ...normalizeDescriptions(section.description)])) ?? []
|
|
57
|
+
);
|
|
58
|
+
if (!links.length)
|
|
59
|
+
return void 0;
|
|
60
|
+
return ["## Optional", "", ...links].join("\n");
|
|
61
|
+
}
|
|
62
|
+
function normalizeRequiredSections(sections) {
|
|
63
|
+
return sections.filter((section) => section.links?.length).map(normalizeSection);
|
|
64
|
+
}
|
|
65
|
+
export function formatAvailableLanguagesSection(i18n, pageCounts, resolveHref = (pathname) => pathname) {
|
|
66
|
+
const lines = ["## Available Languages on Website", ""];
|
|
67
|
+
for (const locale of i18n.locales) {
|
|
68
|
+
const isDefault = locale.code === i18n.defaultLocale;
|
|
69
|
+
const prefix = localePath("/", locale.code, i18n);
|
|
70
|
+
const count = pageCounts.get(locale.code) ?? 0;
|
|
71
|
+
const display = locale.nativeName ? `${locale.nativeName} (${locale.code})` : locale.name ? `${locale.name} (${locale.code})` : locale.code;
|
|
72
|
+
const suffix = isDefault ? "content included below" : "visit this language for content";
|
|
73
|
+
lines.push(normalizeLink({
|
|
74
|
+
title: display,
|
|
75
|
+
href: resolveHref(prefix),
|
|
76
|
+
description: `${count} pages; ${suffix}.`
|
|
77
|
+
}));
|
|
78
|
+
}
|
|
79
|
+
return lines;
|
|
80
|
+
}
|
|
81
|
+
export function formatLlmsTxtPageLink(page) {
|
|
82
|
+
const description = page.description?.trim().replace(RE_INLINE_WHITESPACE, " ");
|
|
83
|
+
const normalizedTitle = page.title ? normalizeInlineText(page.title) : "";
|
|
84
|
+
const title = normalizedTitle && normalizedTitle !== page.pathname ? normalizedTitle : page.pathname;
|
|
85
|
+
const href = (page.href || page.pathname).trim();
|
|
86
|
+
const truncatedDescription = description ? `${description.substring(0, 160)}${description.length > 160 ? "..." : ""}` : void 0;
|
|
87
|
+
return normalizeLink({ title, href, description: truncatedDescription });
|
|
88
|
+
}
|
|
89
|
+
export function normalizeLlmsTxtConfig(config) {
|
|
90
|
+
const required = config.sections?.filter((section) => !section.optional) ?? [];
|
|
91
|
+
const optional = config.sections?.filter((section) => section.optional) ?? [];
|
|
92
|
+
const parts = [
|
|
93
|
+
...normalizePreamble(config),
|
|
94
|
+
...normalizeRequiredSections(required)
|
|
95
|
+
];
|
|
96
|
+
const optionalSection = normalizeOptionalSections(optional);
|
|
97
|
+
if (optionalSection)
|
|
98
|
+
parts.push(optionalSection);
|
|
38
99
|
return parts.join("\n\n");
|
|
39
100
|
}
|
|
@@ -1,23 +1,13 @@
|
|
|
1
1
|
import { useRuntimeConfig } from "nitropack/runtime";
|
|
2
2
|
import { getSiteConfig } from "#site-config/server/composables/getSiteConfig";
|
|
3
|
-
import {
|
|
3
|
+
import { withSiteTrailingSlash, withSiteUrl } from "#site-config/server/composables/utils";
|
|
4
|
+
import { formatAvailableLanguagesSection, formatLlmsTxtPageLink, normalizeLlmsTxtConfig } from "./llms-txt-format.js";
|
|
5
|
+
import { normalizePersistedRoute, toDeployedRoute, toLogicalRoute } from "./route-path.js";
|
|
4
6
|
import { queryPages } from "./server/db/queries.js";
|
|
5
7
|
import { logger } from "./server/logger.js";
|
|
6
|
-
import {
|
|
8
|
+
import { resolveLocaleFromRoute } from "./server/utils/i18n.js";
|
|
7
9
|
import { fetchSitemapUrls } from "./server/utils/sitemap.js";
|
|
8
10
|
export { normalizeLlmsTxtConfig };
|
|
9
|
-
function formatAvailableLanguagesSection(i18n, pageCounts) {
|
|
10
|
-
const lines = ["## Available Languages on Website", ""];
|
|
11
|
-
for (const locale of i18n.locales) {
|
|
12
|
-
const isDefault = locale.code === i18n.defaultLocale;
|
|
13
|
-
const prefix = localePath("/", locale.code, i18n);
|
|
14
|
-
const count = pageCounts.get(locale.code) ?? 0;
|
|
15
|
-
const display = locale.nativeName ? `${locale.nativeName} (${locale.code})` : locale.name ? `${locale.name} (${locale.code})` : locale.code;
|
|
16
|
-
const suffix = isDefault ? "Content included below" : "Visit website for content";
|
|
17
|
-
lines.push(`- ${display} - ${count} pages - ${prefix} - ${suffix}`);
|
|
18
|
-
}
|
|
19
|
-
return lines;
|
|
20
|
-
}
|
|
21
11
|
function getGroupPrefix(url, depth) {
|
|
22
12
|
const segments = url.split("/").filter(Boolean);
|
|
23
13
|
if (segments.length === 0)
|
|
@@ -101,11 +91,7 @@ function formatPagesWithGroups(pages) {
|
|
|
101
91
|
urlsInCurrentGroup = 0;
|
|
102
92
|
}
|
|
103
93
|
urlsInCurrentGroup++;
|
|
104
|
-
|
|
105
|
-
if (page.title && page.title !== page.pathname)
|
|
106
|
-
lines.push(`- [${page.title}](${page.pathname})${descText}`);
|
|
107
|
-
else
|
|
108
|
-
lines.push(`- ${page.pathname}${descText}`);
|
|
94
|
+
lines.push(formatLlmsTxtPageLink(page));
|
|
109
95
|
}
|
|
110
96
|
return lines;
|
|
111
97
|
}
|
|
@@ -116,26 +102,39 @@ export async function buildLlmsTxt(event) {
|
|
|
116
102
|
const siteConfig = getSiteConfig(event);
|
|
117
103
|
const llmsTxtConfig = aiReadyConfig.llmsTxt;
|
|
118
104
|
const i18n = aiReadyConfig.i18n;
|
|
105
|
+
const baseURL = runtimeConfig.app.baseURL;
|
|
106
|
+
const resolvePath = (path) => withSiteTrailingSlash(event, toDeployedRoute(path, baseURL));
|
|
107
|
+
const resolveUrl = (path) => withSiteUrl(event, toDeployedRoute(path, baseURL));
|
|
108
|
+
const canonicalSiteUrl = siteConfig.url ? resolveUrl("/") : void 0;
|
|
119
109
|
const parts = [];
|
|
120
|
-
parts.push(`# ${siteConfig.name ||
|
|
110
|
+
parts.push(`# ${siteConfig.name || canonicalSiteUrl}`);
|
|
121
111
|
if (siteConfig.description) {
|
|
122
112
|
parts.push(`
|
|
123
113
|
> ${siteConfig.description}`);
|
|
124
114
|
}
|
|
125
|
-
if (
|
|
115
|
+
if (canonicalSiteUrl) {
|
|
126
116
|
parts.push(`
|
|
127
|
-
Canonical Origin: ${
|
|
117
|
+
Canonical Origin: ${canonicalSiteUrl}`);
|
|
128
118
|
}
|
|
129
119
|
parts.push("");
|
|
130
|
-
const sections = llmsTxtConfig.sections
|
|
120
|
+
const sections = llmsTxtConfig.sections?.map((section) => ({
|
|
121
|
+
...section,
|
|
122
|
+
links: section.links ? [...section.links] : void 0
|
|
123
|
+
})) ?? [];
|
|
131
124
|
if (sections[0]?.links) {
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
125
|
+
const sitemapRoutes = sitemapConfig?.sitemaps ? Object.values(sitemapConfig.sitemaps).map((s) => s.sitemapName) : ["sitemap.xml"];
|
|
126
|
+
for (const name of sitemapRoutes) {
|
|
127
|
+
sections[0].links.push({
|
|
128
|
+
title: name,
|
|
129
|
+
href: resolveUrl(`/${name}`),
|
|
130
|
+
description: "XML sitemap for search engines and crawlers."
|
|
131
|
+
});
|
|
137
132
|
}
|
|
138
|
-
sections[0].links.push({
|
|
133
|
+
sections[0].links.push({
|
|
134
|
+
title: "robots.txt",
|
|
135
|
+
href: resolveUrl("/robots.txt"),
|
|
136
|
+
description: "Crawler rules and permissions."
|
|
137
|
+
});
|
|
139
138
|
}
|
|
140
139
|
const normalizedContent = normalizeLlmsTxtConfig({ ...llmsTxtConfig, sections });
|
|
141
140
|
if (normalizedContent) {
|
|
@@ -157,23 +156,29 @@ Canonical Origin: ${siteConfig.url}`);
|
|
|
157
156
|
);
|
|
158
157
|
}
|
|
159
158
|
const urls = await urlsPromise;
|
|
159
|
+
const sitemapPaths = urls.map((url) => toLogicalRoute(url.loc, baseURL));
|
|
160
|
+
const sitemapPathSet = new Set(sitemapPaths);
|
|
160
161
|
const seenPaths = /* @__PURE__ */ new Set();
|
|
161
|
-
const errorSet = new Set(errorPages.map((p) => p.route));
|
|
162
|
+
const errorSet = new Set(errorPages.map((p) => normalizePersistedRoute(p.route, sitemapPathSet, baseURL)));
|
|
162
163
|
const prerendered = [];
|
|
163
164
|
for (const page of pages) {
|
|
164
|
-
|
|
165
|
-
|
|
165
|
+
const pathname = normalizePersistedRoute(page.route, sitemapPathSet, baseURL);
|
|
166
|
+
if (seenPaths.has(pathname))
|
|
167
|
+
continue;
|
|
168
|
+
seenPaths.add(pathname);
|
|
169
|
+
prerendered.push({ pathname, title: page.title, description: page.description, locale: page.locale });
|
|
166
170
|
}
|
|
167
171
|
const devModeHint = import.meta.dev && prerendered.length === 0 ? " (dev mode - run `nuxi generate` for page titles)" : "";
|
|
168
172
|
const other = [];
|
|
169
|
-
for (const
|
|
170
|
-
const pathname = url.loc.startsWith("/") ? url.loc : new URL(url.loc).pathname;
|
|
173
|
+
for (const pathname of sitemapPaths) {
|
|
171
174
|
if (!seenPaths.has(pathname) && !errorSet.has(pathname)) {
|
|
172
175
|
const locale = i18n ? resolveLocaleFromRoute(pathname, i18n).locale : void 0;
|
|
173
176
|
other.push({ pathname, locale });
|
|
174
177
|
seenPaths.add(pathname);
|
|
175
178
|
}
|
|
176
179
|
}
|
|
180
|
+
for (const page of [...prerendered, ...other])
|
|
181
|
+
page.href = resolvePath(page.pathname);
|
|
177
182
|
if (i18n) {
|
|
178
183
|
const pageCounts = /* @__PURE__ */ new Map();
|
|
179
184
|
for (const locale of i18n.locales) pageCounts.set(locale.code, 0);
|
|
@@ -181,7 +186,7 @@ Canonical Origin: ${siteConfig.url}`);
|
|
|
181
186
|
const code = p.locale || resolveLocaleFromRoute(p.pathname, i18n).locale;
|
|
182
187
|
pageCounts.set(code, (pageCounts.get(code) ?? 0) + 1);
|
|
183
188
|
}
|
|
184
|
-
parts.push(...formatAvailableLanguagesSection(i18n, pageCounts));
|
|
189
|
+
parts.push(...formatAvailableLanguagesSection(i18n, pageCounts, resolvePath));
|
|
185
190
|
parts.push("");
|
|
186
191
|
}
|
|
187
192
|
const isDefaultLocale = (item) => {
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** Convert a sitemap URL (which includes app.baseURL) to a logical route. */
|
|
2
|
+
export declare function toLogicalRoute(pathOrUrl: string, baseURL: string): string;
|
|
3
|
+
/** Apply app.baseURL to a logical route exactly once. */
|
|
4
|
+
export declare function toDeployedRoute(route: string, baseURL: string): string;
|
|
5
|
+
/**
|
|
6
|
+
* Normalize rows written by older releases, which stored deployed paths.
|
|
7
|
+
* A sitemap match disambiguates those rows from real logical routes whose
|
|
8
|
+
* first segment happens to match app.baseURL.
|
|
9
|
+
*/
|
|
10
|
+
export declare function normalizePersistedRoute(route: string, sitemapRoutes: ReadonlySet<string>, baseURL: string): string;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { joinURL, parseURL, withLeadingSlash, withoutBase } from "ufo";
|
|
2
|
+
import { normalizePagePath } from "./markdown-path.js";
|
|
3
|
+
export function toLogicalRoute(pathOrUrl, baseURL) {
|
|
4
|
+
const pathname = withLeadingSlash(parseURL(pathOrUrl).pathname);
|
|
5
|
+
return normalizePagePath(withoutBase(pathname, baseURL));
|
|
6
|
+
}
|
|
7
|
+
export function toDeployedRoute(route, baseURL) {
|
|
8
|
+
return joinURL(baseURL, withLeadingSlash(route));
|
|
9
|
+
}
|
|
10
|
+
export function normalizePersistedRoute(route, sitemapRoutes, baseURL) {
|
|
11
|
+
const normalizedRoute = normalizePagePath(withLeadingSlash(parseURL(route).pathname));
|
|
12
|
+
if (sitemapRoutes.has(normalizedRoute))
|
|
13
|
+
return normalizedRoute;
|
|
14
|
+
const logicalRoute = toLogicalRoute(normalizedRoute, baseURL);
|
|
15
|
+
return sitemapRoutes.has(logicalRoute) ? logicalRoute : normalizedRoute;
|
|
16
|
+
}
|
|
@@ -57,17 +57,17 @@ async function getPrerenderDb() {
|
|
|
57
57
|
const excludeErrors = sql.includes("is_error = 0");
|
|
58
58
|
const includeMarkdown = wantsMarkdown(sql);
|
|
59
59
|
if (isErrorQuery) {
|
|
60
|
-
return
|
|
61
|
-
route
|
|
62
|
-
title:
|
|
63
|
-
description:
|
|
64
|
-
...includeMarkdown ? { markdown:
|
|
65
|
-
headings:
|
|
66
|
-
keywords:
|
|
67
|
-
updated_at:
|
|
60
|
+
return [...errorRoutes].map((route) => ({
|
|
61
|
+
route,
|
|
62
|
+
title: "",
|
|
63
|
+
description: "",
|
|
64
|
+
...includeMarkdown ? { markdown: "" } : {},
|
|
65
|
+
headings: "[]",
|
|
66
|
+
keywords: "[]",
|
|
67
|
+
updated_at: "",
|
|
68
68
|
is_error: 1,
|
|
69
69
|
indexed: 0,
|
|
70
|
-
locale:
|
|
70
|
+
locale: ""
|
|
71
71
|
}));
|
|
72
72
|
}
|
|
73
73
|
const filtered = excludeErrors ? pages.filter((p) => !errorRoutes.has(p.route)) : pages;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createError, defineEventHandler, getHeader, sendRedirect, setHeader } from "h3";
|
|
2
2
|
import { useRuntimeConfig } from "nitropack/runtime";
|
|
3
3
|
import { withSiteUrl } from "#site-config/server/composables/utils";
|
|
4
|
+
import { toDeployedRoute } from "../../route-path.js";
|
|
4
5
|
import { queryPages } from "../db/queries.js";
|
|
5
6
|
import { logger } from "../logger.js";
|
|
6
7
|
import { convertHtmlToMarkdown, extractLastUpdated, getMarkdownRenderInfo, toMarkdownPath } from "../utils.js";
|
|
@@ -23,7 +24,7 @@ function setMarkdownHeaders(event, path, config, resolveUrl) {
|
|
|
23
24
|
setHeader(event, "cache-control", cacheControl);
|
|
24
25
|
}
|
|
25
26
|
}
|
|
26
|
-
function notFoundMarkdown(canonicalUrl, path, config) {
|
|
27
|
+
function notFoundMarkdown(canonicalUrl, path, config, resolveUrl) {
|
|
27
28
|
const body = [
|
|
28
29
|
`# Page not found`,
|
|
29
30
|
``,
|
|
@@ -31,14 +32,14 @@ function notFoundMarkdown(canonicalUrl, path, config) {
|
|
|
31
32
|
``,
|
|
32
33
|
`Try one of these resources:`,
|
|
33
34
|
``,
|
|
34
|
-
`- [Sitemap](/sitemap.xml)`,
|
|
35
|
-
`- [llms.txt](/llms.txt)`,
|
|
36
|
-
`- [llms-full.txt](/llms-full.txt)`,
|
|
35
|
+
`- [Sitemap](${resolveUrl("/sitemap.xml")})`,
|
|
36
|
+
`- [llms.txt](${resolveUrl("/llms.txt")})`,
|
|
37
|
+
`- [llms-full.txt](${resolveUrl("/llms-full.txt")})`,
|
|
37
38
|
``
|
|
38
39
|
].join("\n");
|
|
39
40
|
const i18n = config.i18n;
|
|
40
41
|
const locale = i18n ? resolveLocaleFromRoute(path, i18n).locale : void 0;
|
|
41
|
-
const alternates = i18n ? computeLocaleAlternates(path, i18n).map((a) => ({ hreflang: a.hreflang, href: a.path })) : void 0;
|
|
42
|
+
const alternates = i18n ? computeLocaleAlternates(path, i18n).map((a) => ({ hreflang: a.hreflang, href: resolveUrl(a.path) })) : void 0;
|
|
42
43
|
const frontmatter = buildFrontmatter({
|
|
43
44
|
title: "Page not found",
|
|
44
45
|
description: `No content is available at ${path}.`,
|
|
@@ -64,15 +65,18 @@ export default defineEventHandler(async (event) => {
|
|
|
64
65
|
});
|
|
65
66
|
}
|
|
66
67
|
const { path, isExplicit, negotiation } = renderInfo;
|
|
67
|
-
const
|
|
68
|
-
const
|
|
68
|
+
const runtimeConfig = useRuntimeConfig(event);
|
|
69
|
+
const config = runtimeConfig["nuxt-ai-ready"];
|
|
70
|
+
const baseURL = runtimeConfig.app.baseURL;
|
|
71
|
+
const resolvePath = (path2) => toDeployedRoute(path2, baseURL);
|
|
72
|
+
const resolveUrl = (path2) => withSiteUrl(event, resolvePath(path2));
|
|
69
73
|
const canonicalUrl = resolveUrl(path);
|
|
70
74
|
if (negotiation === "html") {
|
|
71
75
|
setNegotiationHeaders(event, path, config, resolveUrl);
|
|
72
76
|
return;
|
|
73
77
|
}
|
|
74
78
|
if (!isExplicit) {
|
|
75
|
-
return sendRedirect(event, toMarkdownPath(path), 307);
|
|
79
|
+
return sendRedirect(event, resolvePath(toMarkdownPath(path)), 307);
|
|
76
80
|
}
|
|
77
81
|
const contentPage = await tryGetContentMarkdown(event, path).catch((e) => {
|
|
78
82
|
logger.debug(`[markdown] Content lookup failed for ${path}`, e);
|
|
@@ -91,7 +95,7 @@ export default defineEventHandler(async (event) => {
|
|
|
91
95
|
${contentPage.markdown}`;
|
|
92
96
|
}
|
|
93
97
|
logger.debug(`[markdown] Fetching HTML for ${path}`);
|
|
94
|
-
const response = await event.fetch(path, {
|
|
98
|
+
const response = await event.fetch(resolvePath(path), {
|
|
95
99
|
headers: { [INTERNAL_HEADER]: "1" },
|
|
96
100
|
redirect: "manual"
|
|
97
101
|
}).catch((e) => {
|
|
@@ -100,7 +104,7 @@ ${contentPage.markdown}`;
|
|
|
100
104
|
});
|
|
101
105
|
if (!response) {
|
|
102
106
|
setMarkdownHeaders(event, path, config, resolveUrl);
|
|
103
|
-
return notFoundMarkdown(canonicalUrl, path, config);
|
|
107
|
+
return notFoundMarkdown(canonicalUrl, path, config, resolveUrl);
|
|
104
108
|
}
|
|
105
109
|
if (response.status >= 300 && response.status < 400) {
|
|
106
110
|
const location = response.headers.get("location");
|
|
@@ -115,12 +119,12 @@ ${contentPage.markdown}`;
|
|
|
115
119
|
}
|
|
116
120
|
if (!response.ok) {
|
|
117
121
|
setMarkdownHeaders(event, path, config, resolveUrl);
|
|
118
|
-
return notFoundMarkdown(canonicalUrl, path, config);
|
|
122
|
+
return notFoundMarkdown(canonicalUrl, path, config, resolveUrl);
|
|
119
123
|
}
|
|
120
124
|
const contentType = response.headers.get("content-type") || "";
|
|
121
125
|
if (!contentType.includes("text/html")) {
|
|
122
126
|
setMarkdownHeaders(event, path, config, resolveUrl);
|
|
123
|
-
return notFoundMarkdown(canonicalUrl, path, config);
|
|
127
|
+
return notFoundMarkdown(canonicalUrl, path, config, resolveUrl);
|
|
124
128
|
}
|
|
125
129
|
const html = await response.text();
|
|
126
130
|
logger.debug(`[markdown] Fetched HTML for ${path} (${html.length} bytes)`);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createError, defineEventHandler } from "h3";
|
|
2
2
|
import { useRuntimeConfig } from "nitropack/runtime";
|
|
3
3
|
import { withSiteUrl } from "#site-config/server/composables/utils";
|
|
4
|
+
import { toDeployedRoute } from "../../route-path.js";
|
|
4
5
|
import { logger } from "../logger.js";
|
|
5
6
|
import { convertHtmlToMarkdown, extractLastUpdated, getMarkdownRenderInfo } from "../utils.js";
|
|
6
7
|
import { tryGetContentMarkdown } from "../utils/content.js";
|
|
@@ -26,8 +27,10 @@ export default defineEventHandler(async (event) => {
|
|
|
26
27
|
if (!renderInfo || "notAcceptable" in renderInfo)
|
|
27
28
|
return;
|
|
28
29
|
const { path } = renderInfo;
|
|
29
|
-
const
|
|
30
|
-
const
|
|
30
|
+
const fullRuntimeConfig = useRuntimeConfig(event);
|
|
31
|
+
const runtimeConfig = fullRuntimeConfig["nuxt-ai-ready"];
|
|
32
|
+
const deployedPath = toDeployedRoute(path, fullRuntimeConfig.app.baseURL);
|
|
33
|
+
const canonicalUrl = withSiteUrl(event, deployedPath);
|
|
31
34
|
const contentPage = await tryGetContentMarkdown(event, path).catch(() => null);
|
|
32
35
|
if (contentPage) {
|
|
33
36
|
logger.debug(`[markdown.prerender] Using content source for ${path} (${contentPage.markdown.length} bytes)`);
|
|
@@ -56,7 +59,7 @@ ${contentPage.markdown}`;
|
|
|
56
59
|
logger.debug(`[markdown.prerender] Reusing prerendered HTML for ${path} (${html.length} bytes)`);
|
|
57
60
|
} else {
|
|
58
61
|
logger.debug(`[markdown.prerender] Fetching HTML for ${path}`);
|
|
59
|
-
const response = await event.fetch(
|
|
62
|
+
const response = await event.fetch(deployedPath, { signal: AbortSignal.timeout(3e4) }).catch((err) => {
|
|
60
63
|
if (err?.name === "TimeoutError" || err?.name === "AbortError") {
|
|
61
64
|
throw createError({
|
|
62
65
|
statusCode: 504,
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { eventHandler, sendIterable, setHeader, setResponseHeader } from "h3";
|
|
2
2
|
import { useRuntimeConfig } from "nitropack/runtime";
|
|
3
3
|
import { getSiteConfig } from "#site-config/server/composables";
|
|
4
|
+
import { withSiteUrl } from "#site-config/server/composables/utils";
|
|
5
|
+
import { toDeployedRoute } from "../../route-path.js";
|
|
4
6
|
import { countPages, streamPages } from "../db/queries.js";
|
|
5
7
|
import { buildLlmsFullTxtHeader, formatPageForLlmsFullTxt } from "../utils/llms-full.js";
|
|
6
8
|
export default eventHandler(async (event) => {
|
|
@@ -8,12 +10,15 @@ export default eventHandler(async (event) => {
|
|
|
8
10
|
setHeader(event, "Content-Type", "text/plain; charset=utf-8");
|
|
9
11
|
return "# llms-full.txt\n\nThis file is generated during prerender.";
|
|
10
12
|
}
|
|
11
|
-
const
|
|
13
|
+
const runtimeConfig = useRuntimeConfig(event);
|
|
14
|
+
const config = runtimeConfig["nuxt-ai-ready"];
|
|
12
15
|
const siteConfig = getSiteConfig(event);
|
|
16
|
+
const baseURL = runtimeConfig.app.baseURL;
|
|
17
|
+
const canonicalSiteUrl = siteConfig.url ? withSiteUrl(event, toDeployedRoute("/", baseURL)) : void 0;
|
|
13
18
|
const header = buildLlmsFullTxtHeader(
|
|
14
19
|
{
|
|
15
20
|
name: siteConfig.name,
|
|
16
|
-
url:
|
|
21
|
+
url: canonicalSiteUrl,
|
|
17
22
|
description: siteConfig.description
|
|
18
23
|
},
|
|
19
24
|
config.llmsTxt
|
|
@@ -17,5 +17,5 @@ export declare function hasAssets(event?: H3Event): boolean;
|
|
|
17
17
|
* Falls back to $fetch with timeout to avoid self-fetch hangs on CF Workers.
|
|
18
18
|
*/
|
|
19
19
|
export declare function fetchPublicAsset<T = unknown>(event: H3Event | undefined, path: string, options?: {
|
|
20
|
-
responseType?: 'json' | 'text' | 'arrayBuffer';
|
|
20
|
+
responseType?: 'json' | 'text' | 'arrayBuffer' | 'stream';
|
|
21
21
|
}): Promise<T | null>;
|
|
@@ -19,6 +19,8 @@ export async function fetchPublicAsset(event, path, options) {
|
|
|
19
19
|
return response.text().catch(() => null);
|
|
20
20
|
if (responseType === "arrayBuffer")
|
|
21
21
|
return response.arrayBuffer().catch(() => null);
|
|
22
|
+
if (responseType === "stream")
|
|
23
|
+
return response.body;
|
|
22
24
|
}
|
|
23
25
|
return null;
|
|
24
26
|
}
|
|
@@ -27,6 +29,6 @@ export async function fetchPublicAsset(event, path, options) {
|
|
|
27
29
|
return globalThis.$fetch(path, {
|
|
28
30
|
baseURL: "/",
|
|
29
31
|
signal: controller.signal,
|
|
30
|
-
responseType: responseType === "arrayBuffer"
|
|
32
|
+
responseType: responseType === "arrayBuffer" || responseType === "stream" ? responseType : void 0
|
|
31
33
|
}).catch(() => null).finally(() => clearTimeout(timeout));
|
|
32
34
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { joinURL } from "ufo";
|
|
1
2
|
export const INDEXNOW_HOSTS = ["api.indexnow.org", "www.bing.com"];
|
|
2
3
|
export function getIndexNowEndpoints() {
|
|
3
4
|
const testEndpoint = process.env.INDEXNOW_TEST_ENDPOINT;
|
|
@@ -8,12 +9,12 @@ export function getIndexNowEndpoints() {
|
|
|
8
9
|
}
|
|
9
10
|
export function buildIndexNowBody(routes, key, siteUrl) {
|
|
10
11
|
const urlList = routes.map(
|
|
11
|
-
(route) => route.startsWith("http") ? route :
|
|
12
|
+
(route) => route.startsWith("http") ? route : joinURL(siteUrl, route)
|
|
12
13
|
);
|
|
13
14
|
return {
|
|
14
15
|
host: new URL(siteUrl).host,
|
|
15
16
|
key,
|
|
16
|
-
keyLocation: `${
|
|
17
|
+
keyLocation: joinURL(siteUrl, `${key}.txt`),
|
|
17
18
|
urlList
|
|
18
19
|
};
|
|
19
20
|
}
|
|
@@ -15,9 +15,11 @@ function resolveHeaderUrl(path, resolveUrl) {
|
|
|
15
15
|
export function buildLinkHeader(path, variant, config, resolveUrl) {
|
|
16
16
|
const parts = [];
|
|
17
17
|
if (variant === "html") {
|
|
18
|
-
|
|
18
|
+
const href = resolveHeaderUrl(toMarkdownPath(path), resolveUrl);
|
|
19
|
+
parts.push(`<${encodePathForHeader(href)}>; rel="alternate"; type="text/markdown"`);
|
|
19
20
|
} else {
|
|
20
|
-
|
|
21
|
+
const href = resolveHeaderUrl(path, resolveUrl);
|
|
22
|
+
parts.push(`<${encodePathForHeader(href)}>; rel="alternate"; type="text/html"`);
|
|
21
23
|
}
|
|
22
24
|
if (config.i18n) {
|
|
23
25
|
const alternates = computeLocaleAlternates(path, config.i18n);
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
+
import { joinURL } from "ufo";
|
|
1
2
|
import { normalizeLlmsTxtConfig } from "../../llms-txt-format.js";
|
|
2
|
-
const RE_TRAILING_SLASH = /\/$/;
|
|
3
3
|
const RE_FRONTMATTER = /^---\n[\s\S]*?\n---\n*/;
|
|
4
4
|
const RE_HEADING = /^(#{1,6}) ([^\n]+)$/gm;
|
|
5
5
|
export function formatPageForLlmsFullTxt(route, title, description, markdown, siteUrl) {
|
|
6
|
-
const canonicalUrl = siteUrl ?
|
|
6
|
+
const canonicalUrl = siteUrl ? joinURL(siteUrl, route) : route;
|
|
7
7
|
const heading = title && title !== route ? `### ${title}` : `### ${route}`;
|
|
8
8
|
const content = markdown.replace(RE_FRONTMATTER, "").replace(RE_HEADING, (_, hashes, text) => `h${hashes.length}. ${text}`);
|
|
9
9
|
const parts = [heading, ""];
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { parseSitemapStream } from "@nuxtjs/sitemap/utils";
|
|
2
2
|
import { useRuntimeConfig } from "nitropack/runtime";
|
|
3
3
|
import { withLeadingSlash } from "ufo";
|
|
4
4
|
import { logger } from "../logger.js";
|
|
@@ -26,26 +26,26 @@ export function hasMultipleSitemaps(event) {
|
|
|
26
26
|
const sitemaps = getSitemapsFromConfig(event);
|
|
27
27
|
return sitemaps.length > 1;
|
|
28
28
|
}
|
|
29
|
-
function
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
29
|
+
function normalizeUrl(entry) {
|
|
30
|
+
if (typeof entry === "string")
|
|
31
|
+
return { loc: entry };
|
|
32
|
+
return {
|
|
33
|
+
loc: entry.loc,
|
|
34
|
+
lastmod: entry.lastmod instanceof Date ? entry.lastmod.toISOString() : entry.lastmod
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
function isSitemapXmlInput(value) {
|
|
38
|
+
return typeof value === "string" || value instanceof Uint8Array || typeof value === "object" && value !== null && "getReader" in value;
|
|
39
39
|
}
|
|
40
40
|
export async function fetchSitemapByRoute(event, route, depth = 0) {
|
|
41
41
|
const config = useRuntimeConfig(event)["nuxt-ai-ready"];
|
|
42
42
|
const fetchRoute = withLeadingSlash(route);
|
|
43
43
|
const usePublicAsset = config.sitemapPrerendered && hasAssets(event);
|
|
44
44
|
logger.debug(`[sitemap] Fetching ${fetchRoute} via ${usePublicAsset ? "ASSETS.fetch" : event ? "event.$fetch" : "globalThis.$fetch"}`);
|
|
45
|
-
let
|
|
45
|
+
let sitemapInput = null;
|
|
46
46
|
if (usePublicAsset) {
|
|
47
|
-
|
|
48
|
-
if (!
|
|
47
|
+
sitemapInput = await fetchPublicAsset(event, fetchRoute, { responseType: "stream" });
|
|
48
|
+
if (!sitemapInput) {
|
|
49
49
|
logger.warn(`[sitemap] Not found in ASSETS: ${fetchRoute}`);
|
|
50
50
|
return { urls: [], error: "Not found in ASSETS" };
|
|
51
51
|
}
|
|
@@ -53,38 +53,53 @@ export async function fetchSitemapByRoute(event, route, depth = 0) {
|
|
|
53
53
|
try {
|
|
54
54
|
const $fetch = event?.$fetch ?? globalThis.$fetch;
|
|
55
55
|
const res = await $fetch(fetchRoute, {
|
|
56
|
-
responseType: "
|
|
56
|
+
responseType: "stream",
|
|
57
57
|
timeout: FETCH_TIMEOUT
|
|
58
58
|
});
|
|
59
|
-
if (!res
|
|
59
|
+
if (!res) {
|
|
60
60
|
logger.warn(`[sitemap] Empty response from ${fetchRoute}`);
|
|
61
61
|
return { urls: [], error: "Empty response" };
|
|
62
62
|
}
|
|
63
|
-
|
|
63
|
+
if (!isSitemapXmlInput(res)) {
|
|
64
|
+
logger.warn(`[sitemap] Invalid response body from ${fetchRoute}`);
|
|
65
|
+
return { urls: [], error: "Invalid response body" };
|
|
66
|
+
}
|
|
67
|
+
sitemapInput = res;
|
|
64
68
|
} catch (e) {
|
|
65
69
|
const msg = e instanceof Error ? e.message : String(e);
|
|
66
70
|
logger.warn(`[sitemap] Failed to fetch ${fetchRoute}: ${msg}`);
|
|
67
71
|
return { urls: [], error: msg };
|
|
68
72
|
}
|
|
69
73
|
}
|
|
70
|
-
logger.debug(`[sitemap] Parsing sitemap XML
|
|
71
|
-
|
|
74
|
+
logger.debug(`[sitemap] Parsing sitemap XML stream`);
|
|
75
|
+
const urls = [];
|
|
76
|
+
const indexEntries = [];
|
|
77
|
+
let kind;
|
|
78
|
+
try {
|
|
79
|
+
for await (const parsed of parseSitemapStream(sitemapInput)) {
|
|
80
|
+
if (parsed._tag === "kind")
|
|
81
|
+
kind = parsed.kind;
|
|
82
|
+
else if (parsed._tag === "url")
|
|
83
|
+
urls.push(normalizeUrl(parsed.url));
|
|
84
|
+
else if (parsed._tag === "sitemap")
|
|
85
|
+
indexEntries.push(parsed.sitemap);
|
|
86
|
+
else
|
|
87
|
+
logger.warn(`[sitemap] ${fetchRoute}: ${parsed.warning.message}`);
|
|
88
|
+
}
|
|
89
|
+
} catch (e) {
|
|
90
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
91
|
+
logger.warn(`[sitemap] Failed to parse ${fetchRoute}: ${msg}`);
|
|
92
|
+
return { urls: [], error: msg };
|
|
93
|
+
}
|
|
94
|
+
if (kind === "index") {
|
|
72
95
|
if (depth >= 3) {
|
|
73
96
|
logger.warn(`[sitemap] Sitemap index nesting too deep at ${fetchRoute}, stopping`);
|
|
74
97
|
return { urls: [] };
|
|
75
98
|
}
|
|
76
|
-
|
|
77
|
-
try {
|
|
78
|
-
index = await parseSitemapIndex(sitemapXml);
|
|
79
|
-
} catch (e) {
|
|
80
|
-
const msg = e instanceof Error ? e.message : String(e);
|
|
81
|
-
logger.warn(`[sitemap] Failed to parse sitemap index ${fetchRoute}: ${msg}`);
|
|
82
|
-
return { urls: [], error: msg };
|
|
83
|
-
}
|
|
84
|
-
logger.debug(`[sitemap] ${fetchRoute} is a sitemap index with ${index.entries.length} children`);
|
|
99
|
+
logger.debug(`[sitemap] ${fetchRoute} is a sitemap index with ${indexEntries.length} children`);
|
|
85
100
|
const allUrls = [];
|
|
86
101
|
const childErrors = [];
|
|
87
|
-
for (const entry of
|
|
102
|
+
for (const entry of indexEntries) {
|
|
88
103
|
const childRoute = entry.loc.startsWith("http") ? new URL(entry.loc).pathname : entry.loc;
|
|
89
104
|
if (withLeadingSlash(childRoute) === fetchRoute)
|
|
90
105
|
continue;
|
|
@@ -94,21 +109,12 @@ export async function fetchSitemapByRoute(event, route, depth = 0) {
|
|
|
94
109
|
childErrors.push(`${withLeadingSlash(childRoute)}: ${error}`);
|
|
95
110
|
}
|
|
96
111
|
if (childErrors.length > 0) {
|
|
97
|
-
const msg = `${childErrors.length}/${
|
|
112
|
+
const msg = `${childErrors.length}/${indexEntries.length} child sitemaps failed (${childErrors.join("; ")})`;
|
|
98
113
|
logger.warn(`[sitemap] Sitemap index ${fetchRoute}: ${msg}`);
|
|
99
114
|
return { urls: allUrls, error: msg };
|
|
100
115
|
}
|
|
101
116
|
return { urls: allUrls };
|
|
102
117
|
}
|
|
103
|
-
let result;
|
|
104
|
-
try {
|
|
105
|
-
result = await parseSitemapXml(sitemapXml);
|
|
106
|
-
} catch (e) {
|
|
107
|
-
const msg = e instanceof Error ? e.message : String(e);
|
|
108
|
-
logger.warn(`[sitemap] Failed to parse ${fetchRoute}: ${msg}`);
|
|
109
|
-
return { urls: [], error: msg };
|
|
110
|
-
}
|
|
111
|
-
const urls = normalizeUrls(result?.urls || []);
|
|
112
118
|
logger.debug(`[sitemap] Found ${urls.length} URLs in ${fetchRoute}`);
|
|
113
119
|
return { urls };
|
|
114
120
|
}
|
package/dist/runtime/types.d.ts
CHANGED
|
@@ -260,11 +260,11 @@ export interface LlmsTxtLink {
|
|
|
260
260
|
export interface LlmsTxtSection {
|
|
261
261
|
/** The title of the section */
|
|
262
262
|
title: string;
|
|
263
|
-
/**
|
|
263
|
+
/** Description rendered in the heading-free preamble, or in each optional link note (can be array for multiple paragraphs) */
|
|
264
264
|
description?: string | string[];
|
|
265
265
|
/** The links of the section */
|
|
266
266
|
links?: LlmsTxtLink[];
|
|
267
|
-
/** Mark
|
|
267
|
+
/** Mark links as optional per llms.txt spec. Links from all optional sections are flattened under `## Optional`. */
|
|
268
268
|
optional?: boolean;
|
|
269
269
|
}
|
|
270
270
|
/**
|
|
@@ -273,7 +273,7 @@ export interface LlmsTxtSection {
|
|
|
273
273
|
export interface LlmsTxtConfig {
|
|
274
274
|
/** The sections of the documentation */
|
|
275
275
|
sections?: LlmsTxtSection[];
|
|
276
|
-
/**
|
|
276
|
+
/** Additional heading-free preamble notes rendered before file-list sections */
|
|
277
277
|
notes?: string | string[];
|
|
278
278
|
}
|
|
279
279
|
/**
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nuxt-ai-ready",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "1.5.
|
|
4
|
+
"version": "1.5.7",
|
|
5
5
|
"description": "Best practice AI & LLM discoverability for Nuxt sites.",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "Harlan Wilton",
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
"peerDependencies": {
|
|
43
43
|
"@libsql/client": "^0.14.0",
|
|
44
44
|
"@neondatabase/serverless": "^1.0.0",
|
|
45
|
-
"@nuxtjs/sitemap": ">=
|
|
45
|
+
"@nuxtjs/sitemap": ">=8.3.0",
|
|
46
46
|
"better-sqlite3": "^11.0.0 || ^12.0.0"
|
|
47
47
|
},
|
|
48
48
|
"peerDependenciesMeta": {
|
|
@@ -57,18 +57,18 @@
|
|
|
57
57
|
}
|
|
58
58
|
},
|
|
59
59
|
"dependencies": {
|
|
60
|
-
"@mdream/js": "^1.5.
|
|
60
|
+
"@mdream/js": "^1.5.3",
|
|
61
61
|
"@nuxt/kit": "^4.5.0",
|
|
62
62
|
"citty": "^0.2.2",
|
|
63
63
|
"consola": "^3.4.2",
|
|
64
64
|
"defu": "^6.1.7",
|
|
65
65
|
"drizzle-orm": "^0.45.2",
|
|
66
|
-
"mdream": "^1.5.
|
|
67
|
-
"nuxt-site-config": "^4.1.
|
|
68
|
-
"nuxtseo-shared": "^5.3.
|
|
66
|
+
"mdream": "^1.5.3",
|
|
67
|
+
"nuxt-site-config": "^4.1.2",
|
|
68
|
+
"nuxtseo-shared": "^5.3.3",
|
|
69
69
|
"pathe": "^2.0.3",
|
|
70
70
|
"pkg-types": "^2.3.1",
|
|
71
|
-
"site-config-stack": "^4.1.
|
|
71
|
+
"site-config-stack": "^4.1.2",
|
|
72
72
|
"ufo": "^1.6.4",
|
|
73
73
|
"uncrypto": "^0.1.3"
|
|
74
74
|
},
|
|
@@ -81,8 +81,8 @@
|
|
|
81
81
|
"@nuxt/test-utils": "^4.0.3",
|
|
82
82
|
"@nuxtjs/eslint-config-typescript": "^12.1.0",
|
|
83
83
|
"@nuxtjs/mcp-toolkit": "^0.18.0",
|
|
84
|
-
"@nuxtjs/robots": "^6.1.
|
|
85
|
-
"@nuxtjs/sitemap": "^8.
|
|
84
|
+
"@nuxtjs/robots": "^6.1.3",
|
|
85
|
+
"@nuxtjs/sitemap": "^8.3.0",
|
|
86
86
|
"@types/better-sqlite3": "^7.6.13",
|
|
87
87
|
"@vitest/coverage-v8": "^4.1.10",
|
|
88
88
|
"@vue/test-utils": "^2.4.11",
|
|
@@ -92,15 +92,15 @@
|
|
|
92
92
|
"eslint": "^10.7.0",
|
|
93
93
|
"eslint-plugin-harlanzw": "^0.17.0",
|
|
94
94
|
"execa": "^10.0.0",
|
|
95
|
-
"happy-dom": "^20.
|
|
95
|
+
"happy-dom": "^20.11.0",
|
|
96
96
|
"nitropack": "^2.13.4",
|
|
97
97
|
"nuxt": "^4.5.0",
|
|
98
|
-
"nuxt-site-config": "^4.1.
|
|
99
|
-
"nuxtseo-layer-devtools": "^5.3.
|
|
98
|
+
"nuxt-site-config": "^4.1.2",
|
|
99
|
+
"nuxtseo-layer-devtools": "^5.3.3",
|
|
100
100
|
"playwright": "^1.61.1",
|
|
101
101
|
"playwright-core": "^1.61.1",
|
|
102
102
|
"tinyglobby": "^0.2.17",
|
|
103
|
-
"typescript": "
|
|
103
|
+
"typescript": "6.0.3",
|
|
104
104
|
"unbuild": "^3.6.1",
|
|
105
105
|
"vitest": "^4.1.10",
|
|
106
106
|
"vue": "^3.5.40",
|