nuxt-ai-ready 1.5.6 → 1.5.8
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 +84 -7
- 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 +35 -20
- package/dist/runtime/server/utils/cloudflare.d.ts +1 -1
- package/dist/runtime/server/utils/cloudflare.js +3 -1
- package/dist/runtime/server/utils/sitemap.js +45 -39
- package/dist/runtime/types.d.ts +10 -3
- package/package.json +6 -6
package/dist/module.json
CHANGED
package/dist/module.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createHash, randomBytes } from 'node:crypto';
|
|
2
|
-
import { mkdir, writeFile, appendFile, stat, access, readFile } from 'node:fs/promises';
|
|
3
|
-
import { join, dirname } from 'node:path';
|
|
4
|
-
import { useLogger, useNuxt, addTypeTemplate, addTemplate, defineNuxtModule, createResolver, hasNuxtModule, addServerPlugin, addServerHandler, addPlugin } from '@nuxt/kit';
|
|
2
|
+
import { mkdir, writeFile, appendFile, stat, readdir, access, readFile } from 'node:fs/promises';
|
|
3
|
+
import { join, dirname, relative, resolve } from 'node:path';
|
|
4
|
+
import { useLogger, useNuxt, resolveFiles, addTypeTemplate, addTemplate, defineNuxtModule, createResolver, hasNuxtModule, addServerPlugin, addServerHandler, addPlugin } from '@nuxt/kit';
|
|
5
5
|
import defu from 'defu';
|
|
6
6
|
import { installNuxtSiteConfig, useSiteConfig, withSiteUrl } from 'nuxt-site-config/kit';
|
|
7
7
|
import { setupDevToolsUI } from 'nuxtseo-shared/devtools';
|
|
@@ -9,7 +9,7 @@ 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 { withLeadingSlash, withBase } from 'ufo';
|
|
12
|
+
import { withLeadingSlash, withBase, joinURL } from 'ufo';
|
|
13
13
|
import { normalizePagePath, toMarkdownPath } from '../dist/runtime/markdown-path.js';
|
|
14
14
|
import { toLogicalRoute, toDeployedRoute } from '../dist/runtime/route-path.js';
|
|
15
15
|
import { initSchema, computeContentHash, insertPage, queryAllPages, exportDbDump } from '../dist/runtime/server/db/shared.js';
|
|
@@ -25,6 +25,41 @@ const PRERENDER_PAGE_TIMEOUT = 3e4;
|
|
|
25
25
|
const RE_HTML_MD_EXT = /\.(html|md)$/;
|
|
26
26
|
const RE_INDEX_SUFFIX = /\/index$/;
|
|
27
27
|
const RE_MD_EXT = /\.md$/;
|
|
28
|
+
const RE_NEGATED_GLOB = /^(!?)(.*)$/;
|
|
29
|
+
const RE_PARENT_DIR_GLOB = /!?\.\.\//;
|
|
30
|
+
const MARKDOWN_LINK_AVAILABILITY_FILE = "markdown-link-availability.json";
|
|
31
|
+
async function findOutputMarkdownPaths(publicDir, baseURL, srcDir, ignoredPaths, publicAssets) {
|
|
32
|
+
const paths = /* @__PURE__ */ new Set();
|
|
33
|
+
async function visit(directory, urlBase, relativeDirectory = "") {
|
|
34
|
+
const entries = await readdir(directory, { withFileTypes: true });
|
|
35
|
+
await Promise.all(entries.map(async (entry) => {
|
|
36
|
+
const relativePath = join(relativeDirectory, entry.name);
|
|
37
|
+
if (entry.isDirectory()) {
|
|
38
|
+
await visit(join(directory, entry.name), urlBase, relativePath);
|
|
39
|
+
} else if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
40
|
+
const urlPath = joinURL(urlBase, relativePath.replaceAll("\\", "/"));
|
|
41
|
+
paths.add(joinURL(baseURL, urlPath));
|
|
42
|
+
}
|
|
43
|
+
}));
|
|
44
|
+
}
|
|
45
|
+
await visit(publicDir, "/");
|
|
46
|
+
for (const asset of publicAssets) {
|
|
47
|
+
const patterns = [
|
|
48
|
+
"**/*.md",
|
|
49
|
+
...ignoredPaths.map((ignoredPath) => {
|
|
50
|
+
const [, negated = "", pattern = ""] = ignoredPath.match(RE_NEGATED_GLOB) || [];
|
|
51
|
+
const assetPattern = pattern.startsWith("*") ? pattern : relative(asset.dir, resolve(srcDir, pattern)).replaceAll("\\", "/");
|
|
52
|
+
return `${negated ? "" : "!"}${assetPattern}`;
|
|
53
|
+
}).filter((pattern) => !RE_PARENT_DIR_GLOB.test(pattern))
|
|
54
|
+
];
|
|
55
|
+
const files = await resolveFiles(asset.dir, patterns);
|
|
56
|
+
for (const file of files) {
|
|
57
|
+
const urlPath = joinURL(asset.baseURL || "/", relative(asset.dir, file).replaceAll("\\", "/"));
|
|
58
|
+
paths.add(joinURL(baseURL, urlPath));
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return [...paths].sort();
|
|
62
|
+
}
|
|
28
63
|
async function fetchPreviousMeta(siteUrl, indexNow) {
|
|
29
64
|
const metaUrl = withBase("/__ai-ready/pages.meta.json", siteUrl);
|
|
30
65
|
logger.info(`Fetching previous build meta from ${metaUrl}`);
|
|
@@ -399,6 +434,20 @@ function setupPrerenderHandler(options, dbPath, siteInfo, llmsTxtConfig, indexNo
|
|
|
399
434
|
await submitIndexNow(changed, added, state.siteInfo.url, state.indexNow);
|
|
400
435
|
}
|
|
401
436
|
}
|
|
437
|
+
if (state.dbPath && state.llmsTxtConfig?.markdownLinks) {
|
|
438
|
+
const availabilityFile = join(dirname(state.dbPath), MARKDOWN_LINK_AVAILABILITY_FILE);
|
|
439
|
+
const paths = await findOutputMarkdownPaths(
|
|
440
|
+
nitro.options.output.publicDir,
|
|
441
|
+
nitro.options.baseURL,
|
|
442
|
+
nitro.options.srcDir,
|
|
443
|
+
nitro.options.ignore,
|
|
444
|
+
nitro.options.publicAssets
|
|
445
|
+
);
|
|
446
|
+
await writeFile(availabilityFile, JSON.stringify({
|
|
447
|
+
runtimeMarkdownAvailable: !nitro.options.static,
|
|
448
|
+
paths
|
|
449
|
+
}), "utf-8");
|
|
450
|
+
}
|
|
402
451
|
const llmsStats = await prerenderRoute(nitro, "/llms.txt");
|
|
403
452
|
const llmsFullStats = await stat(state.llmsFullTxtPath);
|
|
404
453
|
nitro._prerenderedRoutes.push({ route: "/llms-full.txt", fileName: "/llms-full.txt" });
|
|
@@ -464,6 +513,10 @@ export {}
|
|
|
464
513
|
addTemplate({
|
|
465
514
|
filename: "types/ai-ready-virtual.d.ts",
|
|
466
515
|
getContents: () => `declare module '#ai-ready-virtual/read-page-data.mjs' {
|
|
516
|
+
export function readMarkdownLinkAvailabilityFromFilesystem(): Promise<{
|
|
517
|
+
runtimeMarkdownAvailable: boolean
|
|
518
|
+
paths: string[]
|
|
519
|
+
}>
|
|
467
520
|
export function readPageDataFromFilesystem(): Promise<{
|
|
468
521
|
pages: Array<{
|
|
469
522
|
route: string
|
|
@@ -616,7 +669,7 @@ const module$1 = defineNuxtModule({
|
|
|
616
669
|
version: ">=6.0.0"
|
|
617
670
|
},
|
|
618
671
|
"@nuxtjs/sitemap": {
|
|
619
|
-
version: ">=8.
|
|
672
|
+
version: ">=8.3.0"
|
|
620
673
|
},
|
|
621
674
|
"nuxt-site-config": {
|
|
622
675
|
version: ">=3.2"
|
|
@@ -765,12 +818,13 @@ const module$1 = defineNuxtModule({
|
|
|
765
818
|
}
|
|
766
819
|
}
|
|
767
820
|
const mergedLlmsTxt = config.llmsTxt ? {
|
|
821
|
+
markdownLinks: config.llmsTxt.markdownLinks ?? false,
|
|
768
822
|
sections: [
|
|
769
823
|
...defaultLlmsTxtSections,
|
|
770
824
|
...config.llmsTxt.sections || []
|
|
771
825
|
],
|
|
772
826
|
notes: config.llmsTxt.notes
|
|
773
|
-
} : { sections: defaultLlmsTxtSections };
|
|
827
|
+
} : { markdownLinks: false, sections: defaultLlmsTxtSections };
|
|
774
828
|
const llmsTxtPayload = {
|
|
775
829
|
sections: mergedLlmsTxt.sections || [],
|
|
776
830
|
notes: typeof mergedLlmsTxt.notes === "string" ? [mergedLlmsTxt.notes] : mergedLlmsTxt.notes || []
|
|
@@ -859,7 +913,30 @@ const module$1 = defineNuxtModule({
|
|
|
859
913
|
}
|
|
860
914
|
}
|
|
861
915
|
nitroConfig.virtual = nitroConfig.virtual || {};
|
|
862
|
-
|
|
916
|
+
const markdownLinkAvailabilityPath = join(dirname(buildDbPath), MARKDOWN_LINK_AVAILABILITY_FILE);
|
|
917
|
+
nitroConfig.virtual["#ai-ready-virtual/read-page-data.mjs"] = nuxt.options.dev ? `
|
|
918
|
+
export async function readPageDataFromFilesystem() { return { pages: [], errorRoutes: [] } }
|
|
919
|
+
export async function readMarkdownLinkAvailabilityFromFilesystem() { return { runtimeMarkdownAvailable: false, paths: [] } }
|
|
920
|
+
` : `
|
|
921
|
+
export async function readMarkdownLinkAvailabilityFromFilesystem() {
|
|
922
|
+
if (!import.meta.prerender) {
|
|
923
|
+
return { runtimeMarkdownAvailable: false, paths: [] }
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
const { readFile } = await import('node:fs/promises')
|
|
927
|
+
try {
|
|
928
|
+
const availability = JSON.parse(await readFile(${JSON.stringify(markdownLinkAvailabilityPath)}, 'utf8'))
|
|
929
|
+
return {
|
|
930
|
+
runtimeMarkdownAvailable: availability?.runtimeMarkdownAvailable === true,
|
|
931
|
+
paths: Array.isArray(availability?.paths) ? availability.paths.filter(path => typeof path === 'string') : [],
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
catch (error) {
|
|
935
|
+
console.warn('[nuxt-ai-ready] Failed to read Markdown link availability; keeping canonical links.', error)
|
|
936
|
+
return { runtimeMarkdownAvailable: false, paths: [] }
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
|
|
863
940
|
export async function readPageDataFromFilesystem() {
|
|
864
941
|
if (!import.meta.prerender) {
|
|
865
942
|
return { pages: [], errorRoutes: [] }
|
|
@@ -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,24 +1,32 @@
|
|
|
1
1
|
import { useRuntimeConfig } from "nitropack/runtime";
|
|
2
|
+
import { decodePath } from "ufo";
|
|
2
3
|
import { getSiteConfig } from "#site-config/server/composables/getSiteConfig";
|
|
3
4
|
import { withSiteTrailingSlash, withSiteUrl } from "#site-config/server/composables/utils";
|
|
4
|
-
import { normalizeLlmsTxtConfig } from "./llms-txt-format.js";
|
|
5
|
+
import { formatAvailableLanguagesSection, formatLlmsTxtPageLink, normalizeLlmsTxtConfig } from "./llms-txt-format.js";
|
|
6
|
+
import { toMarkdownPath } from "./markdown-path.js";
|
|
5
7
|
import { normalizePersistedRoute, toDeployedRoute, toLogicalRoute } from "./route-path.js";
|
|
6
8
|
import { queryPages } from "./server/db/queries.js";
|
|
7
9
|
import { logger } from "./server/logger.js";
|
|
8
|
-
import {
|
|
10
|
+
import { resolveLocaleFromRoute } from "./server/utils/i18n.js";
|
|
9
11
|
import { fetchSitemapUrls } from "./server/utils/sitemap.js";
|
|
10
12
|
export { normalizeLlmsTxtConfig };
|
|
11
|
-
function
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
13
|
+
function hasRuntimeMarkdownHandler(pathname) {
|
|
14
|
+
const lastSegment = pathname.split("/").pop() || "";
|
|
15
|
+
return !pathname.startsWith("/api") && !pathname.startsWith("/_") && !pathname.startsWith("/@") && !lastSegment.includes(".");
|
|
16
|
+
}
|
|
17
|
+
async function findMarkdownLinkAvailability() {
|
|
18
|
+
if (!import.meta.prerender) {
|
|
19
|
+
return {
|
|
20
|
+
runtimeMarkdownAvailable: true,
|
|
21
|
+
paths: /* @__PURE__ */ new Set()
|
|
22
|
+
};
|
|
20
23
|
}
|
|
21
|
-
|
|
24
|
+
const { readMarkdownLinkAvailabilityFromFilesystem } = await import("#ai-ready-virtual/read-page-data.mjs");
|
|
25
|
+
const availability = await readMarkdownLinkAvailabilityFromFilesystem();
|
|
26
|
+
return {
|
|
27
|
+
runtimeMarkdownAvailable: availability.runtimeMarkdownAvailable,
|
|
28
|
+
paths: new Set(availability.paths.map(decodePath))
|
|
29
|
+
};
|
|
22
30
|
}
|
|
23
31
|
function getGroupPrefix(url, depth) {
|
|
24
32
|
const segments = url.split("/").filter(Boolean);
|
|
@@ -103,12 +111,7 @@ function formatPagesWithGroups(pages) {
|
|
|
103
111
|
urlsInCurrentGroup = 0;
|
|
104
112
|
}
|
|
105
113
|
urlsInCurrentGroup++;
|
|
106
|
-
|
|
107
|
-
const href = page.href ?? page.pathname;
|
|
108
|
-
if (page.title && page.title !== page.pathname)
|
|
109
|
-
lines.push(`- [${page.title}](${href})${descText}`);
|
|
110
|
-
else
|
|
111
|
-
lines.push(`- ${href}${descText}`);
|
|
114
|
+
lines.push(formatLlmsTxtPageLink(page));
|
|
112
115
|
}
|
|
113
116
|
return lines;
|
|
114
117
|
}
|
|
@@ -194,8 +197,20 @@ Canonical Origin: ${canonicalSiteUrl}`);
|
|
|
194
197
|
seenPaths.add(pathname);
|
|
195
198
|
}
|
|
196
199
|
}
|
|
200
|
+
let resolvePageHref = resolvePath;
|
|
201
|
+
if (llmsTxtConfig.markdownLinks) {
|
|
202
|
+
const markdownLinkAvailability = await findMarkdownLinkAvailability();
|
|
203
|
+
resolvePageHref = (pathname) => {
|
|
204
|
+
const deployedPathname = resolvePath(pathname);
|
|
205
|
+
const markdownPath = resolvePath(toMarkdownPath(pathname));
|
|
206
|
+
if (markdownLinkAvailability.paths.has(decodePath(markdownPath)) || markdownLinkAvailability.runtimeMarkdownAvailable && hasRuntimeMarkdownHandler(pathname)) {
|
|
207
|
+
return markdownPath;
|
|
208
|
+
}
|
|
209
|
+
return deployedPathname;
|
|
210
|
+
};
|
|
211
|
+
}
|
|
197
212
|
for (const page of [...prerendered, ...other])
|
|
198
|
-
page.href =
|
|
213
|
+
page.href = resolvePageHref(page.pathname);
|
|
199
214
|
if (i18n) {
|
|
200
215
|
const pageCounts = /* @__PURE__ */ new Map();
|
|
201
216
|
for (const locale of i18n.locales) pageCounts.set(locale.code, 0);
|
|
@@ -203,7 +218,7 @@ Canonical Origin: ${canonicalSiteUrl}`);
|
|
|
203
218
|
const code = p.locale || resolveLocaleFromRoute(p.pathname, i18n).locale;
|
|
204
219
|
pageCounts.set(code, (pageCounts.get(code) ?? 0) + 1);
|
|
205
220
|
}
|
|
206
|
-
parts.push(...formatAvailableLanguagesSection(i18n, pageCounts,
|
|
221
|
+
parts.push(...formatAvailableLanguagesSection(i18n, pageCounts, resolvePageHref));
|
|
207
222
|
parts.push("");
|
|
208
223
|
}
|
|
209
224
|
const isDefaultLocale = (item) => {
|
|
@@ -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,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,20 +260,27 @@ 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
|
/**
|
|
271
271
|
* Structured llms.txt configuration
|
|
272
272
|
*/
|
|
273
273
|
export interface LlmsTxtConfig {
|
|
274
|
+
/**
|
|
275
|
+
* Rewrite automatically generated page links to their Markdown representation
|
|
276
|
+
* when the final static output contains the file or the deployment retains
|
|
277
|
+
* the runtime Markdown handler.
|
|
278
|
+
* @default false
|
|
279
|
+
*/
|
|
280
|
+
markdownLinks?: boolean;
|
|
274
281
|
/** The sections of the documentation */
|
|
275
282
|
sections?: LlmsTxtSection[];
|
|
276
|
-
/**
|
|
283
|
+
/** Additional heading-free preamble notes rendered before file-list sections */
|
|
277
284
|
notes?: string | string[];
|
|
278
285
|
}
|
|
279
286
|
/**
|
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.8",
|
|
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": {
|
|
@@ -64,11 +64,11 @@
|
|
|
64
64
|
"defu": "^6.1.7",
|
|
65
65
|
"drizzle-orm": "^0.45.2",
|
|
66
66
|
"mdream": "^1.5.3",
|
|
67
|
-
"nuxt-site-config": "^4.1.
|
|
67
|
+
"nuxt-site-config": "^4.1.2",
|
|
68
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
|
},
|
|
@@ -82,7 +82,7 @@
|
|
|
82
82
|
"@nuxtjs/eslint-config-typescript": "^12.1.0",
|
|
83
83
|
"@nuxtjs/mcp-toolkit": "^0.18.0",
|
|
84
84
|
"@nuxtjs/robots": "^6.1.3",
|
|
85
|
-
"@nuxtjs/sitemap": "^8.
|
|
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",
|
|
@@ -95,7 +95,7 @@
|
|
|
95
95
|
"happy-dom": "^20.11.0",
|
|
96
96
|
"nitropack": "^2.13.4",
|
|
97
97
|
"nuxt": "^4.5.0",
|
|
98
|
-
"nuxt-site-config": "^4.1.
|
|
98
|
+
"nuxt-site-config": "^4.1.2",
|
|
99
99
|
"nuxtseo-layer-devtools": "^5.3.3",
|
|
100
100
|
"playwright": "^1.61.1",
|
|
101
101
|
"playwright-core": "^1.61.1",
|