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.
- package/README.md +1 -0
- package/dist/module.d.mts +12 -1
- package/dist/module.json +1 -1
- package/dist/module.mjs +226 -352
- package/dist/runtime/index.d.ts +5 -7
- package/dist/runtime/index.js +14 -3
- package/dist/runtime/llms-txt-format.d.ts +8 -0
- package/dist/runtime/llms-txt-format.js +32 -0
- package/dist/runtime/llms-txt-utils.d.ts +2 -5
- package/dist/runtime/llms-txt-utils.js +31 -88
- package/dist/runtime/server/db/index.d.ts +3 -7
- package/dist/runtime/server/db/index.js +14 -31
- package/dist/runtime/server/db/queries.d.ts +106 -26
- package/dist/runtime/server/db/queries.js +330 -33
- package/dist/runtime/server/db/schema-sql.d.ts +3 -0
- package/dist/runtime/server/db/schema-sql.js +71 -0
- package/dist/runtime/server/db/shared.d.ts +88 -0
- package/dist/runtime/server/db/shared.js +117 -0
- package/dist/runtime/server/mcp/resources/pages.js +11 -3
- package/dist/runtime/server/mcp/tools/list-pages.js +33 -7
- package/dist/runtime/server/mcp/tools/search-pages.js +3 -3
- package/dist/runtime/server/middleware/markdown.js +2 -3
- package/dist/runtime/server/middleware/markdown.prerender.js +6 -5
- package/dist/runtime/server/plugins/db-restore.js +10 -8
- package/dist/runtime/server/plugins/sitemap-seeder.js +44 -0
- package/dist/runtime/server/routes/__ai-ready/indexnow.post.d.ts +2 -0
- package/dist/runtime/server/routes/__ai-ready/indexnow.post.js +18 -0
- package/dist/runtime/server/routes/__ai-ready/poll.post.d.ts +8 -0
- package/dist/runtime/server/routes/__ai-ready/poll.post.js +25 -0
- package/dist/runtime/server/routes/__ai-ready/prune.post.d.ts +14 -0
- package/dist/runtime/server/routes/__ai-ready/prune.post.js +21 -0
- package/dist/runtime/server/routes/__ai-ready/status.get.d.ts +2 -0
- package/dist/runtime/server/routes/__ai-ready/status.get.js +28 -0
- package/dist/runtime/server/routes/__ai-ready-debug.get.js +13 -14
- package/dist/runtime/server/routes/indexnow-key.get.d.ts +6 -0
- package/dist/runtime/server/routes/indexnow-key.get.js +10 -0
- package/dist/runtime/server/routes/llms-full.txt.get.d.ts +1 -1
- package/dist/runtime/server/routes/llms-full.txt.get.js +34 -3
- package/dist/runtime/server/tasks/ai-ready-index.d.ts +11 -0
- package/dist/runtime/server/tasks/ai-ready-index.js +39 -0
- package/dist/runtime/server/utils/batchIndex.d.ts +26 -0
- package/dist/runtime/server/utils/batchIndex.js +53 -0
- package/dist/runtime/server/utils/indexPage.d.ts +8 -2
- package/dist/runtime/server/utils/indexPage.js +38 -23
- package/dist/runtime/server/utils/indexnow.d.ts +27 -0
- package/dist/runtime/server/utils/indexnow.js +66 -0
- package/dist/runtime/server/utils/keywords.d.ts +0 -4
- package/dist/runtime/server/utils/keywords.js +0 -3
- package/dist/runtime/server/utils/llms-full.d.ts +11 -0
- package/dist/runtime/server/utils/llms-full.js +39 -0
- package/dist/runtime/server/utils/sitemap.js +3 -3
- package/dist/runtime/server/utils.d.ts +12 -10
- package/dist/runtime/server/utils.js +63 -94
- package/dist/runtime/types.d.ts +64 -40
- package/package.json +28 -29
- package/dist/runtime/mcp.d.ts +0 -32
- package/dist/runtime/mcp.js +0 -5
- package/dist/runtime/server/db/dump.d.ts +0 -29
- package/dist/runtime/server/db/dump.js +0 -29
- package/dist/runtime/server/db/schema.d.ts +0 -8
- package/dist/runtime/server/db/schema.js +0 -124
- package/dist/runtime/server/plugins/page-indexer.js +0 -68
- package/dist/runtime/server/tsconfig.json +0 -3
- package/dist/runtime/server/utils/pageData.d.ts +0 -28
- package/dist/runtime/server/utils/pageData.js +0 -43
- package/mcp.d.ts +0 -4
- /package/dist/runtime/{nuxt → app}/plugins/md-hints.prerender.d.ts +0 -0
- /package/dist/runtime/{nuxt → app}/plugins/md-hints.prerender.js +0 -0
- /package/dist/runtime/server/plugins/{page-indexer.d.ts → sitemap-seeder.d.ts} +0 -0
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
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
|
|
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 {
|
|
2
|
-
|
|
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
|
|
9
|
-
|
|
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
|
|
15
|
-
const results = await searchPages(
|
|
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 {
|
|
5
|
-
import {
|
|
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
|
|
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(
|
|
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 {
|
|
4
|
-
import {
|
|
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)
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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
|
|
22
|
-
await
|
|
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,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,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,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 {
|
|
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
|
|
23
|
-
const
|
|
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
|
|
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 = "
|
|
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.
|
|
81
|
+
} else if (mode === "production" && pages.length === 0) {
|
|
83
82
|
if (!jsonFileStatus.available) {
|
|
84
|
-
issues.push("Production mode with no page data -
|
|
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("
|
|
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.
|
|
89
|
+
} else if (mode === "prerender" && pages.length === 0) {
|
|
91
90
|
issues.push("Prerender mode but no pages found");
|
|
92
|
-
suggestions.push("Check if
|
|
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.
|
|
109
|
-
pages:
|
|
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:
|
|
114
|
+
errorRoutes: errorRoutes.map((e) => e.route)
|
|
116
115
|
},
|
|
117
116
|
jsonFile: jsonFileStatus,
|
|
118
117
|
virtualModules: {
|
|
@@ -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 {
|
|
2
|
-
|
|
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
|
-
|
|
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,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
|
|
44
|
+
export declare function indexPageByRoute(route: string, event: H3Event, options?: IndexPageOptions): Promise<IndexPageResult>;
|