nuxt-ai-ready 3.0.1 → 3.0.3

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.CsGXtlrU.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.CsGXtlrU.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.1",
7
+ "version": "3.0.3",
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.CsGXtlrU.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';
@@ -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) {
@@ -2183,6 +2390,13 @@ export function trackDrizzleWork(event, work) { return work }
2183
2390
  }
2184
2391
  });
2185
2392
  }
2393
+ let sharedMarkdownRuleNoted = false;
2394
+ const noteSharedMarkdownRule = (total, dropped) => {
2395
+ if (sharedMarkdownRuleNoted)
2396
+ return;
2397
+ sharedMarkdownRuleNoted = true;
2398
+ logger.info(`Cloudflare allows ${CLOUDFLARE_STATIC_HEADER_RULE_LIMIT} _headers rules. One rule per Markdown page would make ${total}. The ${dropped} Markdown pages share the /*.md rule, which sends no per-page canonical Link header.`);
2399
+ };
2186
2400
  nuxt.hooks.hook("nitro:build:before", (nitro) => {
2187
2401
  const isCloudflarePreset = String(nitro.options.preset || "").startsWith("cloudflare");
2188
2402
  nitro.hooks.hook("prerender:done", async () => {
@@ -2214,7 +2428,7 @@ export function trackDrizzleWork(event, work) { return work }
2214
2428
  );
2215
2429
  if (plan._tag === "skip") {
2216
2430
  applyStaticMarkdownHeaderPlan(nitro.options.routeRules, plan);
2217
- logger.warn(`_headers would hold ${plan.total} rules and Cloudflare allows ${CLOUDFLARE_STATIC_HEADER_RULE_LIMIT}. Skipped the ${plan.dropped} per-page .md rules; the /*.md glob still sets the charset and describedby entries, but static markdown does not send a per-page canonical Link.`);
2431
+ noteSharedMarkdownRule(plan.total, plan.dropped);
2218
2432
  return;
2219
2433
  }
2220
2434
  applyStaticMarkdownHeaderPlan(nitro.options.routeRules, plan);
@@ -2279,10 +2493,11 @@ export function trackDrizzleWork(event, work) { return work }
2279
2493
  if (staticDiscoverySupported && apiCatalogConfig && await hasArtifact(API_CATALOG_PATH))
2280
2494
  discovery.push(`<${encodeURI(apiCatalogConfig.href)}>; rel="api-catalog"`);
2281
2495
  mergedHeaders = mergeStaticDiscoveryHeaders(mergedHeaders, staticBaseURL, discovery);
2496
+ mergedHeaders = dedupeStaticHeaders(mergedHeaders, ["Content-Type", "Link", "Vary"]);
2282
2497
  const budget = enforceStaticHeaderBudget(mergedHeaders, CLOUDFLARE_STATIC_HEADER_RULE_LIMIT);
2283
2498
  if (budget.dropped > 0) {
2284
2499
  mergedHeaders = budget.contents;
2285
- logger.warn(`_headers had ${budget.total + budget.dropped} rules and Cloudflare allows ${CLOUDFLARE_STATIC_HEADER_RULE_LIMIT}. Dropped the ${budget.dropped} per-page .md rules; the /*.md glob still sets the charset and describedby entries, but static markdown no longer sends a per-page canonical Link.`);
2500
+ noteSharedMarkdownRule(budget.total + budget.dropped, budget.dropped);
2286
2501
  }
2287
2502
  if (budget.total > CLOUDFLARE_STATIC_HEADER_RULE_LIMIT)
2288
2503
  logger.warn(`_headers still has ${budget.total} rules after dropping the .md rules. Cloudflare will reject the deploy until the other rules fit under ${CLOUDFLARE_STATIC_HEADER_RULE_LIMIT}.`);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "nuxt-ai-ready",
3
3
  "type": "module",
4
- "version": "3.0.1",
4
+ "version": "3.0.3",
5
5
  "description": "Best practice AI & LLM discoverability for Nuxt sites.",
6
6
  "author": {
7
7
  "name": "Harlan Wilton",
@@ -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