nuxt-ai-ready 1.5.1 → 1.5.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.
package/dist/module.json
CHANGED
package/dist/module.mjs
CHANGED
|
@@ -672,7 +672,7 @@ const module$1 = defineNuxtModule({
|
|
|
672
672
|
const contentVersion = await resolveNuxtContentVersion();
|
|
673
673
|
const hasNuxtContentV3 = !!(contentVersion && contentVersion.version === 3);
|
|
674
674
|
if (typeof config.contentSignal === "object") {
|
|
675
|
-
const robotsOpts = nuxt.options.robots
|
|
675
|
+
const robotsOpts = nuxt.options.robots || {};
|
|
676
676
|
nuxt.options.robots = robotsOpts;
|
|
677
677
|
const groups = robotsOpts.groups || [];
|
|
678
678
|
robotsOpts.groups = groups;
|
|
@@ -315,6 +315,12 @@ export declare function syncSitemaps(event: H3Event | undefined, sitemaps: Array
|
|
|
315
315
|
* Skips sitemaps crawled within minIntervalMinutes (default 5 min)
|
|
316
316
|
*/
|
|
317
317
|
export declare function getNextSitemapToCrawl(event: H3Event | undefined, minIntervalMinutes?: number): Promise<SitemapEntry | null>;
|
|
318
|
+
/**
|
|
319
|
+
* Get the last crawl timestamp for a single sitemap. Used to throttle runtime
|
|
320
|
+
* re-seeding (the sitemap:resolved hook fires on every sitemap request).
|
|
321
|
+
* Returns null when the sitemap row doesn't exist yet.
|
|
322
|
+
*/
|
|
323
|
+
export declare function getSitemapLastCrawledAt(event: H3Event | undefined, name: string): Promise<number | null>;
|
|
318
324
|
/**
|
|
319
325
|
* Mark sitemap as successfully crawled
|
|
320
326
|
*/
|
|
@@ -301,22 +301,31 @@ export async function getPageHash(event, route) {
|
|
|
301
301
|
}
|
|
302
302
|
export async function seedRoutes(event, routes) {
|
|
303
303
|
const db = await getDb(event);
|
|
304
|
-
if (!db)
|
|
304
|
+
if (!db || routes.length === 0)
|
|
305
305
|
return 0;
|
|
306
306
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
307
307
|
const nowMs = Date.now();
|
|
308
|
+
const byRoute = /* @__PURE__ */ new Map();
|
|
308
309
|
for (const entry of routes) {
|
|
309
310
|
const route = typeof entry === "string" ? entry : entry.route;
|
|
310
311
|
const explicitLocale = typeof entry === "string" ? void 0 : entry.locale;
|
|
311
|
-
|
|
312
|
-
|
|
312
|
+
byRoute.set(route, {
|
|
313
|
+
route,
|
|
314
|
+
routeKey: normalizeRouteKey(route),
|
|
315
|
+
locale: deriveLocale(event, route, explicitLocale)
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
const ROWS_PER_INSERT = 20;
|
|
319
|
+
for (const batch of chunk([...byRoute.values()], ROWS_PER_INSERT)) {
|
|
320
|
+
const valuesSql = batch.map(() => `(?, ?, '', '', '', '[]', '[]', ?, 0, 0, 0, 'runtime', ?, ?)`).join(", ");
|
|
321
|
+
const params = batch.flatMap((r) => [r.route, r.routeKey, now, nowMs, r.locale]);
|
|
313
322
|
await db.exec(`
|
|
314
323
|
INSERT INTO ai_ready_pages (route, route_key, title, description, markdown, headings, keywords, updated_at, indexed_at, is_error, indexed, source, last_seen_at, locale)
|
|
315
|
-
VALUES
|
|
324
|
+
VALUES ${valuesSql}
|
|
316
325
|
ON CONFLICT(route) DO UPDATE SET last_seen_at = excluded.last_seen_at, locale = excluded.locale
|
|
317
|
-
`,
|
|
326
|
+
`, params);
|
|
318
327
|
}
|
|
319
|
-
return
|
|
328
|
+
return byRoute.size;
|
|
320
329
|
}
|
|
321
330
|
export async function getSitemapSeededAt(event) {
|
|
322
331
|
const db = await getDb(event);
|
|
@@ -732,6 +741,16 @@ export async function getNextSitemapToCrawl(event, minIntervalMinutes = 5) {
|
|
|
732
741
|
`, [threshold]);
|
|
733
742
|
return row ? rowToSitemapEntry(row) : null;
|
|
734
743
|
}
|
|
744
|
+
export async function getSitemapLastCrawledAt(event, name) {
|
|
745
|
+
const db = await getDb(event);
|
|
746
|
+
if (!db)
|
|
747
|
+
return null;
|
|
748
|
+
const row = await db.first(
|
|
749
|
+
"SELECT last_crawled_at FROM ai_ready_sitemaps WHERE name = ?",
|
|
750
|
+
[name]
|
|
751
|
+
);
|
|
752
|
+
return row?.last_crawled_at ?? null;
|
|
753
|
+
}
|
|
735
754
|
export async function markSitemapCrawled(event, name, urlCount) {
|
|
736
755
|
const db = await getDb(event);
|
|
737
756
|
if (!db)
|
|
@@ -1,5 +1,58 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { useRuntimeConfig } from "nitropack/runtime";
|
|
2
|
+
import { getPageLastmods, getSitemapLastCrawledAt, markSitemapCrawled, seedRoutes } from "../db/queries.js";
|
|
2
3
|
import { logger } from "../logger.js";
|
|
4
|
+
const DIAGNOSTICS_KEY = "_aiReadySitemapDiagnostics";
|
|
5
|
+
function isDebug(event) {
|
|
6
|
+
return !!useRuntimeConfig(event)["nuxt-ai-ready"]?.debug;
|
|
7
|
+
}
|
|
8
|
+
function recordDiagnostic(event, message) {
|
|
9
|
+
const ctx = event.context;
|
|
10
|
+
const list = ctx[DIAGNOSTICS_KEY] ??= [];
|
|
11
|
+
list.push(message);
|
|
12
|
+
}
|
|
13
|
+
const SEED_INTERVAL_MS = 5 * 60 * 1e3;
|
|
14
|
+
const READ_TIMEOUT_MS = 3e3;
|
|
15
|
+
const SLOW_READ_WARN_MS = 1e3;
|
|
16
|
+
const SLOW_SEED_WARN_MS = 1e4;
|
|
17
|
+
function withTimeout(promise, ms, label, fallback, onIssue) {
|
|
18
|
+
return new Promise((resolve) => {
|
|
19
|
+
let settled = false;
|
|
20
|
+
const timer = setTimeout(() => {
|
|
21
|
+
if (settled)
|
|
22
|
+
return;
|
|
23
|
+
settled = true;
|
|
24
|
+
const message = `${label} timed out after ${ms}ms`;
|
|
25
|
+
logger.warn(`[sitemap-seeder] ${message}`);
|
|
26
|
+
onIssue?.(message);
|
|
27
|
+
resolve(fallback);
|
|
28
|
+
}, ms);
|
|
29
|
+
const done = (value) => {
|
|
30
|
+
if (settled)
|
|
31
|
+
return;
|
|
32
|
+
settled = true;
|
|
33
|
+
clearTimeout(timer);
|
|
34
|
+
resolve(value);
|
|
35
|
+
};
|
|
36
|
+
promise.then(done, (e) => {
|
|
37
|
+
const message = `${label} failed: ${e?.message}`;
|
|
38
|
+
logger.warn(`[sitemap-seeder] ${message}`);
|
|
39
|
+
onIssue?.(message);
|
|
40
|
+
done(fallback);
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
function toXmlComment(messages) {
|
|
45
|
+
const body = messages.map((m) => ` - ${m.replace(/-{2,}/g, "- ")}`).join("\n");
|
|
46
|
+
return `<!-- nuxt-ai-ready sitemap-seeder diagnostics:
|
|
47
|
+
${body}
|
|
48
|
+
-->
|
|
49
|
+
`;
|
|
50
|
+
}
|
|
51
|
+
function injectComment(xml, comment) {
|
|
52
|
+
const prolog = xml.match(/^\s*(?:<\?[^>]*\?>\s*)*/);
|
|
53
|
+
const idx = prolog ? prolog[0].length : 0;
|
|
54
|
+
return xml.slice(0, idx) + comment + xml.slice(idx);
|
|
55
|
+
}
|
|
3
56
|
export default function sitemapSeederPlugin(nitroApp) {
|
|
4
57
|
nitroApp.hooks.hook("sitemap:resolved", async (ctx) => {
|
|
5
58
|
if (import.meta.dev)
|
|
@@ -7,26 +60,18 @@ export default function sitemapSeederPlugin(nitroApp) {
|
|
|
7
60
|
const { urls, sitemapName, event } = ctx;
|
|
8
61
|
if (urls.length === 0)
|
|
9
62
|
return;
|
|
10
|
-
logger.debug(`[sitemap-seeder] Processing ${urls.length} routes from ${sitemapName}`);
|
|
11
|
-
const routes = [];
|
|
12
63
|
const routeToUrl = /* @__PURE__ */ new Map();
|
|
13
64
|
for (const u of urls) {
|
|
14
|
-
|
|
15
|
-
if (
|
|
16
|
-
route = u._path.pathname;
|
|
17
|
-
} else {
|
|
18
|
-
const loc = u.loc;
|
|
19
|
-
route = loc.startsWith("/") ? loc.split("?")[0] ?? loc : new URL(loc).pathname;
|
|
20
|
-
}
|
|
21
|
-
if (!route.includes(".")) {
|
|
22
|
-
routes.push(route);
|
|
65
|
+
const route = u._path?.pathname ?? (u.loc.startsWith("/") ? u.loc.split("?")[0] ?? u.loc : new URL(u.loc).pathname);
|
|
66
|
+
if (!route.includes("."))
|
|
23
67
|
routeToUrl.set(route, u);
|
|
24
|
-
}
|
|
25
68
|
}
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
69
|
+
if (routeToUrl.size === 0)
|
|
70
|
+
return;
|
|
71
|
+
logger.debug(`[sitemap-seeder] Processing ${routeToUrl.size} routes from ${sitemapName}`);
|
|
72
|
+
const record = isDebug(event) ? (m) => recordDiagnostic(event, m) : void 0;
|
|
73
|
+
const readStart = Date.now();
|
|
74
|
+
const lastmods = await withTimeout(getPageLastmods(event), READ_TIMEOUT_MS, "getPageLastmods", /* @__PURE__ */ new Map(), record);
|
|
30
75
|
let enriched = 0;
|
|
31
76
|
for (const [route, url] of routeToUrl) {
|
|
32
77
|
const lastmod = lastmods.get(route);
|
|
@@ -38,15 +83,44 @@ export default function sitemapSeederPlugin(nitroApp) {
|
|
|
38
83
|
if (enriched > 0) {
|
|
39
84
|
logger.debug(`[sitemap-seeder] Enriched ${enriched} URLs with lastmod`);
|
|
40
85
|
}
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
});
|
|
48
|
-
if (seeded > 0) {
|
|
49
|
-
logger.debug(`[sitemap-seeder] Seeded ${seeded} new routes from ${sitemapName}`);
|
|
86
|
+
const lastCrawled = await withTimeout(getSitemapLastCrawledAt(event, sitemapName), READ_TIMEOUT_MS, "getSitemapLastCrawledAt", null, record);
|
|
87
|
+
const readMs = Date.now() - readStart;
|
|
88
|
+
if (readMs >= SLOW_READ_WARN_MS) {
|
|
89
|
+
const message = `slow DB reads: ${readMs}ms (timeout ${READ_TIMEOUT_MS}ms)`;
|
|
90
|
+
logger.warn(`[sitemap-seeder] ${message} for ${sitemapName}`);
|
|
91
|
+
record?.(message);
|
|
50
92
|
}
|
|
93
|
+
if (lastCrawled && Date.now() - lastCrawled < SEED_INTERVAL_MS)
|
|
94
|
+
return;
|
|
95
|
+
const routes = [...routeToUrl.keys()];
|
|
96
|
+
const urlCount = urls.length;
|
|
97
|
+
const seed = async () => {
|
|
98
|
+
const seedStart = Date.now();
|
|
99
|
+
const seeded = await seedRoutes(event, routes).catch((e) => {
|
|
100
|
+
logger.warn(`[sitemap-seeder] Failed to seed routes: ${e.message}`);
|
|
101
|
+
return 0;
|
|
102
|
+
});
|
|
103
|
+
await markSitemapCrawled(event, sitemapName, urlCount).catch((e) => {
|
|
104
|
+
logger.warn(`[sitemap-seeder] Failed to mark sitemap: ${e.message}`);
|
|
105
|
+
});
|
|
106
|
+
const seedMs = Date.now() - seedStart;
|
|
107
|
+
if (seedMs >= SLOW_SEED_WARN_MS)
|
|
108
|
+
logger.warn(`[sitemap-seeder] Slow seed: ${seedMs}ms for ${seeded} routes in ${sitemapName}`);
|
|
109
|
+
else if (seeded > 0)
|
|
110
|
+
logger.debug(`[sitemap-seeder] Seeded ${seeded} routes from ${sitemapName} in ${seedMs}ms`);
|
|
111
|
+
};
|
|
112
|
+
if (event.waitUntil) {
|
|
113
|
+
event.waitUntil(seed().catch(
|
|
114
|
+
(err) => logger.error(`[sitemap-seeder] Background seed failed: ${err.message}`)
|
|
115
|
+
));
|
|
116
|
+
} else {
|
|
117
|
+
await seed();
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
nitroApp.hooks.hook("sitemap:output", (ctx) => {
|
|
121
|
+
const diagnostics = ctx.event.context[DIAGNOSTICS_KEY];
|
|
122
|
+
if (!diagnostics?.length)
|
|
123
|
+
return;
|
|
124
|
+
ctx.sitemap = injectComment(ctx.sitemap, toXmlComment(diagnostics));
|
|
51
125
|
});
|
|
52
126
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nuxt-ai-ready",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "1.5.
|
|
4
|
+
"version": "1.5.3",
|
|
5
5
|
"description": "Best practice AI & LLM discoverability for Nuxt sites.",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "Harlan Wilton",
|
|
@@ -81,15 +81,15 @@
|
|
|
81
81
|
"@nuxt/test-utils": "^4.0.3",
|
|
82
82
|
"@nuxtjs/eslint-config-typescript": "^12.1.0",
|
|
83
83
|
"@nuxtjs/mcp-toolkit": "^0.17.2",
|
|
84
|
-
"@nuxtjs/robots": "^6.1.
|
|
85
|
-
"@nuxtjs/sitemap": "^8.2.
|
|
84
|
+
"@nuxtjs/robots": "^6.1.2",
|
|
85
|
+
"@nuxtjs/sitemap": "^8.2.2",
|
|
86
86
|
"@types/better-sqlite3": "^7.6.13",
|
|
87
87
|
"@vitest/coverage-v8": "^4.1.9",
|
|
88
88
|
"@vue/test-utils": "^2.4.11",
|
|
89
89
|
"@vueuse/nuxt": "^14.3.0",
|
|
90
90
|
"better-sqlite3": "^12.11.1",
|
|
91
91
|
"bumpp": "^11.1.0",
|
|
92
|
-
"eslint": "^10.
|
|
92
|
+
"eslint": "^10.6.0",
|
|
93
93
|
"eslint-plugin-harlanzw": "^0.17.0",
|
|
94
94
|
"execa": "^9.6.1",
|
|
95
95
|
"happy-dom": "^20.10.6",
|
|
@@ -103,10 +103,10 @@
|
|
|
103
103
|
"typescript": "^6.0.3",
|
|
104
104
|
"unbuild": "^3.6.1",
|
|
105
105
|
"vitest": "^4.1.9",
|
|
106
|
-
"vue": "^3.5.
|
|
106
|
+
"vue": "^3.5.39",
|
|
107
107
|
"vue-router": "^5.1.0",
|
|
108
108
|
"vue-tsc": "^3.3.5",
|
|
109
|
-
"wrangler": "^4.
|
|
109
|
+
"wrangler": "^4.105.0",
|
|
110
110
|
"zod": "^4.4.3"
|
|
111
111
|
},
|
|
112
112
|
"scripts": {
|