nuxt-ai-ready 0.12.0 → 0.12.1

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/README.md CHANGED
@@ -8,7 +8,7 @@
8
8
 
9
9
  ## Why Nuxt AI Ready?
10
10
 
11
- ChatGPT search interest doubled in the past year. Users now ask AI assistants questions your site could answerbut LLMs only cite sources they can parse.
11
+ [ChatGPT](https://chatgpt.com) search interest doubled in the past year. Users now ask AI assistants questions your site could answer - but LLMs only cite sources they can parse.
12
12
 
13
13
  Two standards are emerging: [llms.txt](https://llmstxt.org/) (4,400 searches/mo, +26,900% YoY) for AI-readable site summaries, and [MCP](https://modelcontextprotocol.io/) (22,200 searches/mo) for letting agents query your content directly.
14
14
 
package/dist/module.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "nuxt": ">=4.0.0"
5
5
  },
6
6
  "configKey": "aiReady",
7
- "version": "0.12.0",
7
+ "version": "0.12.1",
8
8
  "builder": {
9
9
  "@nuxt/module-builder": "1.0.2",
10
10
  "unbuild": "3.6.1"
package/dist/module.mjs CHANGED
@@ -81,6 +81,9 @@ function hookNuxtSeoProLicense() {
81
81
  }
82
82
 
83
83
  const BUILD_FETCH_TIMEOUT = 15e3;
84
+ const RE_HTML_MD_EXT = /\.(html|md)$/;
85
+ const RE_INDEX_SUFFIX = /\/index$/;
86
+ const RE_MD_EXT = /\.md$/;
84
87
  async function fetchPreviousMeta(siteUrl, indexNow) {
85
88
  const metaUrl = `${siteUrl}/__ai-ready/pages.meta.json`;
86
89
  logger.info(`Fetching previous build meta from ${metaUrl}`);
@@ -208,7 +211,7 @@ async function processSitemapEntry(state, nuxt, nitro, entry) {
208
211
  if (state.prerenderedRoutes.has(route)) {
209
212
  return { crawled: false, skipped: true };
210
213
  }
211
- const mdRoute = route === "/" ? "/index.md" : `${route}.md`;
214
+ const mdRoute = route.endsWith("/") ? `${route}index.md` : `${route}.md`;
212
215
  const mdUrl = withBase(mdRoute, nitro.options.baseURL);
213
216
  logger.debug(`Fetching markdown for ${route} \u2192 ${mdUrl}`);
214
217
  const res = await globalThis.$fetch(mdUrl, {
@@ -313,14 +316,14 @@ function setupPrerenderHandler(options, dbPath, siteInfo, llmsTxtConfig, indexNo
313
316
  let initPromise = null;
314
317
  nitro.hooks.hook("prerender:generate", async (route) => {
315
318
  if (route.error) {
316
- const pageRoute2 = route.route.replace(/\.(html|md)$/, "").replace(/\/index$/, "") || "/";
319
+ const pageRoute2 = route.route.replace(RE_HTML_MD_EXT, "").replace(RE_INDEX_SUFFIX, "") || "/";
317
320
  state.errorRoutes.add(pageRoute2);
318
321
  logger.debug(`Detected error page: ${pageRoute2}`);
319
322
  return;
320
323
  }
321
324
  if (!route.fileName?.endsWith(".md"))
322
325
  return;
323
- let pageRoute = route.route.replace(/\.md$/, "");
326
+ let pageRoute = route.route.replace(RE_MD_EXT, "");
324
327
  if (pageRoute === "/index")
325
328
  pageRoute = "/";
326
329
  const pageStartTime = Date.now();
@@ -1,8 +1,10 @@
1
1
  import { cronRuns, info, pages, sitemaps } from "#ai-ready-virtual/db-schema.mjs";
2
2
  import { and, count, desc, eq, gt, isNull, like, lt, or, sql } from "drizzle-orm";
3
3
  import { useDrizzle } from "./client.js";
4
+ const RE_LEADING_SLASH = /^\//;
5
+ const RE_SLASH = /\//g;
4
6
  function normalizeRouteKey(route) {
5
- return route.replace(/^\//, "").replace(/\//g, ":") || "index";
7
+ return route.replace(RE_LEADING_SLASH, "").replace(RE_SLASH, ":") || "index";
6
8
  }
7
9
  function rowToPage(row) {
8
10
  return {
@@ -3,6 +3,7 @@ const driverCache = /* @__PURE__ */ new WeakMap();
3
3
  export function registerDriver(db, type, driver) {
4
4
  driverCache.set(db, { type, driver });
5
5
  }
6
+ const RE_PARAM_PLACEHOLDER = /\?/g;
6
7
  export function getRawExecutor(client) {
7
8
  const cached = driverCache.get(client.db);
8
9
  if (!cached) {
@@ -30,7 +31,7 @@ export function getRawExecutor(client) {
30
31
  case "neon": {
31
32
  const sqlFn = driver;
32
33
  let idx = 0;
33
- const pgQuery = query.replace(/\?/g, () => `$${++idx}`);
34
+ const pgQuery = query.replace(RE_PARAM_PLACEHOLDER, () => `$${++idx}`);
34
35
  const result = await sqlFn.query(pgQuery, params);
35
36
  return result.rows || result;
36
37
  }
@@ -60,7 +61,7 @@ export function getRawExecutor(client) {
60
61
  case "neon": {
61
62
  const sqlFn = driver;
62
63
  let idx = 0;
63
- const pgQuery = query.replace(/\?/g, () => `$${++idx}`);
64
+ const pgQuery = query.replace(RE_PARAM_PLACEHOLDER, () => `$${++idx}`);
64
65
  await sqlFn.query(pgQuery, params);
65
66
  break;
66
67
  }
@@ -13,6 +13,8 @@ function getEventFromContext(providedEvent) {
13
13
  }
14
14
  let devWarningShown = false;
15
15
  let schemaInitialized = false;
16
+ const RE_FTS_CHARS = /[*:^"()]/g;
17
+ const RE_WHITESPACE = /\s+/;
16
18
  async function getDb(event) {
17
19
  if (import.meta.dev) {
18
20
  if (!devWarningShown) {
@@ -213,10 +215,10 @@ export async function searchPages(event, query, options = {}) {
213
215
  if (!db)
214
216
  return [];
215
217
  const { limit = 10 } = options;
216
- const sanitized = query.replace(/[*:^"()]/g, " ").trim();
218
+ const sanitized = query.replace(RE_FTS_CHARS, " ").trim();
217
219
  if (!sanitized)
218
220
  return [];
219
- const terms = sanitized.split(/\s+/).map((t) => `${t}*`).join(" ");
221
+ const terms = sanitized.split(RE_WHITESPACE).map((t) => `${t}*`).join(" ");
220
222
  return db.all(`
221
223
  SELECT p.route, p.title, p.description, bm25(ai_ready_pages_fts, 5.0, 3.0, 1.0, 0.5, 2.0, 2.0) as score
222
224
  FROM ai_ready_pages_fts
@@ -4,7 +4,7 @@ export async function computeContentHash(markdown) {
4
4
  const encoder = new TextEncoder();
5
5
  const data = encoder.encode(markdown);
6
6
  const hashBuffer = await subtle.digest("SHA-256", data);
7
- const hashArray = Array.from(new Uint8Array(hashBuffer));
7
+ const hashArray = [...new Uint8Array(hashBuffer)];
8
8
  return hashArray.map((b) => b.toString(16).padStart(2, "0")).join("").slice(0, 16);
9
9
  }
10
10
  export async function initSchema(db) {
@@ -31,8 +31,10 @@ async function getSchemaVersion(db) {
31
31
  ).catch(() => null);
32
32
  return info?.version || null;
33
33
  }
34
+ const RE_LEADING_SLASH = /^\//;
35
+ const RE_SLASH = /\//g;
34
36
  export function normalizeRouteKey(route) {
35
- return route.replace(/^\//, "").replace(/\//g, ":") || "index";
37
+ return route.replace(RE_LEADING_SLASH, "").replace(RE_SLASH, ":") || "index";
36
38
  }
37
39
  export async function compressToBase64(data) {
38
40
  const json = JSON.stringify(data);
@@ -48,7 +48,7 @@ export async function submitToIndexNowShared(routes, key, siteUrl, options) {
48
48
  return {
49
49
  success: false,
50
50
  error: lastError || "All endpoints rate limited",
51
- host: endpoints[endpoints.length - 1]
51
+ host: endpoints.at(-1)
52
52
  };
53
53
  }
54
54
  function normalizePagesToMap(pages) {
@@ -1,3 +1,6 @@
1
+ const RE_NON_WORD_CHARS = /[^\w\s-]/g;
2
+ const RE_WHITESPACE = /\s+/;
3
+ const RE_DIGITS = /^\d+$/;
1
4
  const STOPWORDS = /* @__PURE__ */ new Set([
2
5
  // Articles, pronouns, prepositions
3
6
  "a",
@@ -271,9 +274,10 @@ export function extractKeywords(text, metaKeywords, max = 10) {
271
274
  }
272
275
  if (!text?.trim())
273
276
  return [];
274
- const words = text.toLowerCase().replace(/[^\w\s-]/g, " ").split(/\s+/).filter((w) => w.length > 2 && !STOPWORDS.has(w) && !/^\d+$/.test(w));
277
+ const words = text.toLowerCase().replace(RE_NON_WORD_CHARS, " ").split(RE_WHITESPACE).filter((w) => w.length > 2 && !STOPWORDS.has(w) && !RE_DIGITS.test(w));
275
278
  const freq = /* @__PURE__ */ new Map();
276
279
  for (const word of words)
277
280
  freq.set(word, (freq.get(word) || 0) + 1);
278
- return Array.from(freq.entries()).sort((a, b) => b[1] - a[1]).slice(0, max).map(([word]) => word);
281
+ const entries = [...freq];
282
+ return entries.sort((a, b) => b[1] - a[1]).slice(0, max).map(([word]) => word);
279
283
  }
@@ -1,8 +1,11 @@
1
1
  import { normalizeLlmsTxtConfig } from "../../llms-txt-format.js";
2
+ const RE_TRAILING_SLASH = /\/$/;
3
+ const RE_FRONTMATTER = /^---\n[\s\S]*?\n---\n*/;
4
+ const RE_HEADING = /^(#{1,6}) ([^\n]+)$/gm;
2
5
  export function formatPageForLlmsFullTxt(route, title, description, markdown, siteUrl) {
3
- const canonicalUrl = siteUrl ? `${siteUrl.replace(/\/$/, "")}${route}` : route;
6
+ const canonicalUrl = siteUrl ? `${siteUrl.replace(RE_TRAILING_SLASH, "")}${route}` : route;
4
7
  const heading = title && title !== route ? `### ${title}` : `### ${route}`;
5
- const content = markdown.replace(/^---\n[\s\S]*?\n---\n*/, "").replace(/^(#{1,6}) ([^\n]+)$/gm, (_, hashes, text) => `h${hashes.length}. ${text}`);
8
+ const content = markdown.replace(RE_FRONTMATTER, "").replace(RE_HEADING, (_, hashes, text) => `h${hashes.length}. ${text}`);
6
9
  const parts = [heading, ""];
7
10
  parts.push(`Source: ${canonicalUrl}`);
8
11
  if (description)
@@ -4,6 +4,7 @@ export declare function getMarkdownRenderInfo(event: H3Event, explicitOnly?: boo
4
4
  path: string;
5
5
  isExplicit: boolean;
6
6
  } | null;
7
+ export declare function clientPrefersMarkdown(event: H3Event): boolean;
7
8
  interface ConvertHtmlOptions {
8
9
  /** Extract updatedAt from meta tags */
9
10
  extractUpdatedAt?: boolean;
@@ -1,11 +1,13 @@
1
1
  import { getBotInfo } from "@nuxtjs/robots/util";
2
2
  import { getHeader, getHeaders } from "h3";
3
3
  import { htmlToMarkdown } from "mdream";
4
+ import { shouldServeMarkdown } from "mdream/negotiate";
4
5
  import { extractionPlugin } from "mdream/plugins";
5
6
  import { withMinimalPreset } from "mdream/preset/minimal";
6
7
  import { useNitroApp } from "nitropack/runtime";
8
+ const RE_NBSP = /\u00A0/g;
7
9
  function normalizeWhitespace(text) {
8
- return text.replace(/\u00A0/g, " ");
10
+ return text.replace(RE_NBSP, " ");
9
11
  }
10
12
  function buildMdreamOptions(url, mdreamOptions, meta, extractUpdatedAt = false) {
11
13
  const extractPlugin = extractionPlugin({
@@ -63,28 +65,22 @@ export function getMarkdownRenderInfo(event, explicitOnly = false) {
63
65
  return null;
64
66
  }
65
67
  let path = isExplicit ? originalPath.slice(0, -3) : originalPath;
66
- if (path === "/index") {
67
- path = "/";
68
+ if (path.endsWith("/index")) {
69
+ path = path.slice(0, -5) || "/";
68
70
  }
69
71
  return { path, isExplicit };
70
72
  }
71
- function clientPrefersMarkdown(event) {
72
- const accept = getHeader(event, "accept") || "";
73
+ export function clientPrefersMarkdown(event) {
74
+ const acceptHeader = getHeader(event, "accept") || "";
73
75
  const secFetchDest = getHeader(event, "sec-fetch-dest") || "";
74
76
  if (secFetchDest === "document") {
75
77
  return false;
76
78
  }
77
- if (accept.includes("text/html")) {
78
- return false;
79
- }
80
- if (accept.includes("text/markdown")) {
81
- return true;
82
- }
83
79
  const botInfo = getBotInfo(getHeaders(event));
84
80
  if (botInfo?.category === "ai") {
85
81
  return true;
86
82
  }
87
- return false;
83
+ return shouldServeMarkdown(acceptHeader, secFetchDest);
88
84
  }
89
85
  export async function convertHtmlToMarkdown(html, url, mdreamOptions, opts = {}) {
90
86
  const meta = { title: "", description: "", metaKeywords: "", headings: [], textContent: [] };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "nuxt-ai-ready",
3
3
  "type": "module",
4
- "version": "0.12.0",
4
+ "version": "0.12.1",
5
5
  "description": "Best practice AI & LLM discoverability for Nuxt sites.",
6
6
  "author": {
7
7
  "name": "Harlan Wilton",
@@ -57,13 +57,13 @@
57
57
  }
58
58
  },
59
59
  "dependencies": {
60
- "@clack/prompts": "^1.0.1",
60
+ "@clack/prompts": "^1.1.0",
61
61
  "@nuxt/kit": "^4.3.1",
62
62
  "citty": "^0.2.1",
63
63
  "consola": "^3.4.2",
64
64
  "defu": "^6.1.4",
65
65
  "drizzle-orm": "^0.45.1",
66
- "mdream": "^0.16.0",
66
+ "mdream": "^0.17.0",
67
67
  "nuxt-site-config": "^3.2.21",
68
68
  "ofetch": "^1.5.1",
69
69
  "pathe": "^2.0.3",
@@ -72,31 +72,32 @@
72
72
  "ufo": "^1.6.3"
73
73
  },
74
74
  "devDependencies": {
75
- "@antfu/eslint-config": "^7.6.1",
75
+ "@antfu/eslint-config": "^7.7.0",
76
76
  "@arethetypeswrong/cli": "^0.18.2",
77
77
  "@headlessui/vue": "^1.7.23",
78
78
  "@libsql/client": "^0.17.0",
79
79
  "@nuxt/content": "^3.12.0",
80
- "@nuxt/devtools-ui-kit": "^3.2.2",
80
+ "@nuxt/devtools-ui-kit": "^3.2.3",
81
81
  "@nuxt/module-builder": "^1.0.2",
82
82
  "@nuxt/test-utils": "^4.0.0",
83
83
  "@nuxtjs/color-mode": "^4.0.0",
84
84
  "@nuxtjs/eslint-config-typescript": "^12.1.0",
85
85
  "@nuxtjs/i18n": "^10.2.3",
86
86
  "@nuxtjs/mcp-toolkit": "^0.7.0",
87
- "@nuxtjs/robots": "^5.7.0",
87
+ "@nuxtjs/robots": "^5.7.1",
88
88
  "@nuxtjs/sitemap": "^7.6.0",
89
89
  "@types/better-sqlite3": "^7.6.13",
90
90
  "@vitest/coverage-v8": "^4.0.18",
91
91
  "@vue/test-utils": "^2.4.6",
92
92
  "@vueuse/nuxt": "^14.2.1",
93
- "agents": "^0.6.0",
94
- "ai": "^6.0.104",
93
+ "agents": "^0.7.5",
94
+ "ai": "^6.0.116",
95
95
  "better-sqlite3": "^12.6.2",
96
96
  "bumpp": "^10.4.1",
97
- "eslint": "^10.0.2",
97
+ "eslint": "^10.0.3",
98
+ "eslint-plugin-harlanzw": "^0.5.15",
98
99
  "execa": "^9.6.1",
99
- "happy-dom": "^20.7.0",
100
+ "happy-dom": "^20.8.3",
100
101
  "nuxt": "^4.3.1",
101
102
  "nuxt-site-config": "^3.2.21",
102
103
  "playwright": "^1.58.2",
@@ -104,10 +105,10 @@
104
105
  "postgres": "^3.4.8",
105
106
  "typescript": "^5.9.3",
106
107
  "vitest": "^4.0.18",
107
- "vue": "^3.5.29",
108
+ "vue": "^3.5.30",
108
109
  "vue-router": "^5.0.3",
109
110
  "vue-tsc": "^3.2.5",
110
- "wrangler": "^4.69.0",
111
+ "wrangler": "^4.71.0",
111
112
  "zod": "^4.3.6"
112
113
  },
113
114
  "resolutions": {