nuxt-ai-ready 0.6.2 → 0.7.0

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.
Files changed (69) hide show
  1. package/README.md +1 -0
  2. package/dist/module.d.mts +12 -1
  3. package/dist/module.json +1 -1
  4. package/dist/module.mjs +226 -352
  5. package/dist/runtime/index.d.ts +5 -7
  6. package/dist/runtime/index.js +14 -3
  7. package/dist/runtime/llms-txt-format.d.ts +8 -0
  8. package/dist/runtime/llms-txt-format.js +32 -0
  9. package/dist/runtime/llms-txt-utils.d.ts +2 -5
  10. package/dist/runtime/llms-txt-utils.js +31 -88
  11. package/dist/runtime/server/db/index.d.ts +3 -7
  12. package/dist/runtime/server/db/index.js +14 -31
  13. package/dist/runtime/server/db/queries.d.ts +106 -26
  14. package/dist/runtime/server/db/queries.js +330 -33
  15. package/dist/runtime/server/db/schema-sql.d.ts +3 -0
  16. package/dist/runtime/server/db/schema-sql.js +71 -0
  17. package/dist/runtime/server/db/shared.d.ts +88 -0
  18. package/dist/runtime/server/db/shared.js +117 -0
  19. package/dist/runtime/server/mcp/resources/pages.js +11 -3
  20. package/dist/runtime/server/mcp/tools/list-pages.js +33 -7
  21. package/dist/runtime/server/mcp/tools/search-pages.js +3 -3
  22. package/dist/runtime/server/middleware/markdown.js +2 -3
  23. package/dist/runtime/server/middleware/markdown.prerender.js +6 -5
  24. package/dist/runtime/server/plugins/db-restore.js +10 -8
  25. package/dist/runtime/server/plugins/sitemap-seeder.js +44 -0
  26. package/dist/runtime/server/routes/__ai-ready/indexnow.post.d.ts +2 -0
  27. package/dist/runtime/server/routes/__ai-ready/indexnow.post.js +18 -0
  28. package/dist/runtime/server/routes/__ai-ready/poll.post.d.ts +8 -0
  29. package/dist/runtime/server/routes/__ai-ready/poll.post.js +25 -0
  30. package/dist/runtime/server/routes/__ai-ready/prune.post.d.ts +14 -0
  31. package/dist/runtime/server/routes/__ai-ready/prune.post.js +21 -0
  32. package/dist/runtime/server/routes/__ai-ready/status.get.d.ts +2 -0
  33. package/dist/runtime/server/routes/__ai-ready/status.get.js +28 -0
  34. package/dist/runtime/server/routes/__ai-ready-debug.get.js +13 -14
  35. package/dist/runtime/server/routes/indexnow-key.get.d.ts +6 -0
  36. package/dist/runtime/server/routes/indexnow-key.get.js +10 -0
  37. package/dist/runtime/server/routes/llms-full.txt.get.d.ts +1 -1
  38. package/dist/runtime/server/routes/llms-full.txt.get.js +34 -3
  39. package/dist/runtime/server/tasks/ai-ready-index.d.ts +11 -0
  40. package/dist/runtime/server/tasks/ai-ready-index.js +39 -0
  41. package/dist/runtime/server/utils/batchIndex.d.ts +26 -0
  42. package/dist/runtime/server/utils/batchIndex.js +53 -0
  43. package/dist/runtime/server/utils/indexPage.d.ts +8 -2
  44. package/dist/runtime/server/utils/indexPage.js +38 -23
  45. package/dist/runtime/server/utils/indexnow.d.ts +27 -0
  46. package/dist/runtime/server/utils/indexnow.js +66 -0
  47. package/dist/runtime/server/utils/keywords.d.ts +0 -4
  48. package/dist/runtime/server/utils/keywords.js +0 -3
  49. package/dist/runtime/server/utils/llms-full.d.ts +11 -0
  50. package/dist/runtime/server/utils/llms-full.js +39 -0
  51. package/dist/runtime/server/utils/sitemap.js +3 -3
  52. package/dist/runtime/server/utils.d.ts +12 -10
  53. package/dist/runtime/server/utils.js +63 -94
  54. package/dist/runtime/types.d.ts +64 -40
  55. package/package.json +28 -29
  56. package/dist/runtime/mcp.d.ts +0 -32
  57. package/dist/runtime/mcp.js +0 -5
  58. package/dist/runtime/server/db/dump.d.ts +0 -29
  59. package/dist/runtime/server/db/dump.js +0 -29
  60. package/dist/runtime/server/db/schema.d.ts +0 -8
  61. package/dist/runtime/server/db/schema.js +0 -124
  62. package/dist/runtime/server/plugins/page-indexer.js +0 -68
  63. package/dist/runtime/server/tsconfig.json +0 -3
  64. package/dist/runtime/server/utils/pageData.d.ts +0 -28
  65. package/dist/runtime/server/utils/pageData.js +0 -43
  66. package/mcp.d.ts +0 -4
  67. /package/dist/runtime/{nuxt → app}/plugins/md-hints.prerender.d.ts +0 -0
  68. /package/dist/runtime/{nuxt → app}/plugins/md-hints.prerender.js +0 -0
  69. /package/dist/runtime/server/plugins/{page-indexer.d.ts → sitemap-seeder.d.ts} +0 -0
package/dist/module.mjs CHANGED
@@ -1,6 +1,6 @@
1
- import { appendFile, mkdir, writeFile, readFile, stat, access } from 'node:fs/promises';
2
- import { join as join$1, dirname } from 'node:path';
3
- import { useLogger, useNuxt, defineNuxtModule, createResolver, addTypeTemplate, hasNuxtModule, addServerHandler, addPlugin } from '@nuxt/kit';
1
+ import { mkdir, writeFile, appendFile, stat, access } from 'node:fs/promises';
2
+ import { join, dirname } from 'node:path';
3
+ import { useLogger, useNuxt, addTypeTemplate, defineNuxtModule, createResolver, hasNuxtModule, addServerHandler, addPlugin } from '@nuxt/kit';
4
4
  import defu from 'defu';
5
5
  import { useSiteConfig, installNuxtSiteConfig, withSiteUrl } from 'nuxt-site-config/kit';
6
6
  import { readPackageJSON } from 'pkg-types';
@@ -10,7 +10,9 @@ import { isTest, isCI } from 'std-env';
10
10
  import { parseSitemapXml } from '@nuxtjs/sitemap/utils';
11
11
  import { colorize } from 'consola/utils';
12
12
  import { withBase } from 'ufo';
13
- import { join, isAbsolute } from 'pathe';
13
+ import { createAdapter, initSchema, computeContentHash, insertPage, queryAllPages, exportDbDump } from '../dist/runtime/server/db/shared.js';
14
+ import { buildLlmsFullTxtHeader, formatPageForLlmsFullTxt } from '../dist/runtime/server/utils/llms-full.js';
15
+ import { join as join$1, isAbsolute } from 'pathe';
14
16
 
15
17
  const logger = useLogger("nuxt-ai-ready");
16
18
 
@@ -19,7 +21,7 @@ function hookNuxtSeoProLicense() {
19
21
  const isBuild = !nuxt.options.dev && !nuxt.options._prepare;
20
22
  if (isBuild && !nuxt._isNuxtSeoProVerifying) {
21
23
  const license = nuxt.options.runtimeConfig.seoProKey || process.env.NUXT_SEO_PRO_KEY;
22
- if (isTest) {
24
+ if (isTest || process.env.VITEST) {
23
25
  return;
24
26
  }
25
27
  if (!isCI && !license) {
@@ -65,118 +67,28 @@ function hookNuxtSeoProLicense() {
65
67
  }
66
68
  }
67
69
 
68
- async function resolveDatabaseAdapter(type, _opts) {
69
- const connectors = {
70
- d1: "db0/connectors/cloudflare-d1",
71
- libsql: "db0/connectors/libsql/node"
72
- };
73
- if (type !== "sqlite" && connectors[type]) {
74
- return connectors[type];
75
- }
76
- if (process.versions.bun) {
77
- return "db0/connectors/bun-sqlite";
78
- }
79
- const nodeVersion = Number.parseInt(process.versions.node?.split(".")[0] || "0");
80
- if (nodeVersion >= 22) {
81
- return "db0/connectors/node-sqlite";
82
- }
83
- return "db0/connectors/better-sqlite3";
84
- }
85
- function refineDatabaseConfig(config, rootDir) {
86
- const type = config.type || "sqlite";
87
- if (type === "sqlite") {
88
- const filename = config.filename || ".data/ai-ready/pages.db";
89
- return {
90
- type: "sqlite",
91
- filename: isAbsolute(filename) ? filename : join(rootDir, filename)
92
- };
93
- }
94
- if (type === "d1") {
95
- return {
96
- type: "d1",
97
- bindingName: config.bindingName || "AI_READY_DB"
98
- };
99
- }
100
- return {
101
- type: "libsql",
102
- url: config.url,
103
- authToken: config.authToken
104
- };
105
- }
106
-
107
- function normalizeLink(link) {
108
- const parts = [];
109
- parts.push(`- [${link.title}](${link.href})`);
110
- if (link.description)
111
- parts.push(` ${link.description}`);
112
- return parts.join("\n");
113
- }
114
- function normalizeSection(section) {
115
- const parts = [];
116
- parts.push(`## ${section.title}`);
117
- parts.push("");
118
- if (section.description) {
119
- const descriptions = Array.isArray(section.description) ? section.description : [section.description];
120
- parts.push(...descriptions);
121
- parts.push("");
122
- }
123
- if (section.links?.length)
124
- parts.push(...section.links.map(normalizeLink));
125
- return parts.join("\n");
126
- }
127
- function normalizeLlmsTxtConfig(config) {
128
- const parts = [];
129
- if (config.sections?.length)
130
- parts.push(...config.sections.map(normalizeSection));
131
- if (config.notes) {
132
- parts.push("## Notes");
133
- parts.push("");
134
- const notes = Array.isArray(config.notes) ? config.notes : [config.notes];
135
- parts.push(...notes);
136
- }
137
- return parts.join("\n\n");
138
- }
139
- function createCrawlerState(pageDataPath, llmsFullTxtPath, siteInfo, llmsTxtConfig) {
70
+ function createCrawlerState(dbPath, llmsFullTxtPath, siteInfo, llmsTxtConfig) {
140
71
  return {
141
72
  prerenderedRoutes: /* @__PURE__ */ new Set(),
142
73
  errorRoutes: /* @__PURE__ */ new Set(),
143
74
  totalProcessingTime: 0,
144
75
  initialized: false,
145
- jsonlInitialized: false,
146
- pageDataPath,
76
+ dbPath,
147
77
  llmsFullTxtPath,
148
78
  siteInfo,
149
79
  llmsTxtConfig
150
80
  };
151
81
  }
152
- function buildLlmsFullTxtHeader(siteInfo, llmsTxtConfig) {
153
- const parts = [];
154
- parts.push(`# ${siteInfo?.name || siteInfo?.url || "Site"}`);
155
- if (siteInfo?.description)
156
- parts.push(`
157
- > ${siteInfo.description}`);
158
- if (siteInfo?.url)
159
- parts.push(`
160
- Canonical Origin: ${siteInfo.url}`);
161
- parts.push("");
162
- if (llmsTxtConfig) {
163
- const normalizedContent = normalizeLlmsTxtConfig(llmsTxtConfig);
164
- if (normalizedContent) {
165
- parts.push(normalizedContent);
166
- parts.push("");
167
- }
168
- }
169
- parts.push("## Pages\n\n");
170
- return parts.join("\n");
171
- }
172
82
  async function initCrawler(state) {
173
83
  if (state.initialized)
174
84
  return;
175
- if (state.pageDataPath) {
176
- await mkdir(dirname(state.pageDataPath), { recursive: true });
177
- await writeFile(state.pageDataPath, "", "utf-8");
178
- state.jsonlInitialized = true;
179
- logger.debug(`Crawler initialized with JSONL at ${state.pageDataPath}`);
85
+ if (state.dbPath) {
86
+ await mkdir(dirname(state.dbPath), { recursive: true });
87
+ const { default: betterSqlite3 } = await import('db0/connectors/better-sqlite3');
88
+ const connector = betterSqlite3({ path: state.dbPath });
89
+ state.db = createAdapter(connector);
90
+ await initSchema(state.db);
91
+ logger.debug(`Crawler initialized with SQLite at ${state.dbPath}`);
180
92
  }
181
93
  if (state.llmsFullTxtPath) {
182
94
  await mkdir(dirname(state.llmsFullTxtPath), { recursive: true });
@@ -189,34 +101,6 @@ async function initCrawler(state) {
189
101
  function flattenHeadings(headings) {
190
102
  return (headings || []).map((h) => Object.entries(h).map(([tag, text]) => `${tag}:${text}`).join("")).join("|");
191
103
  }
192
- function stripFrontmatter(markdown) {
193
- return markdown.replace(/^---\n[\s\S]*?\n---\n*/, "");
194
- }
195
- function normalizeHeadings(markdown) {
196
- return markdown.replace(/^(#{1,6})\s+(.+)$/gm, (_, hashes, text) => {
197
- const level = hashes.length;
198
- return `h${level}. ${text}`;
199
- });
200
- }
201
- function formatPageForLlmsFullTxt(route, title, description, markdown, siteUrl) {
202
- const canonicalUrl = siteUrl ? `${siteUrl.replace(/\/$/, "")}${route}` : route;
203
- const heading = title && title !== route ? `### ${title}` : `### ${route}`;
204
- let content = stripFrontmatter(markdown);
205
- content = normalizeHeadings(content);
206
- const parts = [heading, ""];
207
- parts.push(`Source: ${canonicalUrl}`);
208
- if (description)
209
- parts.push(`Description: ${description}`);
210
- parts.push("");
211
- if (content.trim()) {
212
- parts.push(content.trim());
213
- parts.push("");
214
- }
215
- parts.push("---");
216
- parts.push("");
217
- return `${parts.join("\n")}
218
- `;
219
- }
220
104
  async function processMarkdownRoute(state, nuxt, route, parsed, lastmod, options) {
221
105
  const { markdown, title, description, headings, keywords, updatedAt: metaUpdatedAt } = parsed;
222
106
  let updatedAt = (lastmod instanceof Date ? lastmod.toISOString() : lastmod) || (/* @__PURE__ */ new Date()).toISOString();
@@ -226,18 +110,22 @@ async function processMarkdownRoute(state, nuxt, route, parsed, lastmod, options
226
110
  updatedAt = parsedDate.toISOString();
227
111
  }
228
112
  await nuxt.hooks.callHook("ai-ready:page:markdown", { route, markdown, title, description, headings });
229
- if (state.jsonlInitialized && state.pageDataPath) {
230
- const pageData = {
113
+ if (state.db) {
114
+ const contentHash = await computeContentHash(markdown);
115
+ await insertPage(state.db, {
231
116
  route,
232
117
  title,
233
118
  description,
119
+ markdown,
234
120
  headings: flattenHeadings(headings),
235
121
  keywords: keywords || [],
236
- updatedAt,
237
- markdown
238
- };
239
- await appendFile(state.pageDataPath, `${JSON.stringify(pageData)}
240
- `, "utf-8");
122
+ contentHash,
123
+ updatedAt
124
+ });
125
+ }
126
+ if (state.llmsFullTxtPath && !options?.skipLlmsFullTxt) {
127
+ const pageContent = formatPageForLlmsFullTxt(route, title, description, markdown, state.siteInfo?.url);
128
+ await appendFile(state.llmsFullTxtPath, pageContent, "utf-8");
241
129
  }
242
130
  state.prerenderedRoutes.add(route);
243
131
  }
@@ -275,7 +163,7 @@ async function crawlSitemapEntries(state, nuxt, nitro, entries) {
275
163
  logger.debug(`Skipping ${route}: Response is not JSON (likely HTML instead of markdown conversion)`, err);
276
164
  continue;
277
165
  }
278
- await processMarkdownRoute(state, nuxt, route, parsed, lastmod);
166
+ await processMarkdownRoute(state, nuxt, route, parsed, lastmod, { skipLlmsFullTxt: true });
279
167
  crawled++;
280
168
  }
281
169
  logger.debug(`Sitemap crawl complete: ${crawled} crawled, ${skipped} skipped`);
@@ -314,92 +202,6 @@ function detectSitemapPrerender(sitemapName = "sitemap.xml") {
314
202
  usePrerenderHook: shouldHookIntoPrerender && !prerenderSitemap
315
203
  };
316
204
  }
317
- async function createDatabaseDump(entries, errorRoutes, dbConfig) {
318
- const Database = (await import('better-sqlite3')).default;
319
- const dbPath = dbConfig.filename || ".data/ai-ready/pages.db";
320
- await mkdir(dirname(dbPath), { recursive: true });
321
- const db = new Database(dbPath);
322
- db.exec(`
323
- CREATE TABLE IF NOT EXISTS pages (
324
- id INTEGER PRIMARY KEY AUTOINCREMENT,
325
- route TEXT UNIQUE NOT NULL,
326
- route_key TEXT UNIQUE NOT NULL,
327
- title TEXT NOT NULL DEFAULT '',
328
- description TEXT NOT NULL DEFAULT '',
329
- markdown TEXT NOT NULL DEFAULT '',
330
- headings TEXT NOT NULL DEFAULT '[]',
331
- keywords TEXT NOT NULL DEFAULT '[]',
332
- updated_at TEXT NOT NULL,
333
- indexed_at INTEGER NOT NULL,
334
- is_error INTEGER NOT NULL DEFAULT 0
335
- );
336
- CREATE INDEX IF NOT EXISTS idx_pages_route ON pages(route);
337
- CREATE INDEX IF NOT EXISTS idx_pages_is_error ON pages(is_error);
338
- CREATE VIRTUAL TABLE IF NOT EXISTS pages_fts USING fts5(
339
- route, title, description, markdown, headings, keywords,
340
- content=pages, content_rowid=id
341
- );
342
- CREATE TRIGGER IF NOT EXISTS pages_ai AFTER INSERT ON pages BEGIN
343
- INSERT INTO pages_fts(rowid, route, title, description, markdown, headings, keywords)
344
- VALUES (new.id, new.route, new.title, new.description, new.markdown, new.headings, new.keywords);
345
- END;
346
- CREATE TRIGGER IF NOT EXISTS pages_ad AFTER DELETE ON pages BEGIN
347
- INSERT INTO pages_fts(pages_fts, rowid, route, title, description, markdown, headings, keywords)
348
- VALUES('delete', old.id, old.route, old.title, old.description, old.markdown, old.headings, old.keywords);
349
- END;
350
- CREATE TRIGGER IF NOT EXISTS pages_au AFTER UPDATE ON pages BEGIN
351
- INSERT INTO pages_fts(pages_fts, rowid, route, title, description, markdown, headings, keywords)
352
- VALUES('delete', old.id, old.route, old.title, old.description, old.markdown, old.headings, old.keywords);
353
- INSERT INTO pages_fts(rowid, route, title, description, markdown, headings, keywords)
354
- VALUES (new.id, new.route, new.title, new.description, new.markdown, new.headings, new.keywords);
355
- END;
356
- `);
357
- const insertStmt = db.prepare(`
358
- INSERT OR REPLACE INTO pages (route, route_key, title, description, markdown, headings, keywords, updated_at, indexed_at, is_error)
359
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
360
- `);
361
- const normalizeRouteKey = (route) => route.replace(/^\//, "").replace(/\//g, ":") || "index";
362
- const now = Date.now();
363
- for (const entry of entries) {
364
- insertStmt.run(
365
- entry.route,
366
- normalizeRouteKey(entry.route),
367
- entry.title,
368
- entry.description,
369
- entry.markdown,
370
- entry.headings,
371
- JSON.stringify(entry.keywords),
372
- entry.updatedAt,
373
- now,
374
- 0
375
- );
376
- }
377
- for (const route of errorRoutes) {
378
- insertStmt.run(
379
- route,
380
- normalizeRouteKey(route),
381
- "",
382
- "",
383
- "",
384
- "[]",
385
- "[]",
386
- (/* @__PURE__ */ new Date()).toISOString(),
387
- now,
388
- 1
389
- );
390
- }
391
- const rows = db.prepare(`
392
- SELECT route, route_key, title, description, markdown, headings, keywords, updated_at, indexed_at, is_error
393
- FROM pages
394
- `).all();
395
- db.close();
396
- const json = JSON.stringify(rows);
397
- const encoder = new TextEncoder();
398
- const stream = new Blob([encoder.encode(json)]).stream();
399
- const compressed = stream.pipeThrough(new CompressionStream("gzip"));
400
- const buffer = await new Response(compressed).arrayBuffer();
401
- return Buffer.from(buffer).toString("base64");
402
- }
403
205
  async function prerenderRoute(nitro, route) {
404
206
  const start = Date.now();
405
207
  const encodedRoute = encodeURI(route);
@@ -409,7 +211,7 @@ async function prerenderRoute(nitro, route) {
409
211
  retry: nitro.options.prerender.retry,
410
212
  retryDelay: nitro.options.prerender.retryDelay
411
213
  });
412
- const filePath = join$1(nitro.options.output.publicDir, route);
214
+ const filePath = join(nitro.options.output.publicDir, route);
413
215
  await mkdir(dirname(filePath), { recursive: true });
414
216
  const data = res._data;
415
217
  if (data === void 0)
@@ -423,12 +225,11 @@ async function prerenderRoute(nitro, route) {
423
225
  nitro._prerenderedRoutes.push(_route);
424
226
  return stat(filePath);
425
227
  }
426
- function setupPrerenderHandler(pageDataPath, siteInfo, llmsTxtConfig) {
228
+ function setupPrerenderHandler(dbPath, siteInfo, llmsTxtConfig) {
427
229
  const nuxt = useNuxt();
428
- const dbConfig = refineDatabaseConfig({}, nuxt.options.rootDir);
429
230
  nuxt.hooks.hook("nitro:init", async (nitro) => {
430
- const llmsFullTxtPath = join$1(nitro.options.output.publicDir, "llms-full.txt");
431
- const state = createCrawlerState(pageDataPath, llmsFullTxtPath, siteInfo, llmsTxtConfig);
231
+ const llmsFullTxtPath = join(nitro.options.output.publicDir, "llms-full.txt");
232
+ const state = createCrawlerState(dbPath, llmsFullTxtPath, siteInfo, llmsTxtConfig);
432
233
  let initPromise = null;
433
234
  nitro.hooks.hook("prerender:generate", async (route) => {
434
235
  if (route.error) {
@@ -447,76 +248,49 @@ function setupPrerenderHandler(pageDataPath, siteInfo, llmsTxtConfig) {
447
248
  initPromise = initCrawler(state);
448
249
  await initPromise;
449
250
  const parsed = JSON.parse(route.contents || "{}");
450
- const { markdown, title, description, headings, keywords, updatedAt: metaUpdatedAt } = parsed;
451
- let updatedAt = (/* @__PURE__ */ new Date()).toISOString();
452
- if (metaUpdatedAt) {
453
- const parsedDate = new Date(metaUpdatedAt);
454
- if (!Number.isNaN(parsedDate.getTime()))
455
- updatedAt = parsedDate.toISOString();
456
- }
457
- await nuxt.hooks.callHook("ai-ready:page:markdown", {
458
- route: pageRoute,
459
- markdown,
460
- title,
461
- description,
462
- headings
463
- });
464
- if (state.jsonlInitialized && state.pageDataPath) {
465
- const pageData = {
466
- route: pageRoute,
467
- title,
468
- description,
469
- headings: flattenHeadings(headings),
470
- keywords: keywords || [],
471
- updatedAt,
472
- markdown
473
- };
474
- await appendFile(state.pageDataPath, `${JSON.stringify(pageData)}
475
- `, "utf-8");
476
- }
477
- if (state.llmsFullTxtPath) {
478
- const pageContent = formatPageForLlmsFullTxt(pageRoute, title, description, markdown, state.siteInfo?.url);
479
- await appendFile(state.llmsFullTxtPath, pageContent, "utf-8");
480
- }
481
- state.prerenderedRoutes.add(pageRoute);
482
- route.contents = markdown;
251
+ await processMarkdownRoute(state, nuxt, pageRoute, parsed);
252
+ route.contents = parsed.markdown;
483
253
  state.totalProcessingTime += Date.now() - pageStartTime;
484
254
  });
485
255
  async function writeLlmsFiles() {
486
- if (state.pageDataPath && state.errorRoutes.size > 0) {
256
+ if (state.db && state.errorRoutes.size > 0) {
487
257
  for (const route of state.errorRoutes) {
488
- await appendFile(state.pageDataPath, `${JSON.stringify({ route, _error: true })}
489
- `, "utf-8");
258
+ await insertPage(state.db, {
259
+ route,
260
+ title: "",
261
+ description: "",
262
+ markdown: "",
263
+ headings: "",
264
+ keywords: [],
265
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
266
+ isError: true
267
+ });
490
268
  }
491
- logger.debug(`Wrote ${state.errorRoutes.size} error routes to page data`);
269
+ logger.debug(`Wrote ${state.errorRoutes.size} error routes to database`);
492
270
  }
493
- const publicDataDir = join$1(nitro.options.output.publicDir, "__ai-ready");
271
+ const publicDataDir = join(nitro.options.output.publicDir, "__ai-ready");
494
272
  await mkdir(publicDataDir, { recursive: true });
495
- if (state.pageDataPath) {
496
- const jsonlContent = await readFile(state.pageDataPath, "utf-8").catch(() => "");
497
- if (jsonlContent) {
498
- const entries = jsonlContent.trim().split("\n").filter(Boolean).map((line) => JSON.parse(line));
499
- const pages = entries.filter((e) => !e._error);
500
- const errorRoutesList = entries.filter((e) => e._error).map((e) => e.route);
501
- const jsonContent = JSON.stringify({
502
- pages: pages.map((p) => ({
503
- route: p.route,
504
- title: p.title,
505
- description: p.description,
506
- headings: p.headings,
507
- keywords: p.keywords || [],
508
- updatedAt: p.updatedAt
509
- })),
510
- errorRoutes: errorRoutesList
511
- });
512
- const publicJsonPath = join$1(publicDataDir, "pages.json");
513
- await writeFile(publicJsonPath, jsonContent, "utf-8");
514
- logger.debug(`Wrote ${pages.length} pages to __ai-ready/pages.json`);
515
- const dumpData = await createDatabaseDump(pages, errorRoutesList, dbConfig);
516
- const dumpPath = join$1(publicDataDir, "pages.dump");
517
- await writeFile(dumpPath, dumpData, "utf-8");
518
- logger.debug(`Created database dump at __ai-ready/pages.dump (${(dumpData.length / 1024).toFixed(1)}kb compressed)`);
519
- }
273
+ if (state.db) {
274
+ const pages = await queryAllPages(state.db);
275
+ const errorRoutesList = (await queryAllPages(state.db, { includeErrors: true })).filter((p) => p.isError).map((p) => p.route);
276
+ const jsonContent = JSON.stringify({
277
+ pages: pages.map((p) => ({
278
+ route: p.route,
279
+ title: p.title,
280
+ description: p.description,
281
+ headings: p.headings,
282
+ keywords: p.keywords || [],
283
+ updatedAt: p.updatedAt
284
+ })),
285
+ errorRoutes: errorRoutesList
286
+ });
287
+ const publicJsonPath = join(publicDataDir, "pages.json");
288
+ await writeFile(publicJsonPath, jsonContent, "utf-8");
289
+ logger.debug(`Wrote ${pages.length} pages to __ai-ready/pages.json`);
290
+ const dumpData = await exportDbDump(state.db);
291
+ const dumpPath = join(publicDataDir, "pages.dump");
292
+ await writeFile(dumpPath, dumpData, "utf-8");
293
+ logger.debug(`Created database dump at __ai-ready/pages.dump (${(dumpData.length / 1024).toFixed(1)}kb compressed)`);
520
294
  }
521
295
  const llmsStats = await prerenderRoute(nitro, "/llms.txt");
522
296
  const llmsFullStats = await stat(state.llmsFullTxtPath);
@@ -555,6 +329,90 @@ function setupPrerenderHandler(pageDataPath, siteInfo, llmsTxtConfig) {
555
329
  });
556
330
  }
557
331
 
332
+ function registerTypeTemplates(_ctx) {
333
+ addTypeTemplate({
334
+ filename: "types/nuxt-ai-ready.d.ts",
335
+ getContents: () => `// Generated by nuxt-ai-ready
336
+ import type { MarkdownContext, PageIndexedContext } from 'nuxt-ai-ready'
337
+ import type { HTMLToMarkdownOptions } from 'mdream'
338
+
339
+ declare module 'nitropack/types' {
340
+ interface NitroRuntimeHooks {
341
+ 'ai-ready:markdown': (context: MarkdownContext) => void | Promise<void>
342
+ 'ai-ready:mdreamConfig': (config: HTMLToMarkdownOptions) => void | Promise<void>
343
+ 'ai-ready:page:indexed': (context: PageIndexedContext) => void | Promise<void>
344
+ }
345
+ }
346
+
347
+ declare module '#ai-ready-virtual/read-page-data.mjs' {
348
+ export function readPageDataFromFilesystem(): Promise<{
349
+ pages: Array<{
350
+ route: string
351
+ title: string
352
+ description: string
353
+ headings: string
354
+ keywords: string[]
355
+ updatedAt: string
356
+ markdown: string
357
+ }>
358
+ errorRoutes: string[]
359
+ }>
360
+ }
361
+
362
+ declare module '#ai-ready-virtual/page-data.mjs' {
363
+ export const pages: never[]
364
+ }
365
+
366
+ declare module '#ai-ready/adapter' {
367
+ import type { Connector } from 'db0'
368
+ const connector: (config: unknown) => Connector
369
+ export default connector
370
+ }
371
+
372
+ export {}
373
+ `
374
+ });
375
+ }
376
+
377
+ async function resolveDatabaseAdapter(type) {
378
+ const connectors = {
379
+ d1: "db0/connectors/cloudflare-d1",
380
+ libsql: "db0/connectors/libsql/node"
381
+ };
382
+ if (type !== "sqlite" && connectors[type]) {
383
+ return connectors[type];
384
+ }
385
+ if (process.versions.bun) {
386
+ return "db0/connectors/bun-sqlite";
387
+ }
388
+ const nodeVersion = Number.parseInt(process.versions.node?.split(".")[0] || "0");
389
+ if (nodeVersion >= 22) {
390
+ return "db0/connectors/node-sqlite";
391
+ }
392
+ return "db0/connectors/better-sqlite3";
393
+ }
394
+ function refineDatabaseConfig(config, rootDir) {
395
+ const type = config.type || "sqlite";
396
+ if (type === "sqlite") {
397
+ const filename = config.filename || ".data/ai-ready/pages.db";
398
+ return {
399
+ type: "sqlite",
400
+ filename: isAbsolute(filename) ? filename : join$1(rootDir, filename)
401
+ };
402
+ }
403
+ if (type === "d1") {
404
+ return {
405
+ type: "d1",
406
+ bindingName: config.bindingName || "AI_READY_DB"
407
+ };
408
+ }
409
+ return {
410
+ type: "libsql",
411
+ url: config.url,
412
+ authToken: config.authToken
413
+ };
414
+ }
415
+
558
416
  const module$1 = defineNuxtModule({
559
417
  meta: {
560
418
  name: "nuxt-ai-ready",
@@ -606,7 +464,6 @@ const module$1 = defineNuxtModule({
606
464
  hookNuxtSeoProLicense();
607
465
  nuxt.options.nitro.alias = nuxt.options.nitro.alias || {};
608
466
  nuxt.options.alias["#ai-ready"] = resolve("./runtime");
609
- ({ resolver: createResolver(import.meta.url) });
610
467
  const dbType = config.database?.type || "sqlite";
611
468
  const adapterPath = await resolveDatabaseAdapter(dbType);
612
469
  nuxt.options.alias["#ai-ready/adapter"] = adapterPath;
@@ -628,48 +485,7 @@ const module$1 = defineNuxtModule({
628
485
  contentSignal: [`ai-train=${config.contentSignal.aiTrain ? "yes" : "no"}`, `search=${config.contentSignal.search ? "yes" : "no"}`, `ai-input=${config.contentSignal.aiInput ? "yes" : "no"}`]
629
486
  });
630
487
  }
631
- addTypeTemplate({
632
- filename: "types/nuxt-ai-ready.d.ts",
633
- getContents: () => `// Generated by nuxt-ai-ready
634
- import type { MarkdownContext, PageIndexedContext } from 'nuxt-ai-ready'
635
- import type { HTMLToMarkdownOptions } from 'mdream'
636
-
637
- declare module 'nitropack/types' {
638
- interface NitroRuntimeHooks {
639
- 'ai-ready:markdown': (context: MarkdownContext) => void | Promise<void>
640
- 'ai-ready:mdreamConfig': (config: HTMLToMarkdownOptions) => void | Promise<void>
641
- 'ai-ready:page:indexed': (context: PageIndexedContext) => void | Promise<void>
642
- }
643
- }
644
-
645
- declare module '#ai-ready-virtual/read-page-data.mjs' {
646
- export function readPageDataFromFilesystem(): Promise<{
647
- pages: Array<{
648
- route: string
649
- title: string
650
- description: string
651
- headings: string
652
- keywords: string[]
653
- updatedAt: string
654
- markdown: string
655
- }>
656
- errorRoutes: string[]
657
- }>
658
- }
659
-
660
- declare module '#ai-ready-virtual/page-data.mjs' {
661
- export const pages: never[]
662
- }
663
-
664
- declare module '#ai-ready/adapter' {
665
- import type { Connector } from 'db0'
666
- const connector: (config: unknown) => Connector
667
- export default connector
668
- }
669
-
670
- export {}
671
- `
672
- }, { nitro: true });
488
+ registerTypeTemplates();
673
489
  const defaultLlmsTxtSections = [];
674
490
  const llmsFullRoute = withSiteUrl("llms-full.txt");
675
491
  defaultLlmsTxtSections.push({
@@ -720,24 +536,55 @@ export {}
720
536
  await nuxt.callHook("ai-ready:llms-txt", llmsTxtPayload);
721
537
  mergedLlmsTxt.sections = llmsTxtPayload.sections;
722
538
  mergedLlmsTxt.notes = llmsTxtPayload.notes.length > 0 ? llmsTxtPayload.notes : void 0;
723
- const prerenderCacheDir = join$1(nuxt.options.rootDir, "node_modules/.cache/nuxt-seo/ai-ready/routes");
724
- const pageDataPath = join$1(nuxt.options.buildDir, ".data/ai-ready/page-data.jsonl");
539
+ const prerenderCacheDir = join(nuxt.options.rootDir, "node_modules/.cache/nuxt-seo/ai-ready/routes");
540
+ const buildDbPath = join(nuxt.options.buildDir, ".data/ai-ready/build.db");
725
541
  nuxt.hooks.hook("nitro:config", (nitroConfig) => {
726
542
  nitroConfig.experimental = nitroConfig.experimental || {};
727
543
  nitroConfig.experimental.asyncContext = true;
544
+ const runtimeSyncEnabled2 = config.runtimeSync?.enabled ?? false;
545
+ const cron = config.runtimeSync?.cron;
546
+ if (runtimeSyncEnabled2 && cron) {
547
+ nitroConfig.tasks = nitroConfig.tasks || {};
548
+ nitroConfig.tasks["ai-ready:index"] = {
549
+ handler: resolve("./runtime/server/tasks/ai-ready-index")
550
+ };
551
+ nitroConfig.scheduledTasks = nitroConfig.scheduledTasks || {};
552
+ nitroConfig.scheduledTasks[cron] = nitroConfig.scheduledTasks[cron] || [];
553
+ nitroConfig.scheduledTasks[cron].push("ai-ready:index");
554
+ }
728
555
  nitroConfig.virtual = nitroConfig.virtual || {};
729
556
  nitroConfig.virtual["#ai-ready-virtual/read-page-data.mjs"] = `
730
- import { readFile } from 'node:fs/promises'
731
-
732
557
  export async function readPageDataFromFilesystem() {
733
558
  if (!import.meta.prerender) {
734
559
  return { pages: [], errorRoutes: [] }
735
560
  }
736
- const data = await readFile(${JSON.stringify(pageDataPath)}, 'utf-8').catch(() => null)
737
- if (!data) return { pages: [], errorRoutes: [] }
738
- const entries = data.trim().split('\\n').filter(Boolean).map(line => JSON.parse(line))
739
- const pages = entries.filter(e => !e._error)
740
- const errorRoutes = entries.filter(e => e._error).map(e => e.route)
561
+
562
+ const dbPath = ${JSON.stringify(buildDbPath)}
563
+
564
+ // Check if database file exists
565
+ const { existsSync } = await import('node:fs')
566
+ if (!existsSync(dbPath)) {
567
+ return { pages: [], errorRoutes: [] }
568
+ }
569
+
570
+ // Use better-sqlite3 to read pages
571
+ const Database = (await import('better-sqlite3')).default
572
+ const db = new Database(dbPath, { readonly: true })
573
+
574
+ const rows = db.prepare('SELECT route, title, description, markdown, headings, keywords, updated_at, is_error FROM ai_ready_pages').all()
575
+ db.close()
576
+
577
+ const pages = rows.filter(r => !r.is_error).map(r => ({
578
+ route: r.route,
579
+ title: r.title,
580
+ description: r.description,
581
+ markdown: r.markdown,
582
+ headings: r.headings,
583
+ keywords: JSON.parse(r.keywords || '[]'),
584
+ updatedAt: r.updated_at,
585
+ }))
586
+ const errorRoutes = rows.filter(r => r.is_error).map(r => r.route)
587
+
741
588
  return { pages, errorRoutes }
742
589
  }
743
590
  `;
@@ -745,6 +592,9 @@ export async function readPageDataFromFilesystem() {
745
592
  export const errorRoutes = []`;
746
593
  });
747
594
  const database = refineDatabaseConfig(config.database || {}, nuxt.options.rootDir);
595
+ const runtimeSyncEnabled = config.runtimeSync?.enabled ?? false;
596
+ const indexNowKey = config.indexNow?.key || process.env.NUXT_AI_READY_INDEXNOW_KEY;
597
+ const indexNowEnabled = !!(config.indexNow?.enabled !== false && indexNowKey);
748
598
  nuxt.options.runtimeConfig["nuxt-ai-ready"] = {
749
599
  version: version || "0.0.0",
750
600
  debug: config.debug || false,
@@ -756,12 +606,24 @@ export const errorRoutes = []`;
756
606
  llmsTxt: mergedLlmsTxt,
757
607
  cacheMaxAgeSeconds: config.cacheMaxAgeSeconds ?? 600,
758
608
  prerenderCacheDir,
759
- ttl: config.ttl ?? 0,
760
- database
609
+ database,
610
+ runtimeSync: {
611
+ enabled: runtimeSyncEnabled,
612
+ ttl: config.runtimeSync?.ttl ?? 3600,
613
+ batchSize: config.runtimeSync?.batchSize ?? 20,
614
+ secret: config.runtimeSync?.secret,
615
+ pruneTtl: config.runtimeSync?.pruneTtl ?? 0
616
+ },
617
+ indexNow: indexNowEnabled ? {
618
+ enabled: true,
619
+ key: indexNowKey,
620
+ host: config.indexNow?.host || "api.indexnow.org"
621
+ } : void 0
761
622
  };
762
623
  nuxt.options.nitro.plugins = nuxt.options.nitro.plugins || [];
763
624
  nuxt.options.nitro.plugins.push(resolve("./runtime/server/plugins/db-restore"));
764
- nuxt.options.nitro.plugins.push(resolve("./runtime/server/plugins/page-indexer"));
625
+ if (runtimeSyncEnabled)
626
+ nuxt.options.nitro.plugins.push(resolve("./runtime/server/plugins/sitemap-seeder"));
765
627
  addServerHandler({
766
628
  middleware: true,
767
629
  handler: resolve("./runtime/server/middleware/markdown.prerender")
@@ -773,7 +635,7 @@ export const errorRoutes = []`;
773
635
  if (nuxt.options.build) {
774
636
  addPlugin({
775
637
  mode: "server",
776
- src: resolve("./runtime/nuxt/plugins/md-hints.prerender")
638
+ src: resolve("./runtime/app/plugins/md-hints.prerender")
777
639
  });
778
640
  }
779
641
  addServerHandler({ route: "/llms.txt", handler: resolve("./runtime/server/routes/llms.txt.get") });
@@ -781,6 +643,18 @@ export const errorRoutes = []`;
781
643
  if (config.debug) {
782
644
  addServerHandler({ route: "/__ai-ready-debug", handler: resolve("./runtime/server/routes/__ai-ready-debug.get") });
783
645
  }
646
+ if (runtimeSyncEnabled) {
647
+ addServerHandler({ route: "/__ai-ready/status", handler: resolve("./runtime/server/routes/__ai-ready/status.get") });
648
+ addServerHandler({ route: "/__ai-ready/poll", method: "post", handler: resolve("./runtime/server/routes/__ai-ready/poll.post") });
649
+ addServerHandler({ route: "/__ai-ready/prune", method: "post", handler: resolve("./runtime/server/routes/__ai-ready/prune.post") });
650
+ }
651
+ if (indexNowEnabled && indexNowKey) {
652
+ addServerHandler({ route: `/${indexNowKey}.txt`, handler: resolve("./runtime/server/routes/indexnow-key.get") });
653
+ addServerHandler({ route: "/__ai-ready/indexnow", method: "post", handler: resolve("./runtime/server/routes/__ai-ready/indexnow.post") });
654
+ if (!runtimeSyncEnabled) {
655
+ addServerHandler({ route: "/__ai-ready/status", handler: resolve("./runtime/server/routes/__ai-ready/status.get") });
656
+ }
657
+ }
784
658
  const isStatic = nuxt.options.nitro.static || nuxt.options._generate || false;
785
659
  const hasPrerenderedRoutes = nuxt.options.nitro.prerender?.routes?.length;
786
660
  const isSPA = nuxt.options.ssr === false;
@@ -794,7 +668,7 @@ export const errorRoutes = []`;
794
668
  }
795
669
  if (isStatic || hasPrerenderedRoutes) {
796
670
  const siteConfig = useSiteConfig();
797
- setupPrerenderHandler(pageDataPath, {
671
+ setupPrerenderHandler(buildDbPath, {
798
672
  name: siteConfig.name,
799
673
  url: siteConfig.url,
800
674
  description: siteConfig.description
@@ -805,7 +679,7 @@ export const errorRoutes = []`;
805
679
  nuxt.options.nitro.routeRules["/llms-full.txt"] = { headers: { "Content-Type": "text/plain; charset=utf-8" } };
806
680
  nuxt.hooks.hook("nitro:build:before", (nitro) => {
807
681
  nitro.hooks.hook("compiled", async () => {
808
- const headersPath = join$1(nitro.options.output.publicDir, "_headers");
682
+ const headersPath = join(nitro.options.output.publicDir, "_headers");
809
683
  const exists = await access(headersPath).then(() => true).catch(() => false);
810
684
  if (exists) {
811
685
  await appendFile(headersPath, `