nuxt-ai-ready 3.0.0 → 3.0.2

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.
@@ -2,7 +2,7 @@ import { createHash } from 'node:crypto';
2
2
  import { realpath, readFile, readdir } from 'node:fs/promises';
3
3
  import { join, resolve, isAbsolute, relative, sep } from 'node:path';
4
4
  import { parseDocument } from 'yaml';
5
- import { A as AGENT_SKILLS_SCHEMA } from '../shared/nuxt-ai-ready.C34F0LJo.mjs';
5
+ import { A as AGENT_SKILLS_SCHEMA } from '../shared/nuxt-ai-ready.Deh7Mj_G.mjs';
6
6
  import 'node:module';
7
7
  import 'node:url';
8
8
  import '@nuxt/kit';
@@ -6,7 +6,7 @@ import { resolveLocaleFromRoute } from 'nuxtseo-shared/i18n-runtime';
6
6
  import { createPrerenderFetch } from 'nuxtseo-shared/prerender';
7
7
  import { collectSitemap } from 'sitemapd/parse';
8
8
  import { withLeadingSlash, joinURL, withBase } from 'ufo';
9
- import { l as logger, M as MARKDOWN_LINK_AVAILABILITY_FILE } from '../shared/nuxt-ai-ready.C34F0LJo.mjs';
9
+ import { l as logger, M as MARKDOWN_LINK_AVAILABILITY_FILE } from '../shared/nuxt-ai-ready.Deh7Mj_G.mjs';
10
10
  import { normalizePagePath, toMarkdownPath } from '../../dist/runtime/markdown-path.js';
11
11
  import { toDeployedRoute, toLogicalRoute } from '../../dist/runtime/route-path.js';
12
12
  import { initSchema, computeContentHash, insertPage, queryAllPages, exportDbDump } from '../../dist/runtime/server/db/shared.js';
@@ -299,7 +299,7 @@ async function prerenderRoute(nitro, route, fetch) {
299
299
  nitro._prerenderedRoutes.push(_route);
300
300
  return stat(filePath);
301
301
  }
302
- function setupPrerenderHandler(options, dbPath, siteInfo, llmsTxtConfig, extras = {}) {
302
+ function setupPrerenderHandler(options, dbPath, siteInfo, llmsTxtConfig, extras) {
303
303
  const nuxt = useNuxt();
304
304
  nuxt.hooks.hook("nitro:init", async (nitro) => {
305
305
  const llmsFullTxtPath = join(nitro.options.output.publicDir, "llms-full.txt");
@@ -390,20 +390,22 @@ function setupPrerenderHandler(options, dbPath, siteInfo, llmsTxtConfig, extras
390
390
  logger.debug(`Writing pages.json: ${publicJsonPath} (${(jsonContent.length / 1024).toFixed(1)}kb)`);
391
391
  await writeFile(publicJsonPath, jsonContent, "utf-8");
392
392
  logger.debug(`Wrote ${pages.length} pages to __ai-ready/pages.json`);
393
- const dumpData = await exportDbDump(state.db);
394
- const dumpPath = join(publicDataDir, "pages.dump");
395
- logger.debug(`Writing pages.dump: ${dumpPath} (${(dumpData.length / 1024).toFixed(1)}kb)`);
396
- await writeFile(dumpPath, dumpData, "utf-8");
397
- logger.debug(`Created database dump at __ai-ready/pages.dump (${(dumpData.length / 1024).toFixed(1)}kb compressed)`);
398
- const buildId = Date.now().toString(36);
399
- const metaContent = JSON.stringify({
400
- buildId,
401
- pageCount: pages.length,
402
- createdAt: (/* @__PURE__ */ new Date()).toISOString()
403
- });
404
- logger.debug(`Writing pages.meta.json (${(metaContent.length / 1024).toFixed(1)}kb)`);
405
- await writeFile(join(publicDataDir, "pages.meta.json"), metaContent, "utf-8");
406
- logger.debug(`Wrote build metadata: buildId=${buildId}`);
393
+ if (extras.runtimeDatabase === "Enabled") {
394
+ const dumpData = await exportDbDump(state.db);
395
+ const dumpPath = join(publicDataDir, "pages.dump");
396
+ logger.debug(`Writing pages.dump: ${dumpPath} (${(dumpData.length / 1024).toFixed(1)}kb)`);
397
+ await writeFile(dumpPath, dumpData, "utf-8");
398
+ logger.debug(`Created database dump at __ai-ready/pages.dump (${(dumpData.length / 1024).toFixed(1)}kb compressed)`);
399
+ const buildId = Date.now().toString(36);
400
+ const metaContent = JSON.stringify({
401
+ buildId,
402
+ pageCount: pages.length,
403
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
404
+ });
405
+ logger.debug(`Writing pages.meta.json (${(metaContent.length / 1024).toFixed(1)}kb)`);
406
+ await writeFile(join(publicDataDir, "pages.meta.json"), metaContent, "utf-8");
407
+ logger.debug(`Wrote build metadata: buildId=${buildId}`);
408
+ }
407
409
  }
408
410
  if (state.dbPath && state.llmsTxtConfig?.markdownLinks) {
409
411
  const availabilityFile = join(dirname(state.dbPath), MARKDOWN_LINK_AVAILABILITY_FILE);
package/dist/module.d.mts CHANGED
@@ -6,6 +6,22 @@ import { ResolvedWebMcpToolsConfig } from '../dist/runtime/site-tool-config.js';
6
6
  export { GetPageMarkdownToolOptions, ListPagesToolOptions, McpSiteToolAttachmentOptions, SearchPagesToolOptions, SiteToolOptions, SiteToolsConfig, WebMcpSiteToolAttachmentOptions } from '../dist/runtime/site-tool-config.js';
7
7
  import { AiCatalog } from '../dist/runtime/server/utils/discovery-response.js';
8
8
 
9
+ type DatabaseType = 'sqlite' | 'bun' | 'd1' | 'libsql' | 'neon' | 'postgres';
10
+ /**
11
+ * Database state after parsing. Every consumer reads `_tag` first, so a
12
+ * disabled database can never carry a driver, a path, or credentials.
13
+ */
14
+ type ResolvedDatabase = {
15
+ _tag: 'Disabled';
16
+ } | {
17
+ _tag: 'Enabled';
18
+ type: DatabaseType;
19
+ filename?: string;
20
+ bindingName?: string;
21
+ url?: string;
22
+ authToken?: string;
23
+ };
24
+
9
25
  interface ParsedMarkdownResult {
10
26
  markdown: string;
11
27
  title: string;
@@ -31,22 +47,6 @@ interface ResolvedApiCatalogConfig {
31
47
  };
32
48
  }
33
49
 
34
- type DatabaseType = 'sqlite' | 'bun' | 'd1' | 'libsql' | 'neon' | 'postgres';
35
- /**
36
- * Database state after parsing. Every consumer reads `_tag` first, so a
37
- * disabled database can never carry a driver, a path, or credentials.
38
- */
39
- type ResolvedDatabase = {
40
- _tag: 'Disabled';
41
- } | {
42
- _tag: 'Enabled';
43
- type: DatabaseType;
44
- filename?: string;
45
- bindingName?: string;
46
- url?: string;
47
- authToken?: string;
48
- };
49
-
50
50
  interface ResolvedWebMcpConfig {
51
51
  tools: ResolvedWebMcpToolsConfig;
52
52
  exposedTo?: string[];
package/dist/module.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "nuxt": "^4.6.0 || ^5.0.0-0"
5
5
  },
6
6
  "configKey": "aiReady",
7
- "version": "3.0.0",
7
+ "version": "3.0.2",
8
8
  "builder": {
9
9
  "@nuxt/module-builder": "1.0.3",
10
10
  "unbuild": "3.6.1"
package/dist/module.mjs CHANGED
@@ -7,7 +7,7 @@ import '@nuxt/kit';
7
7
  import 'defu';
8
8
  import 'nuxt-site-config/kit';
9
9
  import 'nuxtseo-shared/kit';
10
- export { m as default } from './shared/nuxt-ai-ready.C34F0LJo.mjs';
10
+ export { m as default } from './shared/nuxt-ai-ready.Deh7Mj_G.mjs';
11
11
  import './shared/nuxt-ai-ready.B3hAuOLQ.mjs';
12
12
  import '../dist/runtime/markdown-path.js';
13
13
  import '../dist/runtime/route-path.js';
@@ -0,0 +1,4 @@
1
+ import { hash } from "ohash";
2
+ export function siteToolCacheKey(args) {
3
+ return hash(args);
4
+ }
@@ -3,6 +3,7 @@ import siteTools from "#ai-ready-virtual/site-tools.mjs";
3
3
  import { useEvent } from "#nuxtseo/nitro";
4
4
  import { normalizeSiteRoute, SITE_TOOL_CATALOG } from "../../../site-tool-catalog.js";
5
5
  import { queryPages } from "../../db/queries.js";
6
+ import { siteToolCacheKey } from "../cache-key.js";
6
7
  const inputSchema = {
7
8
  route: z.string().describe(SITE_TOOL_CATALOG.get_page_markdown.parameters.route)
8
9
  };
@@ -13,7 +14,7 @@ const tool = {
13
14
  inputSchema,
14
15
  annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false },
15
16
  enabled: () => siteTools.getPageMarkdown.mcp.enabled,
16
- cache: "5m",
17
+ cache: { maxAge: "5m", getKey: siteToolCacheKey },
17
18
  async handler({ route }) {
18
19
  const path = normalizeSiteRoute(route);
19
20
  if (!path) {
@@ -3,6 +3,7 @@ import siteTools from "#ai-ready-virtual/site-tools.mjs";
3
3
  import { useEvent } from "#nuxtseo/nitro";
4
4
  import { SITE_TOOL_CATALOG } from "../../../site-tool-catalog.js";
5
5
  import { countPages, queryPages } from "../../db/queries.js";
6
+ import { siteToolCacheKey } from "../cache-key.js";
6
7
  const inputSchema = {
7
8
  limit: z.number().int().min(1).max(50).optional().default(siteTools.listPages.defaultLimit).describe(
8
9
  `${SITE_TOOL_CATALOG.list_pages.parameters.limit} Defaults to ${siteTools.listPages.defaultLimit}.`
@@ -16,7 +17,7 @@ const tool = {
16
17
  inputSchema,
17
18
  annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false },
18
19
  enabled: () => siteTools.listPages.mcp.enabled,
19
- cache: "1h",
20
+ cache: { maxAge: "1h", getKey: siteToolCacheKey },
20
21
  async handler({ limit, offset }) {
21
22
  const event = useEvent();
22
23
  const pages = await queryPages(event, { limit, offset });
@@ -3,6 +3,7 @@ import siteTools from "#ai-ready-virtual/site-tools.mjs";
3
3
  import { useEvent } from "#nuxtseo/nitro";
4
4
  import { SITE_TOOL_CATALOG } from "../../../site-tool-catalog.js";
5
5
  import { searchPages } from "../../db/queries.js";
6
+ import { siteToolCacheKey } from "../cache-key.js";
6
7
  const inputSchema = {
7
8
  query: z.string().describe(SITE_TOOL_CATALOG.search_pages.parameters.query),
8
9
  limit: z.number().int().min(1).max(50).optional().default(siteTools.searchPages.defaultLimit).describe(
@@ -16,7 +17,7 @@ const tool = {
16
17
  inputSchema,
17
18
  annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false },
18
19
  enabled: () => siteTools.searchPages.mcp.enabled,
19
- cache: "5m",
20
+ cache: { maxAge: "5m", getKey: siteToolCacheKey },
20
21
  async handler({ query, limit }) {
21
22
  const event = useEvent();
22
23
  const results = await searchPages(event, query, { limit });
@@ -1,20 +1,21 @@
1
1
  import { defineNitroPlugin } from "#nuxtseo/nitro";
2
- import { ERROR_LINK_HEADER, STATUS_AWARE_LINK_HEADER } from "./link-header.js";
2
+ import { STATUS_AWARE_LINK_HEADER } from "./link-header.js";
3
3
  function readHeader(event) {
4
4
  return event.context[STATUS_AWARE_LINK_HEADER];
5
5
  }
6
+ function setLinkUnlessImmutable(response, value) {
7
+ try {
8
+ response.headers.set("link", value);
9
+ } catch (error) {
10
+ if (!(error instanceof TypeError))
11
+ throw error;
12
+ }
13
+ }
6
14
  export default defineNitroPlugin((nitro) => {
7
15
  const hooks = nitro.hooks;
8
- hooks.hook("error", (_error, { event }) => {
9
- if (!event)
10
- return;
11
- const header = readHeader(event);
12
- if (header)
13
- event.req.headers.set(ERROR_LINK_HEADER, header.error);
14
- });
15
16
  hooks.hook("response", (response, event) => {
16
17
  const header = readHeader(event);
17
18
  if (header)
18
- response.headers.set("link", response.status >= 400 ? header.error : header.success);
19
+ setLinkUnlessImmutable(response, response.status >= 400 ? header.error : header.success);
19
20
  });
20
21
  });
@@ -18,6 +18,8 @@ export default defineNitroPlugin((nitro) => {
18
18
  event.node.req.headers[ERROR_LINK_HEADER] = header.error;
19
19
  });
20
20
  nitro.hooks.hook("beforeResponse", (event) => {
21
+ if (event.node.res.headersSent)
22
+ return;
21
23
  const header = event.context[STATUS_AWARE_LINK_HEADER];
22
24
  if (!header)
23
25
  return;
@@ -3,7 +3,7 @@ import { readFile, mkdir, writeFile, access } from 'node:fs/promises';
3
3
  import { createRequire } from 'node:module';
4
4
  import { dirname, basename, join, resolve, relative } from 'node:path';
5
5
  import { fileURLToPath, pathToFileURL } from 'node:url';
6
- import { hasNuxtModule, useNuxt, useLogger, addTypeTemplate, addTemplate, defineNuxtModule, createResolver, getLayerDirectories, tryResolveModule, directoryToURL, addServerImports, addServerHandler, extendRouteRules, getNitroVersion, addNitroPlugin, addPlugin, addImports } from '@nuxt/kit';
6
+ import { hasNuxtModule, useNuxt, useLogger, addTypeTemplate, addTemplate, defineNuxtModule, createResolver, getLayerDirectories, tryResolveModule, directoryToURL, addServerImports, addServerHandler, extendRouteRules, getNitroVersion, addPlugin, addNitroPlugin, addImports } from '@nuxt/kit';
7
7
  import defu$1, { defu } from 'defu';
8
8
  import { useSiteConfig, withSiteUrl } from 'nuxt-site-config/kit';
9
9
  import { resolveNuxtContentVersion, renderNitroTypeAugmentations, setupRuntimeAliases, setupNitroRuntimeCompatibility } from 'nuxtseo-shared/kit';
@@ -756,14 +756,81 @@ const DATABASE_CONSUMER_LABELS = {
756
756
  mcp: "MCP site tools (`@nuxtjs/mcp-toolkit`)",
757
757
  webmcp: "WebMCP site tools (`aiReady.webmcp`)"
758
758
  };
759
- function formatMissingDrizzleMessage(input) {
759
+ const RE_SEMVER = /^v?(\d+)\.(\d+)\.(\d+)(?:-([\da-z-]+(?:\.[\da-z-]+)*))?(?:\+[\da-z-]+(?:\.[\da-z-]+)*)?$/i;
760
+ const RE_NUMERIC = /^\d+$/;
761
+ function parseSemVer(input) {
762
+ const match = RE_SEMVER.exec(input.trim());
763
+ if (!match)
764
+ return void 0;
765
+ return {
766
+ core: [Number(match[1]), Number(match[2]), Number(match[3])],
767
+ prerelease: match[4]?.split(".") ?? []
768
+ };
769
+ }
770
+ function compareCore(a, b) {
771
+ for (let i = 0; i < 3; i++) {
772
+ if (a[i] !== b[i])
773
+ return a[i] < b[i] ? -1 : 1;
774
+ }
775
+ return 0;
776
+ }
777
+ function comparePrerelease(a, b) {
778
+ if (a.length === 0 && b.length === 0)
779
+ return 0;
780
+ if (a.length === 0)
781
+ return 1;
782
+ if (b.length === 0)
783
+ return -1;
784
+ for (let i = 0; i < Math.max(a.length, b.length); i++) {
785
+ const left = a[i];
786
+ const right = b[i];
787
+ if (left === void 0)
788
+ return -1;
789
+ if (right === void 0)
790
+ return 1;
791
+ if (left === right)
792
+ continue;
793
+ const leftNumeric = RE_NUMERIC.test(left);
794
+ const rightNumeric = RE_NUMERIC.test(right);
795
+ if (leftNumeric && rightNumeric)
796
+ return Number(left) < Number(right) ? -1 : 1;
797
+ if (leftNumeric !== rightNumeric)
798
+ return leftNumeric ? -1 : 1;
799
+ return left < right ? -1 : 1;
800
+ }
801
+ return 0;
802
+ }
803
+ function matchCaretRange(version, range) {
804
+ const installed = parseSemVer(version);
805
+ const minimum = range.trim().startsWith("^") ? parseSemVer(range.trim().slice(1)) : void 0;
806
+ if (!installed || !minimum)
807
+ return { _tag: "Unparsed" };
808
+ const [major, minor, patch] = minimum.core;
809
+ const ceiling = major > 0 ? [major + 1, 0, 0] : minor > 0 ? [0, minor + 1, 0] : [0, 0, patch + 1];
810
+ const sameCore = compareCore(installed.core, minimum.core) === 0;
811
+ const satisfied = (compareCore(installed.core, minimum.core) > 0 || sameCore && comparePrerelease(installed.prerelease, minimum.prerelease) >= 0) && compareCore(installed.core, ceiling) < 0 && (installed.prerelease.length === 0 || sameCore && minimum.prerelease.length > 0);
812
+ return satisfied ? { _tag: "Satisfied" } : { _tag: "Unsatisfied" };
813
+ }
814
+ function checkDrizzleInstall(install, peerRange) {
815
+ if (install._tag === "Missing")
816
+ return { _tag: "Missing" };
817
+ if (!install.version || !peerRange)
818
+ return void 0;
819
+ return matchCaretRange(install.version, peerRange)._tag === "Unsatisfied" ? { _tag: "OutOfRange", version: install.version } : void 0;
820
+ }
821
+ function formatDrizzleMessage(input) {
760
822
  const consumers = Object.keys(DATABASE_CONSUMER_LABELS).filter((consumer) => input.requested[consumer]);
761
823
  const subject = consumers.length === 0 ? "The `aiReady.database` option turns on the database." : `These features need the database: ${consumers.map((consumer) => DATABASE_CONSUMER_LABELS[consumer]).join(", ")}.`;
762
824
  const version = input.peerRange?.replace(/^\^/, "");
763
- const lines = [
825
+ const command = `npx nypm add drizzle-orm${version ? `@${version}` : ""}`;
826
+ const lines = input.problem._tag === "Missing" ? [
764
827
  subject,
765
828
  "The database needs `drizzle-orm`. This app does not install it.",
766
- `Install it: npx nypm add drizzle-orm${version ? `@${version}` : ""}`
829
+ `Install it: ${command}`
830
+ ] : [
831
+ subject,
832
+ `The database needs \`drizzle-orm\` ${input.peerRange}. This app installs ${input.problem.version}.`,
833
+ `Upgrade it: ${command}`
767
834
  ];
768
835
  if (consumers.length > 0 && consumers.every((consumer) => consumer === "mcp" || consumer === "webmcp"))
769
836
  lines.push("If you do not need the site tools, set `aiReady.database: false`.");
@@ -1176,6 +1243,137 @@ function mergeStaticDiscoveryHeaders(contents, baseURL, entries) {
1176
1243
  }
1177
1244
  return entries.reduce((headers, entry) => ensureStaticHeader(headers, route, "Link", entry, "append"), contents);
1178
1245
  }
1246
+ function parseHeaderLine(line) {
1247
+ const colon = line.indexOf(":");
1248
+ if (!/^[\t ]/.test(line) || colon < 0)
1249
+ return void 0;
1250
+ const name = line.slice(0, colon).trim();
1251
+ if (!name || name.startsWith("!") || /\s/.test(name))
1252
+ return void 0;
1253
+ const eol = line.endsWith("\r\n") ? "\r\n" : line.endsWith("\n") ? "\n" : "";
1254
+ return {
1255
+ name,
1256
+ value: line.slice(colon + 1, line.length - eol.length).trim(),
1257
+ withValue: (value) => `${line.slice(0, colon + 1)} ${value}${eol}`
1258
+ };
1259
+ }
1260
+ const RE_DETACH_LINE = /^[\t ]+![\t ]*([^\s:]+)/;
1261
+ function patternToRegExp(pattern) {
1262
+ const source = pattern.split(/(\*|:\w+)/).map((part) => part === "*" ? ".*" : part.startsWith(":") ? "[^/]+" : escapeRegExp(part)).join("");
1263
+ return new RegExp(`^${source}$`);
1264
+ }
1265
+ function coversPattern(broad, narrow) {
1266
+ if (broad === narrow || !broad.startsWith("/") || !narrow.startsWith("/"))
1267
+ return false;
1268
+ if (!/[*:]/.test(narrow))
1269
+ return patternToRegExp(broad).test(narrow);
1270
+ return broad.indexOf("*") === broad.length - 1 && !broad.includes(":") && narrow.startsWith(broad.slice(0, -1));
1271
+ }
1272
+ function splitLinkEntries(value) {
1273
+ const entries = [];
1274
+ let current = "";
1275
+ let inUrl = false;
1276
+ let inQuote = false;
1277
+ for (const char of value) {
1278
+ if (char === "<" && !inQuote)
1279
+ inUrl = true;
1280
+ else if (char === ">" && !inQuote)
1281
+ inUrl = false;
1282
+ else if (char === '"' && !inUrl)
1283
+ inQuote = !inQuote;
1284
+ if (char === "," && !inUrl && !inQuote) {
1285
+ entries.push(current.trim());
1286
+ current = "";
1287
+ continue;
1288
+ }
1289
+ current += char;
1290
+ }
1291
+ entries.push(current.trim());
1292
+ return entries.filter(Boolean);
1293
+ }
1294
+ function headerTokens(name, value) {
1295
+ if (name === "link")
1296
+ return splitLinkEntries(value);
1297
+ if (name === "vary")
1298
+ return value.split(",").map((token) => token.trim()).filter(Boolean);
1299
+ return [value.trim()];
1300
+ }
1301
+ function tokenKey(name, token) {
1302
+ return name === "vary" ? token.toLowerCase() : token;
1303
+ }
1304
+ function dedupeStaticHeaders(contents, names) {
1305
+ const owned = new Set(names.map((name) => name.toLowerCase()));
1306
+ const blocks = splitHeaderBlocks(contents);
1307
+ const detached = /* @__PURE__ */ new Set();
1308
+ const parsed = blocks.map((block) => {
1309
+ const headers = [];
1310
+ for (const line of block.text.split(/(?<=\n)/).slice(block.route === null ? 0 : 1)) {
1311
+ const detach = line.match(RE_DETACH_LINE);
1312
+ if (detach) {
1313
+ detached.add(detach[1].toLowerCase());
1314
+ continue;
1315
+ }
1316
+ const header = parseHeaderLine(line);
1317
+ if (!header)
1318
+ continue;
1319
+ const name = header.name.toLowerCase();
1320
+ if (owned.has(name))
1321
+ headers.push({ name, tokens: headerTokens(name, header.value) });
1322
+ }
1323
+ return headers;
1324
+ });
1325
+ const lastRuleIndex = /* @__PURE__ */ new Map();
1326
+ blocks.forEach((block, index) => {
1327
+ if (block.route !== null)
1328
+ lastRuleIndex.set(block.route, index);
1329
+ });
1330
+ let changed = false;
1331
+ const output = blocks.map((block) => {
1332
+ const route = block.route;
1333
+ if (route === null)
1334
+ return block.text;
1335
+ const provided = /* @__PURE__ */ new Map();
1336
+ lastRuleIndex.forEach((index, pattern) => {
1337
+ if (!coversPattern(pattern, route))
1338
+ return;
1339
+ for (const { name, tokens } of parsed[index]) {
1340
+ if (detached.has(name))
1341
+ continue;
1342
+ const values = provided.get(name) ?? /* @__PURE__ */ new Set();
1343
+ tokens.forEach((token) => values.add(tokenKey(name, token)));
1344
+ provided.set(name, values);
1345
+ }
1346
+ });
1347
+ if (provided.size === 0)
1348
+ return block.text;
1349
+ const [routeLine, ...lines] = block.text.split(/(?<=\n)/);
1350
+ let headerLines = 0;
1351
+ const kept = lines.flatMap((line) => {
1352
+ const header = parseHeaderLine(line);
1353
+ if (!header) {
1354
+ if (RE_DETACH_LINE.test(line))
1355
+ headerLines++;
1356
+ return [line];
1357
+ }
1358
+ headerLines++;
1359
+ const name = header.name.toLowerCase();
1360
+ const covered = provided.get(name);
1361
+ if (!covered)
1362
+ return [line];
1363
+ const tokens = headerTokens(name, header.value);
1364
+ const remaining = tokens.filter((token) => !covered.has(tokenKey(name, token)));
1365
+ if (remaining.length === tokens.length)
1366
+ return [line];
1367
+ changed = true;
1368
+ if (remaining.length > 0)
1369
+ return [header.withValue(remaining.join(", "))];
1370
+ headerLines--;
1371
+ return [];
1372
+ });
1373
+ return headerLines === 0 ? kept.join("") : `${routeLine}${kept.join("")}`;
1374
+ });
1375
+ return changed ? output.join("") : contents;
1376
+ }
1179
1377
  const CLOUDFLARE_STATIC_HEADER_RULE_LIMIT = 100;
1180
1378
  const RE_EXACT_MARKDOWN_ROUTE = /^\/[^*\s]*\.md[\t ]*\r?$/;
1181
1379
  function splitHeaderBlocks(contents) {
@@ -1449,8 +1647,10 @@ ${details}`);
1449
1647
  const databaseEnabled = database._tag === "Enabled";
1450
1648
  if (databaseEnabled) {
1451
1649
  const drizzle = await tryResolveModule("drizzle-orm", directoryToURL(nuxt.options.rootDir));
1452
- if (!drizzle)
1453
- throw new Error(`[nuxt-ai-ready] ${formatMissingDrizzleMessage({ requested: databaseRequested, peerRange: peerDependencies["drizzle-orm"] })}`);
1650
+ const peerRange = peerDependencies["drizzle-orm"];
1651
+ const problem = checkDrizzleInstall(drizzle ? { _tag: "Installed", version: (await readPackageMetadata(drizzle)).version } : { _tag: "Missing" }, peerRange);
1652
+ if (problem)
1653
+ throw new Error(`[nuxt-ai-ready] ${formatDrizzleMessage({ problem, requested: databaseRequested, peerRange })}`);
1454
1654
  }
1455
1655
  if (!databaseEnabled)
1456
1656
  logger.debug("Database disabled. Page storage, runtime indexing and site tools are off.");
@@ -2018,11 +2218,17 @@ export function trackDrizzleWork(event, work) { return work }
2018
2218
  apiCatalog: apiCatalogConfig
2019
2219
  };
2020
2220
  if (prerenderEnabled) {
2021
- addNitroPlugin(resolve$1("./runtime/server/plugins/html-capture.prerender"));
2022
2221
  addServerHandler({
2023
2222
  middleware: true,
2223
+ env: "prerender",
2024
2224
  handler: resolve$1("./runtime/server/middleware/markdown.prerender")
2025
2225
  });
2226
+ const htmlCapturePlugin = resolve$1("./runtime/server/plugins/html-capture.prerender");
2227
+ nuxt.hook("nitro:init", (nitro) => {
2228
+ nitro.hooks.hook("prerender:config", (prerenderConfig) => {
2229
+ prerenderConfig.plugins = [...prerenderConfig.plugins || [], htmlCapturePlugin];
2230
+ });
2231
+ });
2026
2232
  addPlugin({
2027
2233
  mode: "server",
2028
2234
  src: resolve$1("./runtime/app/plugins/md-hints.prerender")
@@ -2127,7 +2333,8 @@ export function trackDrizzleWork(event, work) { return work }
2127
2333
  }, mergedLlmsTxt, {
2128
2334
  ftsTokenizer,
2129
2335
  i18n: i18nConfig,
2130
- artifactRoutes: agentSkillsResult._tag === "Enabled" ? Object.keys(agentSkillsResult.localArtifacts) : []
2336
+ artifactRoutes: agentSkillsResult._tag === "Enabled" ? Object.keys(agentSkillsResult.localArtifacts) : [],
2337
+ runtimeDatabase: database._tag
2131
2338
  });
2132
2339
  }
2133
2340
  if (databaseEnabled) {
@@ -2279,6 +2486,7 @@ export function trackDrizzleWork(event, work) { return work }
2279
2486
  if (staticDiscoverySupported && apiCatalogConfig && await hasArtifact(API_CATALOG_PATH))
2280
2487
  discovery.push(`<${encodeURI(apiCatalogConfig.href)}>; rel="api-catalog"`);
2281
2488
  mergedHeaders = mergeStaticDiscoveryHeaders(mergedHeaders, staticBaseURL, discovery);
2489
+ mergedHeaders = dedupeStaticHeaders(mergedHeaders, ["Content-Type", "Link", "Vary"]);
2282
2490
  const budget = enforceStaticHeaderBudget(mergedHeaders, CLOUDFLARE_STATIC_HEADER_RULE_LIMIT);
2283
2491
  if (budget.dropped > 0) {
2284
2492
  mergedHeaders = budget.contents;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "nuxt-ai-ready",
3
3
  "type": "module",
4
- "version": "3.0.0",
4
+ "version": "3.0.2",
5
5
  "description": "Best practice AI & LLM discoverability for Nuxt sites.",
6
6
  "author": {
7
7
  "name": "Harlan Wilton",
@@ -82,6 +82,7 @@
82
82
  "mdream": "^2.0.1",
83
83
  "nuxt-site-config": "^5.0.0",
84
84
  "nuxtseo-shared": "^6.0.0",
85
+ "ohash": "^2.0.12",
85
86
  "pathe": "^2.0.3",
86
87
  "site-config-stack": "^5.0.0",
87
88
  "sitemapd": "^0.2.2",
@@ -99,7 +99,7 @@ Runtime indexing, database drivers, Cloudflare D1, and the server query helpers:
99
99
  - **Root `skills/` folders require explicit publication.** Automatic discovery scans public directories only. Set `agentSkills.dir: 'skills'` to publish root skills intentionally.
100
100
  - **The same hook name exists twice.** `ai-ready:page:markdown` is a Nuxt hook (`nuxt.config` `hooks`, prerender only) and a Nitro hook (`nitroApp.hooks`, every HTML conversion). They receive different context. The Nitro `ctx.event` is undefined when indexing runs outside a request.
101
101
  - **`database: false` conflicts with `runtimeSync` and `cron`.** The build fails and names the option.
102
- - **A database feature without `drizzle-orm` fails the build.** `runtimeSync`, `cron`, MCP site tools, WebMCP site tools, and an explicit `database` option turn on the database. The error names the feature and the install command. If MCP Toolkit serves only your own tools, set `database: false` instead.
102
+ - **A database feature without `drizzle-orm` 1.x fails the build.** `runtimeSync`, `cron`, MCP site tools, WebMCP site tools, and an explicit `database` option turn on the database. A `drizzle-orm` 0.x install fails too. The error names the feature and the install command. If MCP Toolkit serves only your own tools, set `database: false` instead.
103
103
 
104
104
  ## Version limits
105
105