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
@@ -1,4 +1,5 @@
1
- import { getPagesList } from "../../utils/pageData.js";
1
+ import { useEvent } from "nitropack/runtime";
2
+ import { queryPages } from "../../db/queries.js";
2
3
  export default {
3
4
  uri: "resource://nuxt-ai-ready/pages",
4
5
  name: "All Pages",
@@ -8,12 +9,19 @@ export default {
8
9
  },
9
10
  cache: "1h",
10
11
  async handler(uri) {
11
- const pages = await getPagesList();
12
+ const event = useEvent();
13
+ const pages = await queryPages(event);
12
14
  return {
13
15
  contents: [{
14
16
  uri: uri.toString(),
15
17
  mimeType: "application/json",
16
- text: JSON.stringify(pages)
18
+ text: JSON.stringify(pages.map((p) => ({
19
+ route: p.route,
20
+ title: p.title || p.route,
21
+ description: p.description || "",
22
+ headings: p.headings || void 0,
23
+ keywords: p.keywords?.length ? p.keywords : void 0
24
+ })))
17
25
  }]
18
26
  };
19
27
  }
@@ -1,11 +1,37 @@
1
- import { getPagesList } from "../../utils/pageData.js";
2
- export default {
1
+ import { useEvent } from "nitropack/runtime";
2
+ import { z } from "zod";
3
+ import { countPages, queryPages } from "../../db/queries.js";
4
+ const inputSchema = {
5
+ limit: z.number().optional().default(100).describe("Max pages to return (default: 100)"),
6
+ offset: z.number().optional().default(0).describe("Skip first N pages (for pagination)")
7
+ };
8
+ const tool = {
3
9
  name: "list_pages",
4
- description: "Lists all available pages with titles, descriptions and routes.",
5
- inputSchema: {},
10
+ description: "Lists all available pages with titles, descriptions and routes. Supports pagination via limit/offset.",
11
+ inputSchema,
6
12
  cache: "1h",
7
- async handler() {
8
- const pages = await getPagesList();
9
- return { content: [{ type: "text", text: JSON.stringify(pages) }] };
13
+ async handler({ limit, offset }) {
14
+ const event = useEvent();
15
+ const pages = await queryPages(event, { limit, offset });
16
+ const total = await countPages(event);
17
+ return {
18
+ content: [{
19
+ type: "text",
20
+ text: JSON.stringify({
21
+ pages: pages.map((p) => ({
22
+ route: p.route,
23
+ title: p.title || p.route,
24
+ description: p.description || "",
25
+ headings: p.headings || void 0,
26
+ keywords: p.keywords?.length ? p.keywords : void 0
27
+ })),
28
+ total,
29
+ limit,
30
+ offset,
31
+ hasMore: offset + pages.length < total
32
+ })
33
+ }]
34
+ };
10
35
  }
11
36
  };
37
+ export default tool;
@@ -1,5 +1,5 @@
1
+ import { useEvent } from "nitropack/runtime";
1
2
  import { z } from "zod";
2
- import { useDatabase } from "../../db/index.js";
3
3
  import { searchPages } from "../../db/queries.js";
4
4
  const inputSchema = {
5
5
  query: z.string().describe("Search query"),
@@ -11,8 +11,8 @@ const tool = {
11
11
  inputSchema,
12
12
  cache: "5m",
13
13
  async handler({ query, limit }) {
14
- const db = await useDatabase();
15
- const results = await searchPages(db, query, { limit });
14
+ const event = useEvent();
15
+ const results = await searchPages(event, query, { limit });
16
16
  return { content: [{ type: "text", text: JSON.stringify(results) }] };
17
17
  }
18
18
  };
@@ -53,9 +53,8 @@ export default defineEventHandler(async (event) => {
53
53
  const result = await convertHtmlToMarkdown(
54
54
  html,
55
55
  withSiteUrl(event, path),
56
- config,
57
- path,
58
- event
56
+ config.mdreamOptions,
57
+ { hooks: { route: path, event } }
59
58
  );
60
59
  setHeader(event, "content-type", "text/markdown; charset=utf-8");
61
60
  if (config.markdownCacheHeaders) {
@@ -1,8 +1,8 @@
1
1
  import { withSiteUrl } from "#site-config/server/composables/utils";
2
2
  import { createError, defineEventHandler } from "h3";
3
3
  import { useRuntimeConfig } from "nitropack/runtime";
4
- import { extractKeywords, stripMarkdown } from "../utils/keywords.js";
5
- import { convertHtmlToMarkdownMeta, getMarkdownRenderInfo } from "../utils.js";
4
+ import { convertHtmlToMarkdown, getMarkdownRenderInfo } from "../utils.js";
5
+ import { extractKeywords } from "../utils/keywords.js";
6
6
  export default defineEventHandler(async (event) => {
7
7
  if (!import.meta.prerender) {
8
8
  return;
@@ -27,12 +27,13 @@ export default defineEventHandler(async (event) => {
27
27
  message: `Page rendered as error: ${path}`
28
28
  });
29
29
  }
30
- const result = await convertHtmlToMarkdownMeta(
30
+ const result = await convertHtmlToMarkdown(
31
31
  html,
32
32
  withSiteUrl(event, path),
33
- runtimeConfig.mdreamOptions
33
+ runtimeConfig.mdreamOptions,
34
+ { extractUpdatedAt: true }
34
35
  );
35
- const keywords = extractKeywords(stripMarkdown(result.markdown), result.metaKeywords);
36
+ const keywords = extractKeywords(result.textContent, result.metaKeywords);
36
37
  return JSON.stringify({
37
38
  markdown: result.markdown,
38
39
  title: result.title,
@@ -1,13 +1,14 @@
1
1
  import { defineNitroPlugin } from "nitropack/runtime";
2
2
  import { useDatabase } from "../db/index.js";
3
- import { hasPages } from "../db/queries.js";
4
- import { decompressDump, importDump } from "../db/dump.js";
3
+ import { countPages } from "../db/queries.js";
4
+ import { decompressFromBase64, importDbDump } from "../db/shared.js";
5
5
  import { logger } from "../logger.js";
6
6
  export default defineNitroPlugin(async () => {
7
- if (import.meta.prerender) return;
8
- if (import.meta.dev) return;
9
- const db = await useDatabase();
10
- if (await hasPages(db)) {
7
+ if (import.meta.prerender)
8
+ return;
9
+ if (import.meta.dev)
10
+ return;
11
+ if (await countPages() > 0) {
11
12
  logger.debug("[db-restore] Database already has data, skipping restore");
12
13
  return;
13
14
  }
@@ -18,7 +19,8 @@ export default defineNitroPlugin(async () => {
18
19
  logger.debug("[db-restore] No dump found, starting with empty database");
19
20
  return;
20
21
  }
21
- const rows = await decompressDump(dumpData);
22
- await importDump(db, rows);
22
+ const db = await useDatabase();
23
+ const rows = await decompressFromBase64(dumpData);
24
+ await importDbDump(db, rows);
23
25
  logger.info(`[db-restore] Restored ${rows.length} pages from dump`);
24
26
  });
@@ -0,0 +1,44 @@
1
+ import { defineNitroPlugin, useRuntimeConfig } from "nitropack/runtime";
2
+ import { getSitemapSeededAt, pruneStaleRoutes, seedRoutes, setSitemapSeededAt } from "../db/queries.js";
3
+ import { logger } from "../logger.js";
4
+ import { fetchSitemapUrls } from "../utils/sitemap.js";
5
+ let seeding = null;
6
+ export default defineNitroPlugin((nitro) => {
7
+ if (import.meta.prerender)
8
+ return;
9
+ nitro.hooks.hook("request", async (event) => {
10
+ if (!seeding) {
11
+ seeding = seedFromSitemap(event).catch((err) => {
12
+ logger.error("[sitemap-seeder] Failed to seed:", err);
13
+ });
14
+ }
15
+ });
16
+ });
17
+ async function seedFromSitemap(event) {
18
+ const config = useRuntimeConfig()["nuxt-ai-ready"];
19
+ const { ttl, pruneTtl } = config.runtimeSync;
20
+ const seededAt = await getSitemapSeededAt(event);
21
+ if (seededAt && ttl > 0) {
22
+ const age = (Date.now() - seededAt) / 1e3;
23
+ if (age < ttl) {
24
+ logger.debug(`[sitemap-seeder] Sitemap fresh (${Math.round(age)}s old), skipping`);
25
+ return;
26
+ }
27
+ }
28
+ const urls = await fetchSitemapUrls(event);
29
+ if (urls.length === 0) {
30
+ return;
31
+ }
32
+ const routes = urls.map((u) => {
33
+ const url = new URL(u.loc);
34
+ return url.pathname;
35
+ }).filter((route) => !route.includes("."));
36
+ await seedRoutes(event, routes);
37
+ await setSitemapSeededAt(event, Date.now());
38
+ logger.info(`[sitemap-seeder] Seeded ${routes.length} routes from sitemap`);
39
+ if (pruneTtl > 0) {
40
+ const pruned = await pruneStaleRoutes(event, pruneTtl);
41
+ if (pruned > 0)
42
+ logger.info(`[sitemap-seeder] Pruned ${pruned} stale routes`);
43
+ }
44
+ }
@@ -0,0 +1,2 @@
1
+ declare const _default: import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<import("../../utils/indexnow.js").IndexNowResult>>;
2
+ export default _default;
@@ -0,0 +1,18 @@
1
+ import { createError, eventHandler, getQuery } from "h3";
2
+ import { useRuntimeConfig } from "nitropack/runtime";
3
+ import { syncToIndexNow } from "../../utils/indexnow.js";
4
+ export default eventHandler(async (event) => {
5
+ const config = useRuntimeConfig(event)["nuxt-ai-ready"];
6
+ if (!config.indexNow?.enabled) {
7
+ throw createError({ statusCode: 400, message: "IndexNow not enabled" });
8
+ }
9
+ const query = getQuery(event);
10
+ if (config.runtimeSync?.secret) {
11
+ const secret = query.secret;
12
+ if (secret !== config.runtimeSync.secret) {
13
+ throw createError({ statusCode: 401, message: "Unauthorized" });
14
+ }
15
+ }
16
+ const limit = query.limit ? Number(query.limit) : 100;
17
+ return syncToIndexNow(event, limit);
18
+ });
@@ -0,0 +1,8 @@
1
+ declare const _default: import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<{
2
+ indexed: number;
3
+ remaining: number;
4
+ errors: string[] | undefined;
5
+ duration: number;
6
+ complete: boolean;
7
+ }>>;
8
+ export default _default;
@@ -0,0 +1,25 @@
1
+ import { createError, eventHandler, getQuery } from "h3";
2
+ import { useRuntimeConfig } from "nitropack/runtime";
3
+ import { batchIndexPages } from "../../utils/batchIndex.js";
4
+ export default eventHandler(async (event) => {
5
+ const config = useRuntimeConfig()["nuxt-ai-ready"];
6
+ const query = getQuery(event);
7
+ if (config.runtimeSync.secret) {
8
+ const secret = query.secret;
9
+ if (secret !== config.runtimeSync.secret) {
10
+ throw createError({ statusCode: 401, message: "Unauthorized" });
11
+ }
12
+ }
13
+ const result = await batchIndexPages(event, {
14
+ limit: query.limit ? Number(query.limit) : void 0,
15
+ all: query.all === "true" || query.all === "1",
16
+ timeout: query.timeout ? Number(query.timeout) : void 0
17
+ });
18
+ return {
19
+ indexed: result.indexed,
20
+ remaining: result.remaining,
21
+ errors: result.errors.length > 0 ? result.errors : void 0,
22
+ duration: result.duration,
23
+ complete: result.complete
24
+ };
25
+ });
@@ -0,0 +1,14 @@
1
+ declare const _default: import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<{
2
+ routes: string[];
3
+ count: number;
4
+ ttl: number;
5
+ dry: boolean;
6
+ pruned?: undefined;
7
+ } | {
8
+ pruned: number;
9
+ ttl: number;
10
+ dry: boolean;
11
+ routes?: undefined;
12
+ count?: undefined;
13
+ }>>;
14
+ export default _default;
@@ -0,0 +1,21 @@
1
+ import { createError, eventHandler, getQuery } from "h3";
2
+ import { useRuntimeConfig } from "nitropack/runtime";
3
+ import { getStaleRoutes, pruneStaleRoutes } from "../../db/queries.js";
4
+ export default eventHandler(async (event) => {
5
+ const config = useRuntimeConfig()["nuxt-ai-ready"];
6
+ const query = getQuery(event);
7
+ const dry = query.dry === "true" || query.dry === "1";
8
+ if (!dry && config.runtimeSync.secret) {
9
+ const secret = query.secret;
10
+ if (secret !== config.runtimeSync.secret) {
11
+ throw createError({ statusCode: 401, message: "Unauthorized" });
12
+ }
13
+ }
14
+ const ttl = query.ttl ? Number(query.ttl) : config.runtimeSync.pruneTtl;
15
+ if (dry) {
16
+ const routes = await getStaleRoutes(event, ttl);
17
+ return { routes, count: routes.length, ttl, dry: true };
18
+ }
19
+ const pruned = await pruneStaleRoutes(event, ttl);
20
+ return { pruned, ttl, dry: false };
21
+ });
@@ -0,0 +1,2 @@
1
+ declare const _default: import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<Record<string, unknown>>>;
2
+ export default _default;
@@ -0,0 +1,28 @@
1
+ import { eventHandler } from "h3";
2
+ import { useRuntimeConfig } from "nitropack/runtime";
3
+ import { countPages, countPagesNeedingIndexNowSync, getIndexNowStats } from "../../db/queries.js";
4
+ export default eventHandler(async (event) => {
5
+ const config = useRuntimeConfig(event)["nuxt-ai-ready"];
6
+ const [total, pending] = await Promise.all([
7
+ countPages(event),
8
+ countPages(event, { where: { pending: true } })
9
+ ]);
10
+ const result = {
11
+ total,
12
+ indexed: total - pending,
13
+ pending
14
+ };
15
+ if (config.indexNow?.enabled) {
16
+ const [indexNowPending, indexNowStats] = await Promise.all([
17
+ countPagesNeedingIndexNowSync(event),
18
+ getIndexNowStats(event)
19
+ ]);
20
+ result.indexNow = {
21
+ pending: indexNowPending,
22
+ totalSubmitted: indexNowStats.totalSubmitted,
23
+ lastSubmittedAt: indexNowStats.lastSubmittedAt,
24
+ lastError: indexNowStats.lastError
25
+ };
26
+ }
27
+ return result;
28
+ });
@@ -1,6 +1,6 @@
1
1
  import { createError, eventHandler, setHeader } from "h3";
2
2
  import { useRuntimeConfig } from "nitropack/runtime";
3
- import { getErrorRoutes, getPages, getPagesList } from "../utils/pageData.js";
3
+ import { queryPages } from "../db/queries.js";
4
4
  export default eventHandler(async (event) => {
5
5
  const runtimeConfig = useRuntimeConfig(event)["nuxt-ai-ready"];
6
6
  if (!runtimeConfig.debug) {
@@ -19,16 +19,15 @@ export default eventHandler(async (event) => {
19
19
  } else {
20
20
  mode = "production";
21
21
  }
22
- const pages = await getPages(event);
23
- const pagesList = await getPagesList(event);
24
- const errorRoutes = await getErrorRoutes(event);
22
+ const pages = await queryPages(event);
23
+ const errorRoutes = await queryPages(event, { where: { hasError: true } });
25
24
  let source;
26
25
  if (isDev) {
27
- source = "empty (dev mode returns empty Map)";
26
+ source = "empty (dev mode returns empty array)";
28
27
  } else if (isPrerender) {
29
28
  source = "#ai-ready-virtual/read-page-data.mjs (reads from filesystem)";
30
29
  } else {
31
- source = "fetch('/__ai-ready/pages.json') (public directory)";
30
+ source = "database (db0 adapter)";
32
31
  }
33
32
  let pageDataModuleInfo = { available: false, pagesCount: 0, errorRoutesCount: 0 };
34
33
  let readPageDataModuleInfo = { available: false, note: "" };
@@ -79,17 +78,17 @@ export default eventHandler(async (event) => {
79
78
  if (mode === "development") {
80
79
  issues.push("Development mode: page data is intentionally empty");
81
80
  suggestions.push("Run `nuxi generate` or `nuxi build --prerender` to generate page data");
82
- } else if (mode === "production" && pages.size === 0) {
81
+ } else if (mode === "production" && pages.length === 0) {
83
82
  if (!jsonFileStatus.available) {
84
- issues.push("Production mode with no page data - /__ai-ready/pages.json not found");
83
+ issues.push("Production mode with no page data - database may be empty");
85
84
  suggestions.push("Run `nuxi generate` or `nuxi build --prerender` to generate the page data");
86
85
  } else {
87
- issues.push("pages.json exists but returned empty page data");
86
+ issues.push("Database exists but returned empty page data");
88
87
  suggestions.push("Check if pages were prerendered correctly");
89
88
  }
90
- } else if (mode === "prerender" && pages.size === 0) {
89
+ } else if (mode === "prerender" && pages.length === 0) {
91
90
  issues.push("Prerender mode but no pages found");
92
- suggestions.push("Check if page-data.jsonl exists in .nuxt/.data/ai-ready/");
91
+ suggestions.push("Check if pages.db exists in .data/ai-ready/");
93
92
  }
94
93
  const debugInfo = {
95
94
  version: runtimeConfig.version || "unknown",
@@ -105,14 +104,14 @@ export default eventHandler(async (event) => {
105
104
  },
106
105
  pageData: {
107
106
  source,
108
- pageCount: pages.size,
109
- pages: pagesList.map((p) => ({
107
+ pageCount: pages.length,
108
+ pages: pages.map((p) => ({
110
109
  route: p.route,
111
110
  title: p.title,
112
111
  hasDescription: !!p.description,
113
112
  hasHeadings: !!p.headings
114
113
  })),
115
- errorRoutes: Array.from(errorRoutes)
114
+ errorRoutes: errorRoutes.map((e) => e.route)
116
115
  },
117
116
  jsonFile: jsonFileStatus,
118
117
  virtualModules: {
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Key verification endpoint for IndexNow
3
+ * Returns the API key as plain text for search engine verification
4
+ */
5
+ declare const _default: import("h3").EventHandler<import("h3").EventHandlerRequest, string | null>;
6
+ export default _default;
@@ -0,0 +1,10 @@
1
+ import { eventHandler, setHeader } from "h3";
2
+ import { useRuntimeConfig } from "nitropack/runtime";
3
+ export default eventHandler((event) => {
4
+ const config = useRuntimeConfig(event)["nuxt-ai-ready"];
5
+ const key = config.indexNow?.key;
6
+ if (!key)
7
+ return null;
8
+ setHeader(event, "Content-Type", "text/plain; charset=utf-8");
9
+ return key;
10
+ });
@@ -1,2 +1,2 @@
1
- declare const _default: import("h3").EventHandler<import("h3").EventHandlerRequest, string>;
1
+ declare const _default: import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<string | void>>;
2
2
  export default _default;
@@ -1,5 +1,36 @@
1
- import { eventHandler, setHeader } from "h3";
2
- export default eventHandler((event) => {
1
+ import { useSiteConfig } from "#imports";
2
+ import { eventHandler, sendIterable, setHeader, setResponseHeader } from "h3";
3
+ import { useRuntimeConfig } from "nitropack/runtime";
4
+ import { countPages, streamPages } from "../db/queries.js";
5
+ import { buildLlmsFullTxtHeader, formatPageForLlmsFullTxt } from "../utils/llms-full.js";
6
+ export default eventHandler(async (event) => {
7
+ if (import.meta.prerender) {
8
+ setHeader(event, "Content-Type", "text/plain; charset=utf-8");
9
+ return "# llms-full.txt\n\nThis file is generated during prerender.";
10
+ }
11
+ const config = useRuntimeConfig()["nuxt-ai-ready"];
12
+ const siteConfig = useSiteConfig();
13
+ const header = buildLlmsFullTxtHeader(
14
+ {
15
+ name: siteConfig.name,
16
+ url: siteConfig.url,
17
+ description: siteConfig.description
18
+ },
19
+ config.llmsTxt
20
+ );
21
+ const total = await countPages(event);
22
+ if (total === 0) {
23
+ setHeader(event, "Content-Type", "text/plain; charset=utf-8");
24
+ return `${header}No pages indexed. Run \`nuxi generate\` or enable runtime indexing.`;
25
+ }
3
26
  setHeader(event, "Content-Type", "text/plain; charset=utf-8");
4
- return "# llms-full.txt\n\nThis file is only available for prerendered routes.\nRun `nuxi generate` to generate this file.";
27
+ setResponseHeader(event, "Cache-Control", "public, max-age=3600, s-maxage=3600, stale-while-revalidate=86400");
28
+ const siteUrl = siteConfig.url;
29
+ async function* generateContent() {
30
+ yield header;
31
+ for await (const page of streamPages(event)) {
32
+ yield formatPageForLlmsFullTxt(page.route, page.title, page.description, page.markdown, siteUrl);
33
+ }
34
+ }
35
+ return sendIterable(event, generateContent());
5
36
  });
@@ -0,0 +1,11 @@
1
+ declare const _default: import("nitropack/types").Task<{
2
+ indexed: number;
3
+ remaining: number;
4
+ errors: string[];
5
+ complete: boolean;
6
+ indexNow: {
7
+ submitted: number;
8
+ error: any;
9
+ } | undefined;
10
+ }>;
11
+ export default _default;
@@ -0,0 +1,39 @@
1
+ import { defineTask, useRuntimeConfig } from "nitropack/runtime";
2
+ import { batchIndexPages } from "../utils/batchIndex.js";
3
+ import { syncToIndexNow } from "../utils/indexnow.js";
4
+ export default defineTask({
5
+ meta: {
6
+ name: "ai-ready:index",
7
+ description: "Index pending pages for AI Ready"
8
+ },
9
+ async run({ payload }) {
10
+ const config = useRuntimeConfig()["nuxt-ai-ready"];
11
+ const limit = payload?.limit ?? config.runtimeSync.batchSize;
12
+ const mockEvent = {
13
+ $fetch: globalThis.$fetch
14
+ };
15
+ const result = await batchIndexPages(mockEvent, {
16
+ limit,
17
+ all: false
18
+ });
19
+ let indexNowResult;
20
+ if (config.indexNow?.enabled) {
21
+ indexNowResult = await syncToIndexNow(mockEvent, 100).catch((err) => {
22
+ console.warn("[ai-ready:index] IndexNow sync failed:", err.message);
23
+ return { success: false, submitted: 0, error: err.message };
24
+ });
25
+ }
26
+ return {
27
+ result: {
28
+ indexed: result.indexed,
29
+ remaining: result.remaining,
30
+ errors: result.errors,
31
+ complete: result.complete,
32
+ indexNow: indexNowResult ? {
33
+ submitted: indexNowResult.submitted,
34
+ error: indexNowResult.error
35
+ } : void 0
36
+ }
37
+ };
38
+ }
39
+ });
@@ -0,0 +1,26 @@
1
+ import type { H3Event } from 'h3';
2
+ export interface BatchIndexOptions {
3
+ /** Max pages per batch (default: 10, max: 50) */
4
+ limit?: number;
5
+ /** Process until complete (ignores limit) */
6
+ all?: boolean;
7
+ /** Max ms to run (for all mode, default: 30000) */
8
+ timeout?: number;
9
+ }
10
+ export interface BatchIndexResult {
11
+ /** Number of pages indexed */
12
+ indexed: number;
13
+ /** Pages remaining to index */
14
+ remaining: number;
15
+ /** Routes that failed to index */
16
+ errors: string[];
17
+ /** Duration in ms */
18
+ duration: number;
19
+ /** True if all pages are indexed */
20
+ complete: boolean;
21
+ }
22
+ /**
23
+ * Batch index pending pages (max 50 per batch)
24
+ * Shared logic used by poll endpoint and scheduled task
25
+ */
26
+ export declare function batchIndexPages(event: H3Event, options?: BatchIndexOptions): Promise<BatchIndexResult>;
@@ -0,0 +1,53 @@
1
+ import { countPages, queryPages } from "../db/queries.js";
2
+ import { logger } from "../logger.js";
3
+ import { indexPageByRoute } from "./indexPage.js";
4
+ export async function batchIndexPages(event, options = {}) {
5
+ const startTime = Date.now();
6
+ const limit = Math.min(options.limit ?? 10, 50);
7
+ const timeout = options.timeout ?? 3e4;
8
+ const beforeCount = await countPages(event, { where: { pending: true } });
9
+ if (beforeCount === 0) {
10
+ return {
11
+ indexed: 0,
12
+ remaining: 0,
13
+ errors: [],
14
+ duration: Date.now() - startTime,
15
+ complete: true
16
+ };
17
+ }
18
+ let indexed = 0;
19
+ const errors = [];
20
+ let iteration = 0;
21
+ while (true) {
22
+ if (options.all && Date.now() - startTime >= timeout) {
23
+ logger.debug(`[batchIndex] Timeout reached after ${Date.now() - startTime}ms`);
24
+ break;
25
+ }
26
+ if (!options.all && iteration >= limit) {
27
+ break;
28
+ }
29
+ const pages = await queryPages(event, { where: { pending: true }, limit: 1 });
30
+ const route = pages[0]?.route;
31
+ if (!route)
32
+ break;
33
+ iteration++;
34
+ const result = await indexPageByRoute(route, event, {
35
+ markFailedAsError: true,
36
+ force: true
37
+ });
38
+ if (result.success) {
39
+ indexed++;
40
+ logger.debug(`[batchIndex] Indexed: ${route}`);
41
+ } else {
42
+ errors.push(route);
43
+ }
44
+ }
45
+ const remaining = await countPages(event, { where: { pending: true } });
46
+ return {
47
+ indexed,
48
+ remaining,
49
+ errors,
50
+ duration: Date.now() - startTime,
51
+ complete: remaining === 0
52
+ };
53
+ }
@@ -1,4 +1,5 @@
1
1
  import type { H3Event } from 'h3';
2
+ export declare const INDEXING_HEADER = "x-ai-ready-indexing";
2
3
  export interface IndexPageOptions {
3
4
  /** Skip if page was indexed within TTL (uses config ttl if not specified) */
4
5
  ttl?: number;
@@ -6,6 +7,8 @@ export interface IndexPageOptions {
6
7
  force?: boolean;
7
8
  /** Skip calling the ai-ready:page:indexed hook */
8
9
  skipHook?: boolean;
10
+ /** Mark failed fetches as errors in DB to prevent retry loops */
11
+ markFailedAsError?: boolean;
9
12
  }
10
13
  export interface IndexPageResult {
11
14
  success: boolean;
@@ -13,6 +16,8 @@ export interface IndexPageResult {
13
16
  skipped?: boolean;
14
17
  /** True if this was an update vs new index */
15
18
  isUpdate?: boolean;
19
+ /** True if content actually changed (hash differs from previous) */
20
+ contentChanged?: boolean;
16
21
  /** Page data if successful */
17
22
  data?: {
18
23
  route: string;
@@ -21,6 +26,7 @@ export interface IndexPageResult {
21
26
  headings: string;
22
27
  keywords: string[];
23
28
  markdown: string;
29
+ contentHash: string;
24
30
  updatedAt: string;
25
31
  };
26
32
  /** Error message if failed */
@@ -30,9 +36,9 @@ export interface IndexPageResult {
30
36
  * Manually index a page's HTML content into database
31
37
  * Use this from custom plugins or API routes to trigger indexing
32
38
  */
33
- export declare function indexPage(route: string, html: string, options?: IndexPageOptions): Promise<IndexPageResult>;
39
+ export declare function indexPage(route: string, html: string, options?: IndexPageOptions, event?: H3Event): Promise<IndexPageResult>;
34
40
  /**
35
41
  * Index a page by fetching its HTML first
36
42
  * Convenience wrapper around indexPage that handles fetching
37
43
  */
38
- export declare function indexPageByRoute(route: string, event?: H3Event, options?: IndexPageOptions): Promise<IndexPageResult>;
44
+ export declare function indexPageByRoute(route: string, event: H3Event, options?: IndexPageOptions): Promise<IndexPageResult>;