nuxt-ai-ready 0.12.0 → 0.12.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.
- package/README.md +1 -1
- package/dist/module.json +1 -1
- package/dist/module.mjs +6 -3
- package/dist/runtime/server/db/drizzle/queries.js +3 -1
- package/dist/runtime/server/db/drizzle/raw.js +3 -2
- package/dist/runtime/server/db/queries.js +4 -2
- package/dist/runtime/server/db/shared.js +4 -2
- package/dist/runtime/server/middleware/markdown.js +1 -0
- package/dist/runtime/server/utils/indexnow-shared.js +1 -1
- package/dist/runtime/server/utils/keywords.js +6 -2
- package/dist/runtime/server/utils/llms-full.js +5 -2
- package/dist/runtime/server/utils.d.ts +1 -0
- package/dist/runtime/server/utils.js +8 -12
- package/package.json +13 -12
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 answer
|
|
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
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
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
218
|
+
const sanitized = query.replace(RE_FTS_CHARS, " ").trim();
|
|
217
219
|
if (!sanitized)
|
|
218
220
|
return [];
|
|
219
|
-
const terms = sanitized.split(
|
|
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 =
|
|
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(
|
|
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);
|
|
@@ -71,6 +71,7 @@ export default defineEventHandler(async (event) => {
|
|
|
71
71
|
{ hooks: { route: path, event } }
|
|
72
72
|
);
|
|
73
73
|
setHeader(event, "content-type", "text/markdown; charset=utf-8");
|
|
74
|
+
setHeader(event, "vary", "Accept, Sec-Fetch-Dest");
|
|
74
75
|
if (config.markdownCacheHeaders) {
|
|
75
76
|
const { maxAge, swr } = config.markdownCacheHeaders;
|
|
76
77
|
const cacheControl = swr ? `public, max-age=${maxAge}, stale-while-revalidate=${maxAge}` : `public, max-age=${maxAge}`;
|
|
@@ -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
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
4
|
+
"version": "0.12.2",
|
|
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
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
94
|
-
"ai": "^6.0.
|
|
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.
|
|
97
|
+
"eslint": "^10.0.3",
|
|
98
|
+
"eslint-plugin-harlanzw": "^0.5.15",
|
|
98
99
|
"execa": "^9.6.1",
|
|
99
|
-
"happy-dom": "^20.
|
|
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.
|
|
108
|
+
"vue": "^3.5.30",
|
|
108
109
|
"vue-router": "^5.0.3",
|
|
109
110
|
"vue-tsc": "^3.2.5",
|
|
110
|
-
"wrangler": "^4.
|
|
111
|
+
"wrangler": "^4.71.0",
|
|
111
112
|
"zod": "^4.3.6"
|
|
112
113
|
},
|
|
113
114
|
"resolutions": {
|