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 CHANGED
@@ -4,7 +4,7 @@
4
4
  "nuxt": ">=4.0.0"
5
5
  },
6
6
  "configKey": "aiReady",
7
- "version": "1.5.5",
7
+ "version": "1.5.7",
8
8
  "builder": {
9
9
  "@nuxt/module-builder": "1.0.2",
10
10
  "unbuild": "3.6.1"
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 = `${siteUrl}/__ai-ready/pages.meta.json`;
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 = `${siteUrl}/${indexNow}.txt`;
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 = normalizePagePath(loc.startsWith("http") ? new URL(loc).pathname : loc);
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 = withBase(mdRoute, nitro.options.baseURL);
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 pageRoute2 = route.route.replace(RE_HTML_MD_EXT, "").replace(RE_INDEX_SUFFIX, "") || "/";
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.0.0"
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 markdown string
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
- function normalizeLink(link) {
2
- const parts = [];
3
- parts.push(`- [${link.title}](${link.href})`);
4
- if (link.description)
5
- parts.push(` ${link.description}`);
6
- return parts.join("\n");
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 normalizeSection(section, headingLevel = 2) {
9
- const prefix = "#".repeat(headingLevel);
10
- const parts = [];
11
- parts.push(`${prefix} ${section.title}`);
12
- parts.push("");
13
- if (section.description) {
14
- const descriptions = Array.isArray(section.description) ? section.description : [section.description];
15
- parts.push(...descriptions);
16
- parts.push("");
17
- }
9
+ function normalizeLinkTitle(value) {
10
+ return normalizeInlineText(value).replace(RE_LINK_TITLE_BRACKET, (bracket) => bracket === "[" ? "&#91;" : "&#93;");
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
- export function normalizeLlmsTxtConfig(config) {
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
- parts.push("## Notes");
34
- parts.push("");
35
- const notes = Array.isArray(config.notes) ? config.notes : [config.notes];
36
- parts.push(...notes);
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 { normalizeLlmsTxtConfig } from "./llms-txt-format.js";
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 { localePath, resolveLocaleFromRoute } from "./server/utils/i18n.js";
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
- const descText = page.description ? `: ${page.description.substring(0, 160)}${page.description.length > 160 ? "..." : ""}` : "";
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 || siteConfig.url}`);
110
+ parts.push(`# ${siteConfig.name || canonicalSiteUrl}`);
121
111
  if (siteConfig.description) {
122
112
  parts.push(`
123
113
  > ${siteConfig.description}`);
124
114
  }
125
- if (siteConfig.url) {
115
+ if (canonicalSiteUrl) {
126
116
  parts.push(`
127
- Canonical Origin: ${siteConfig.url}`);
117
+ Canonical Origin: ${canonicalSiteUrl}`);
128
118
  }
129
119
  parts.push("");
130
- const sections = llmsTxtConfig.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
- if (sitemapConfig?.sitemaps) {
133
- const sitemapRoutes = Object.values(sitemapConfig.sitemaps).map((s) => s.sitemapName);
134
- for (const name of sitemapRoutes) {
135
- sections[0].links.push({ title: name, href: `/${name}`, description: "XML sitemap for search engines and crawlers." });
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({ title: "robots.txt", href: "/robots.txt", description: "Crawler rules and permissions." });
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
- seenPaths.add(page.route);
165
- prerendered.push({ pathname: page.route, title: page.title, description: page.description, locale: page.locale });
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 url of urls) {
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 pages.filter((p) => errorRoutes.has(p.route)).map((p) => ({
61
- route: p.route,
62
- title: p.title,
63
- description: p.description,
64
- ...includeMarkdown ? { markdown: p.markdown } : {},
65
- headings: p.headings,
66
- keywords: JSON.stringify(p.keywords),
67
- updated_at: p.updatedAt,
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: p.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 config = useRuntimeConfig(event)["nuxt-ai-ready"];
68
- const resolveUrl = (path2) => withSiteUrl(event, path2);
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 runtimeConfig = useRuntimeConfig(event)["nuxt-ai-ready"];
30
- const canonicalUrl = withSiteUrl(event, path);
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(path, { signal: AbortSignal.timeout(3e4) }).catch((err) => {
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 config = useRuntimeConfig()["nuxt-ai-ready"];
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: siteConfig.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" ? "arrayBuffer" : void 0
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 : `${siteUrl}${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: `${siteUrl}/${key}.txt`,
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
- parts.push(`<${encodePathForHeader(toMarkdownPath(path))}>; rel="alternate"; type="text/markdown"`);
18
+ const href = resolveHeaderUrl(toMarkdownPath(path), resolveUrl);
19
+ parts.push(`<${encodePathForHeader(href)}>; rel="alternate"; type="text/markdown"`);
19
20
  } else {
20
- parts.push(`<${encodePathForHeader(path)}>; rel="alternate"; type="text/html"`);
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 ? `${siteUrl.replace(RE_TRAILING_SLASH, "")}${route}` : route;
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 { isSitemapIndex, parseSitemapIndex, parseSitemapXml } from "@nuxtjs/sitemap/utils";
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 normalizeUrls(urls) {
30
- return urls.map((entry) => {
31
- if (typeof entry === "string")
32
- return { loc: entry };
33
- const e = entry;
34
- return {
35
- loc: e.loc,
36
- lastmod: e.lastmod instanceof Date ? e.lastmod.toISOString() : e.lastmod
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 sitemapXml = null;
45
+ let sitemapInput = null;
46
46
  if (usePublicAsset) {
47
- sitemapXml = await fetchPublicAsset(event, fetchRoute, { responseType: "text" });
48
- if (!sitemapXml) {
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: "text",
56
+ responseType: "stream",
57
57
  timeout: FETCH_TIMEOUT
58
58
  });
59
- if (!res || typeof res !== "string") {
59
+ if (!res) {
60
60
  logger.warn(`[sitemap] Empty response from ${fetchRoute}`);
61
61
  return { urls: [], error: "Empty response" };
62
62
  }
63
- sitemapXml = res;
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 (${sitemapXml.length} bytes)`);
71
- if (isSitemapIndex(sitemapXml)) {
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
- let index;
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 index.entries) {
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}/${index.entries.length} child sitemaps failed (${childErrors.join("; ")})`;
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
  }
@@ -260,11 +260,11 @@ export interface LlmsTxtLink {
260
260
  export interface LlmsTxtSection {
261
261
  /** The title of the section */
262
262
  title: string;
263
- /** The description of the section (can be array for multiple paragraphs) */
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 section as optional per llms.txt spec. Optional sections appear under `## Optional` and LLMs may skip them with shorter context windows. */
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
- /** Notes section (always appears at the end) */
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.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": ">=7.0.0",
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.2",
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.2",
67
- "nuxt-site-config": "^4.1.1",
68
- "nuxtseo-shared": "^5.3.2",
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.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.2",
85
- "@nuxtjs/sitemap": "^8.2.2",
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.10.6",
95
+ "happy-dom": "^20.11.0",
96
96
  "nitropack": "^2.13.4",
97
97
  "nuxt": "^4.5.0",
98
- "nuxt-site-config": "^4.1.1",
99
- "nuxtseo-layer-devtools": "^5.3.2",
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": "^6.0.3",
103
+ "typescript": "6.0.3",
104
104
  "unbuild": "^3.6.1",
105
105
  "vitest": "^4.1.10",
106
106
  "vue": "^3.5.40",