nuxt-ai-ready 1.7.5 → 1.7.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chunks/agent-skills.mjs +1 -1
- package/dist/chunks/prerender.mjs +3 -9
- package/dist/module.d.mts +2 -10
- package/dist/module.json +1 -1
- package/dist/module.mjs +1 -1
- package/dist/runtime/llms-txt-format.d.ts +2 -2
- package/dist/runtime/llms-txt-format.js +8 -4
- package/dist/runtime/llms-txt-utils.js +6 -4
- package/dist/runtime/server/db/drizzle/index.d.ts +1 -1
- package/dist/runtime/server/db/drizzle/index.js +2 -0
- package/dist/runtime/server/db/drizzle/queries.d.ts +11 -1
- package/dist/runtime/server/db/drizzle/queries.js +129 -42
- package/dist/runtime/server/db/drizzle/raw.d.ts +8 -0
- package/dist/runtime/server/db/drizzle/raw.js +49 -0
- package/dist/runtime/server/db/queries.d.ts +26 -3
- package/dist/runtime/server/db/queries.js +121 -33
- package/dist/runtime/server/db/schema/postgres.d.ts +34 -0
- package/dist/runtime/server/db/schema/postgres.js +2 -1
- package/dist/runtime/server/db/schema/sqlite.d.ts +38 -0
- package/dist/runtime/server/db/schema/sqlite.js +2 -1
- package/dist/runtime/server/db/schema-sql.d.ts +1 -1
- package/dist/runtime/server/db/schema-sql.js +11 -3
- package/dist/runtime/server/db/shared.d.ts +8 -5
- package/dist/runtime/server/db/shared.js +61 -10
- package/dist/runtime/server/middleware/markdown.js +25 -21
- package/dist/runtime/server/plugins/sitemap-seeder.js +4 -8
- package/dist/runtime/server/routes/__ai-ready/status.get.js +2 -1
- package/dist/runtime/server/utils/cron-plan.d.ts +15 -0
- package/dist/runtime/server/utils/cron-plan.js +13 -0
- package/dist/runtime/server/utils/i18n.d.ts +6 -38
- package/dist/runtime/server/utils/i18n.js +13 -29
- package/dist/runtime/server/utils/indexPage.js +5 -6
- package/dist/runtime/server/utils/link-header.d.ts +2 -2
- package/dist/runtime/server/utils/link-header.js +9 -5
- package/dist/runtime/server/utils/runCron.d.ts +1 -0
- package/dist/runtime/server/utils/runCron.js +52 -22
- package/dist/runtime/server/utils/sitemap-crawl-state.d.ts +19 -0
- package/dist/runtime/server/utils/sitemap-crawl-state.js +51 -0
- package/dist/runtime/server/utils/sitemap.d.ts +34 -0
- package/dist/runtime/server/utils/sitemap.js +148 -15
- package/dist/shared/{nuxt-ai-ready.CDEc2X7T.mjs → nuxt-ai-ready.DJimiOHi.mjs} +67 -16
- package/package.json +17 -17
|
@@ -2,7 +2,7 @@ import { createHash } from 'node:crypto';
|
|
|
2
2
|
import { realpath, readFile } from 'node:fs/promises';
|
|
3
3
|
import { resolve, isAbsolute, relative, sep } from 'node:path';
|
|
4
4
|
import { parseDocument } from 'yaml';
|
|
5
|
-
import { A as AGENT_SKILLS_SCHEMA } from '../shared/nuxt-ai-ready.
|
|
5
|
+
import { A as AGENT_SKILLS_SCHEMA } from '../shared/nuxt-ai-ready.DJimiOHi.mjs';
|
|
6
6
|
import 'node:module';
|
|
7
7
|
import '@nuxt/kit';
|
|
8
8
|
import 'defu';
|
|
@@ -2,9 +2,10 @@ import { mkdir, writeFile, appendFile, stat, readdir } from 'node:fs/promises';
|
|
|
2
2
|
import { join, dirname, relative, resolve } from 'node:path';
|
|
3
3
|
import { useNuxt, hasNuxtModule, resolveFiles } from '@nuxt/kit';
|
|
4
4
|
import { colorize } from 'consola/utils';
|
|
5
|
+
import { resolveLocaleFromRoute } from 'nuxtseo-shared/i18n-runtime';
|
|
5
6
|
import { collectSitemap } from 'sitemapd/parse';
|
|
6
7
|
import { withLeadingSlash, withBase, joinURL } from 'ufo';
|
|
7
|
-
import { l as logger, M as MARKDOWN_LINK_AVAILABILITY_FILE } from '../shared/nuxt-ai-ready.
|
|
8
|
+
import { l as logger, M as MARKDOWN_LINK_AVAILABILITY_FILE } from '../shared/nuxt-ai-ready.DJimiOHi.mjs';
|
|
8
9
|
import { normalizePagePath, toMarkdownPath } from '../../dist/runtime/markdown-path.js';
|
|
9
10
|
import { toLogicalRoute, toDeployedRoute } from '../../dist/runtime/route-path.js';
|
|
10
11
|
import { initSchema, computeContentHash, insertPage, queryAllPages, exportDbDump } from '../../dist/runtime/server/db/shared.js';
|
|
@@ -151,14 +152,7 @@ function flattenHeadings(headings) {
|
|
|
151
152
|
return (headings || []).map((h) => Object.entries(h).map(([tag, text]) => `${tag}:${text}`).join("")).join("|");
|
|
152
153
|
}
|
|
153
154
|
function resolveRouteLocale(route, i18n) {
|
|
154
|
-
|
|
155
|
-
return "";
|
|
156
|
-
if (i18n.strategy === "no_prefix")
|
|
157
|
-
return i18n.defaultLocale;
|
|
158
|
-
const segments = route.split("/").filter(Boolean);
|
|
159
|
-
const first = segments[0];
|
|
160
|
-
const matched = first ? i18n.locales.find((l) => l.code === first) : void 0;
|
|
161
|
-
return matched ? matched.code : i18n.defaultLocale;
|
|
155
|
+
return i18n ? resolveLocaleFromRoute(route, i18n).locale : "";
|
|
162
156
|
}
|
|
163
157
|
async function processMarkdownRoute(state, nuxt, route, parsed, lastmod, options) {
|
|
164
158
|
route = normalizePagePath(route);
|
package/dist/module.d.mts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import * as _nuxt_schema from '@nuxt/schema';
|
|
2
2
|
import { LlmsTxtConfig, ContentNegotiationPolicy, ModuleOptions } from '../dist/runtime/types.js';
|
|
3
3
|
export { AgentSkillConfig, AgentSkillsConfig, AgentSkillsIndex, AgentSkillsIndexEntry, ApiCatalogConfig, ApiCatalogEntry, ApiCatalogLinkTarget, ApiCatalogLinks, ExternalAgentSkillConfig, LocalAgentSkillConfig, McpServerCardConfig, ModuleOptions } from '../dist/runtime/types.js';
|
|
4
|
+
import { RuntimeI18nConfig } from 'nuxtseo-shared/i18n-runtime';
|
|
4
5
|
import { ResolvedWebMcpToolsConfig } from '../dist/runtime/site-tool-config.js';
|
|
5
6
|
export { GetPageMarkdownToolOptions, ListPagesToolOptions, McpSiteToolAttachmentOptions, SearchPagesToolOptions, SiteToolOptions, SiteToolsConfig, WebMcpSiteToolAttachmentOptions } from '../dist/runtime/site-tool-config.js';
|
|
6
7
|
import { AiCatalog } from '../dist/runtime/server/utils/discovery-response.js';
|
|
@@ -86,16 +87,7 @@ interface ModulePublicRuntimeConfig {
|
|
|
86
87
|
runtimeSyncSecret?: string;
|
|
87
88
|
indexNow?: string;
|
|
88
89
|
sitemapPrerendered: boolean;
|
|
89
|
-
i18n?:
|
|
90
|
-
defaultLocale: string;
|
|
91
|
-
strategy: 'no_prefix' | 'prefix_except_default' | 'prefix' | 'prefix_and_default';
|
|
92
|
-
locales: Array<{
|
|
93
|
-
code: string;
|
|
94
|
-
hreflang: string;
|
|
95
|
-
name?: string;
|
|
96
|
-
nativeName?: string;
|
|
97
|
-
}>;
|
|
98
|
-
} | null;
|
|
90
|
+
i18n?: RuntimeI18nConfig | null;
|
|
99
91
|
ftsTokenizer?: string;
|
|
100
92
|
aiCatalog?: {
|
|
101
93
|
cacheMaxAge: number;
|
package/dist/module.json
CHANGED
package/dist/module.mjs
CHANGED
|
@@ -7,7 +7,7 @@ import 'defu';
|
|
|
7
7
|
import 'nuxt-site-config/kit';
|
|
8
8
|
import 'nuxtseo-shared/kit';
|
|
9
9
|
import 'pkg-types';
|
|
10
|
-
export { m as default } from './shared/nuxt-ai-ready.
|
|
10
|
+
export { m as default } from './shared/nuxt-ai-ready.DJimiOHi.mjs';
|
|
11
11
|
import '../dist/runtime/server/utils/discovery-response.js';
|
|
12
12
|
import 'node:url';
|
|
13
13
|
import 'pathe';
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Pure formatting functions for llms.txt - no runtime dependencies
|
|
3
3
|
*/
|
|
4
|
-
import type { RuntimeI18nConfig } from './server/utils/i18n.js';
|
|
4
|
+
import type { RuntimeI18nConfig, RuntimeRouteContext } from './server/utils/i18n.js';
|
|
5
5
|
import type { LlmsTxtConfig } from './types.js';
|
|
6
|
-
export declare function formatAvailableLanguagesSection(i18n: RuntimeI18nConfig, pageCounts: Map<string, number>, resolveHref?: (pathname: string) => string): string[];
|
|
6
|
+
export declare function formatAvailableLanguagesSection(i18n: RuntimeI18nConfig, pageCounts: Map<string, number>, resolveHref?: (pathname: string) => string, routeContext?: RuntimeRouteContext): string[];
|
|
7
7
|
interface LlmsTxtPageLink {
|
|
8
8
|
pathname: string;
|
|
9
9
|
href?: string;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { localePath } from "./server/utils/i18n.js";
|
|
1
|
+
import { computeLocaleAlternates, localePath, resolveLocaleAlternateUrl } from "./server/utils/i18n.js";
|
|
2
2
|
const RE_INLINE_WHITESPACE = /\s+/g;
|
|
3
3
|
const RE_LINK_TITLE_BRACKET = /[[\]]/g;
|
|
4
4
|
const RE_LINK_HREF_UNSAFE = /[\s()]/g;
|
|
@@ -62,17 +62,21 @@ function normalizeOptionalSections(sections) {
|
|
|
62
62
|
function normalizeRequiredSections(sections) {
|
|
63
63
|
return sections.filter((section) => section.links?.length).map(normalizeSection);
|
|
64
64
|
}
|
|
65
|
-
export function formatAvailableLanguagesSection(i18n, pageCounts, resolveHref = (pathname) => pathname) {
|
|
65
|
+
export function formatAvailableLanguagesSection(i18n, pageCounts, resolveHref = (pathname) => pathname, routeContext = {}) {
|
|
66
66
|
const lines = ["## Available Languages on Website", ""];
|
|
67
|
+
const rootAlternates = new Map(computeLocaleAlternates("/", i18n, routeContext).map((alternate) => [alternate.code, alternate]));
|
|
67
68
|
for (const locale of i18n.locales) {
|
|
68
69
|
const isDefault = locale.code === i18n.defaultLocale;
|
|
69
|
-
const
|
|
70
|
+
const alternate = rootAlternates.get(locale.code);
|
|
71
|
+
const prefix = alternate?.path ?? localePath("/", locale.code, i18n, routeContext);
|
|
72
|
+
const resolvedPath = resolveHref(prefix);
|
|
73
|
+
const href = alternate ? resolveLocaleAlternateUrl({ ...alternate, path: resolvedPath }, (candidate) => candidate) : resolvedPath;
|
|
70
74
|
const count = pageCounts.get(locale.code) ?? 0;
|
|
71
75
|
const display = locale.nativeName ? `${locale.nativeName} (${locale.code})` : locale.name ? `${locale.name} (${locale.code})` : locale.code;
|
|
72
76
|
const suffix = isDefault ? "content included below" : "visit this language for content";
|
|
73
77
|
lines.push(normalizeLink({
|
|
74
78
|
title: display,
|
|
75
|
-
href
|
|
79
|
+
href,
|
|
76
80
|
description: `${count} pages; ${suffix}.`
|
|
77
81
|
}));
|
|
78
82
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { decodePath } from "ufo";
|
|
2
|
+
import { getRequestURL } from "#nuxtseo/h3";
|
|
2
3
|
import { useRuntimeConfig } from "#nuxtseo/nitro";
|
|
3
4
|
import { getSiteConfig } from "#site-config/server/composables/getSiteConfig";
|
|
4
5
|
import { withSiteTrailingSlash, withSiteUrl } from "#site-config/server/composables/utils";
|
|
@@ -122,6 +123,7 @@ export async function buildLlmsTxt(event) {
|
|
|
122
123
|
const siteConfig = getSiteConfig(event);
|
|
123
124
|
const llmsTxtConfig = aiReadyConfig.llmsTxt;
|
|
124
125
|
const i18n = aiReadyConfig.i18n;
|
|
126
|
+
const i18nContext = { host: getRequestURL(event).host };
|
|
125
127
|
const baseURL = runtimeConfig.app.baseURL;
|
|
126
128
|
const resolvePath = (path) => withSiteTrailingSlash(event, toDeployedRoute(path, baseURL));
|
|
127
129
|
const resolveUrl = (path) => withSiteUrl(event, toDeployedRoute(path, baseURL));
|
|
@@ -192,7 +194,7 @@ Canonical Origin: ${canonicalSiteUrl}`);
|
|
|
192
194
|
const other = [];
|
|
193
195
|
for (const pathname of sitemapPaths) {
|
|
194
196
|
if (!seenPaths.has(pathname) && !errorSet.has(pathname)) {
|
|
195
|
-
const locale = i18n ? resolveLocaleFromRoute(pathname, i18n).locale : void 0;
|
|
197
|
+
const locale = i18n ? resolveLocaleFromRoute(pathname, i18n, i18nContext).locale : void 0;
|
|
196
198
|
other.push({ pathname, locale });
|
|
197
199
|
seenPaths.add(pathname);
|
|
198
200
|
}
|
|
@@ -215,16 +217,16 @@ Canonical Origin: ${canonicalSiteUrl}`);
|
|
|
215
217
|
const pageCounts = /* @__PURE__ */ new Map();
|
|
216
218
|
for (const locale of i18n.locales) pageCounts.set(locale.code, 0);
|
|
217
219
|
for (const p of [...prerendered, ...other]) {
|
|
218
|
-
const code = p.locale || resolveLocaleFromRoute(p.pathname, i18n).locale;
|
|
220
|
+
const code = p.locale || resolveLocaleFromRoute(p.pathname, i18n, i18nContext).locale;
|
|
219
221
|
pageCounts.set(code, (pageCounts.get(code) ?? 0) + 1);
|
|
220
222
|
}
|
|
221
|
-
parts.push(...formatAvailableLanguagesSection(i18n, pageCounts, resolvePageHref));
|
|
223
|
+
parts.push(...formatAvailableLanguagesSection(i18n, pageCounts, resolvePageHref, i18nContext));
|
|
222
224
|
parts.push("");
|
|
223
225
|
}
|
|
224
226
|
const isDefaultLocale = (item) => {
|
|
225
227
|
if (!i18n)
|
|
226
228
|
return true;
|
|
227
|
-
const code = item.locale || resolveLocaleFromRoute(item.pathname, i18n).locale;
|
|
229
|
+
const code = item.locale || resolveLocaleFromRoute(item.pathname, i18n, i18nContext).locale;
|
|
228
230
|
return code === i18n.defaultLocale;
|
|
229
231
|
};
|
|
230
232
|
const filteredPrerendered = i18n ? prerendered.filter(isDefaultLocale) : prerendered;
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
export * from '#ai-ready-virtual/db-schema.mjs';
|
|
5
5
|
export { closeDrizzle, useDrizzle } from './client.js';
|
|
6
6
|
export type { DatabaseDialect, DrizzleDatabase } from './client.js';
|
|
7
|
-
export { completeCronRun, countPages, countPagesNeedingIndexNowSync, deleteInfoValue, deletePage, getAllPages, getContentHashes, getInfoValue, getNextSitemapToCrawl, getPageByRoute, getPageLastmods, getPagesNeedingIndexNowSync, getPendingPages, getRecentCronRuns, getSitemapStatus, initSchema, markIndexNowSynced, markPageIndexed, markRoutesPending, markSitemapCrawled, markSitemapError, resetSitemapErrors, searchPages, seedRoutes, setInfoValue, startCronRun, syncSitemaps, upsertPage, } from './queries.js';
|
|
7
|
+
export { completeCronRun, countPages, countPagesNeedingIndexNowSync, deleteInfoValue, deletePage, getAllPages, getContentHashes, getInfoValue, getNextSitemapToCrawl, getPageByRoute, getPageLastmods, getPagesNeedingIndexNowSync, getPendingPages, getRecentCronRuns, getSitemapStatus, initSchema, markIndexNowSynced, markPageIndexed, markRoutesPending, markSitemapCrawled, markSitemapCrawlPartial, markSitemapError, markSitemapSeeded, resetSitemapErrors, searchPages, seedRoutes, setInfoValue, startCronRun, syncSitemaps, upsertPage, } from './queries.js';
|
|
8
8
|
export type { CronRunOutput, PageInput, PageMetaOutput, PageOutput, SitemapOutput, } from './queries.js';
|
|
9
9
|
export { useRawDb } from './raw.js';
|
|
10
10
|
export type { RawExecutor } from './raw.js';
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { H3Event } from '#nuxtseo/h3';
|
|
2
|
+
import type { SitemapCrawlState } from '../../utils/sitemap-crawl-state.js';
|
|
2
3
|
export interface PageInput {
|
|
3
4
|
route: string;
|
|
4
5
|
title: string;
|
|
@@ -155,6 +156,10 @@ export interface SitemapOutput {
|
|
|
155
156
|
urlCount: number;
|
|
156
157
|
errorCount: number;
|
|
157
158
|
lastError: string | null;
|
|
159
|
+
continuing: boolean;
|
|
160
|
+
}
|
|
161
|
+
interface SitemapEntry extends Omit<SitemapOutput, 'continuing'> {
|
|
162
|
+
crawlState: SitemapCrawlState | null;
|
|
158
163
|
}
|
|
159
164
|
/**
|
|
160
165
|
* Sync sitemaps from config
|
|
@@ -169,11 +174,15 @@ export declare function syncSitemaps(event: H3Event | undefined, sitemapList: Ar
|
|
|
169
174
|
/**
|
|
170
175
|
* Get next sitemap to crawl
|
|
171
176
|
*/
|
|
172
|
-
export declare function getNextSitemapToCrawl(event: H3Event | undefined, minIntervalMinutes?: number): Promise<
|
|
177
|
+
export declare function getNextSitemapToCrawl(event: H3Event | undefined, minIntervalMinutes?: number): Promise<SitemapEntry | null>;
|
|
173
178
|
/**
|
|
174
179
|
* Mark sitemap as crawled
|
|
175
180
|
*/
|
|
176
181
|
export declare function markSitemapCrawled(event: H3Event | undefined, name: string, urlCount: number): Promise<void>;
|
|
182
|
+
/** Record a deferred sitemap hook seed only when no crawl is in progress. */
|
|
183
|
+
export declare function markSitemapSeeded(event: H3Event | undefined, name: string, urlCount: number, expectedLastCrawledAt: number | null): Promise<void>;
|
|
184
|
+
/** Persist a resumable sitemap crawl without incrementing its error budget. */
|
|
185
|
+
export declare function markSitemapCrawlPartial(event: H3Event | undefined, name: string, state: SitemapCrawlState): Promise<void>;
|
|
177
186
|
/**
|
|
178
187
|
* Mark sitemap error
|
|
179
188
|
*/
|
|
@@ -190,3 +199,4 @@ export declare function resetSitemapErrors(event: H3Event | undefined): Promise<
|
|
|
190
199
|
* Seed routes from sitemap
|
|
191
200
|
*/
|
|
192
201
|
export declare function seedRoutes(event: H3Event | undefined, routes: string[]): Promise<number>;
|
|
202
|
+
export {};
|
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
import { and, count, desc, eq, gt, isNull, like, lt, or, sql } from "drizzle-orm";
|
|
1
|
+
import { and, count, desc, eq, gt, isNotNull, isNull, like, lt, or, sql } from "drizzle-orm";
|
|
2
2
|
import { cronRuns, info, pages, sitemaps } from "#ai-ready-virtual/db-schema.mjs";
|
|
3
3
|
import { useRuntimeConfig } from "#nuxtseo/nitro";
|
|
4
|
+
import { parseSitemapCrawlState, serializeSitemapCrawlState } from "../../utils/sitemap-crawl-state.js";
|
|
4
5
|
import { resolveFtsTokenizer as validateFtsTokenizer } from "../schema-sql.js";
|
|
5
6
|
import { useDrizzle } from "./client.js";
|
|
7
|
+
import { useRawDb } from "./raw.js";
|
|
6
8
|
function resolveFtsTokenizer(event) {
|
|
7
9
|
const cfg = useRuntimeConfig(event);
|
|
8
10
|
return validateFtsTokenizer(cfg["nuxt-ai-ready"]?.ftsTokenizer);
|
|
@@ -76,12 +78,22 @@ export async function upsertPage(event, page) {
|
|
|
76
78
|
}
|
|
77
79
|
export async function getAllPages(event, options) {
|
|
78
80
|
const client = await useDrizzle(event);
|
|
79
|
-
|
|
81
|
+
const cols = options?.excludeMarkdown ? {
|
|
82
|
+
route: pages.route,
|
|
83
|
+
title: pages.title,
|
|
84
|
+
description: pages.description,
|
|
85
|
+
headings: pages.headings,
|
|
86
|
+
keywords: pages.keywords,
|
|
87
|
+
contentHash: pages.contentHash,
|
|
88
|
+
updatedAt: pages.updatedAt,
|
|
89
|
+
isError: pages.isError
|
|
90
|
+
} : void 0;
|
|
91
|
+
let query = cols ? client.db.select(cols).from(pages) : client.db.select().from(pages);
|
|
80
92
|
if (!options?.includeErrors) {
|
|
81
93
|
query = query.where(eq(pages.isError, 0));
|
|
82
94
|
}
|
|
83
95
|
const rows = await query;
|
|
84
|
-
return rows.map((row) =>
|
|
96
|
+
return rows.map((row) => cols ? rowToMeta(row) : rowToPage(row));
|
|
85
97
|
}
|
|
86
98
|
export async function getPageByRoute(event, route) {
|
|
87
99
|
const client = await useDrizzle(event);
|
|
@@ -178,8 +190,18 @@ export async function markPageIndexed(event, route) {
|
|
|
178
190
|
export async function markRoutesPending(event, routes) {
|
|
179
191
|
if (routes.length === 0)
|
|
180
192
|
return;
|
|
181
|
-
const
|
|
182
|
-
|
|
193
|
+
const db = await useRawDb(event);
|
|
194
|
+
const stmts = [];
|
|
195
|
+
const D1_MAX_IN_ROUTES = 99;
|
|
196
|
+
for (let i = 0; i < routes.length; i += D1_MAX_IN_ROUTES) {
|
|
197
|
+
const batch = routes.slice(i, i + D1_MAX_IN_ROUTES);
|
|
198
|
+
const placeholders = batch.map(() => "?").join(",");
|
|
199
|
+
stmts.push({
|
|
200
|
+
sql: `UPDATE ai_ready_pages SET indexed = 0 WHERE route IN (${placeholders})`,
|
|
201
|
+
params: batch
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
await db.batch(stmts);
|
|
183
205
|
}
|
|
184
206
|
export async function getContentHashes(event) {
|
|
185
207
|
const client = await useDrizzle(event);
|
|
@@ -202,7 +224,7 @@ export async function deleteInfoValue(event, key) {
|
|
|
202
224
|
const client = await useDrizzle(event);
|
|
203
225
|
await client.db.delete(info).where(eq(info.id, key));
|
|
204
226
|
}
|
|
205
|
-
const SCHEMA_VERSION = "v2.
|
|
227
|
+
const SCHEMA_VERSION = "v2.2.0-drizzle";
|
|
206
228
|
export async function initSchema(event) {
|
|
207
229
|
const client = await useDrizzle(event);
|
|
208
230
|
const tokenizer = resolveFtsTokenizer(event);
|
|
@@ -331,7 +353,8 @@ async function createSQLiteTables(client, ftsTokenizer) {
|
|
|
331
353
|
last_crawled_at INTEGER,
|
|
332
354
|
url_count INTEGER DEFAULT 0,
|
|
333
355
|
error_count INTEGER DEFAULT 0,
|
|
334
|
-
last_error TEXT
|
|
356
|
+
last_error TEXT,
|
|
357
|
+
crawl_state TEXT
|
|
335
358
|
)`,
|
|
336
359
|
// Indexes
|
|
337
360
|
sql`CREATE INDEX IF NOT EXISTS idx_ai_ready_pages_route ON ai_ready_pages(route)`,
|
|
@@ -345,7 +368,9 @@ async function createSQLiteTables(client, ftsTokenizer) {
|
|
|
345
368
|
route, title, description, markdown, headings, keywords,
|
|
346
369
|
content=ai_ready_pages, content_rowid=id, tokenize='${ftsTokenizer}'
|
|
347
370
|
)`),
|
|
348
|
-
// FTS triggers
|
|
371
|
+
// FTS triggers. The UPDATE trigger only fires when a searchable column
|
|
372
|
+
// actually changed; updates to bookkeeping columns (indexed, indexed_at,
|
|
373
|
+
// indexnow_synced_at, last_seen_at…) skip the delete+reinsert FTS churn.
|
|
349
374
|
sql`CREATE TRIGGER IF NOT EXISTS ai_ready_pages_ai AFTER INSERT ON ai_ready_pages BEGIN
|
|
350
375
|
INSERT INTO ai_ready_pages_fts(rowid, route, title, description, markdown, headings, keywords)
|
|
351
376
|
VALUES (new.id, new.route, new.title, new.description, new.markdown, new.headings, new.keywords);
|
|
@@ -354,7 +379,14 @@ async function createSQLiteTables(client, ftsTokenizer) {
|
|
|
354
379
|
INSERT INTO ai_ready_pages_fts(ai_ready_pages_fts, rowid, route, title, description, markdown, headings, keywords)
|
|
355
380
|
VALUES('delete', old.id, old.route, old.title, old.description, old.markdown, old.headings, old.keywords);
|
|
356
381
|
END`,
|
|
357
|
-
sql`CREATE TRIGGER IF NOT EXISTS ai_ready_pages_au AFTER UPDATE ON ai_ready_pages
|
|
382
|
+
sql`CREATE TRIGGER IF NOT EXISTS ai_ready_pages_au AFTER UPDATE ON ai_ready_pages
|
|
383
|
+
WHEN old.route IS NOT new.route
|
|
384
|
+
OR old.title IS NOT new.title
|
|
385
|
+
OR old.description IS NOT new.description
|
|
386
|
+
OR old.markdown IS NOT new.markdown
|
|
387
|
+
OR old.headings IS NOT new.headings
|
|
388
|
+
OR old.keywords IS NOT new.keywords
|
|
389
|
+
BEGIN
|
|
358
390
|
INSERT INTO ai_ready_pages_fts(ai_ready_pages_fts, rowid, route, title, description, markdown, headings, keywords)
|
|
359
391
|
VALUES('delete', old.id, old.route, old.title, old.description, old.markdown, old.headings, old.keywords);
|
|
360
392
|
INSERT INTO ai_ready_pages_fts(rowid, route, title, description, markdown, headings, keywords)
|
|
@@ -418,7 +450,8 @@ async function createPostgresTables(client) {
|
|
|
418
450
|
last_crawled_at INTEGER,
|
|
419
451
|
url_count INTEGER DEFAULT 0,
|
|
420
452
|
error_count INTEGER DEFAULT 0,
|
|
421
|
-
last_error TEXT
|
|
453
|
+
last_error TEXT,
|
|
454
|
+
crawl_state TEXT
|
|
422
455
|
)`,
|
|
423
456
|
// Indexes
|
|
424
457
|
sql`CREATE INDEX IF NOT EXISTS idx_ai_ready_pages_route ON ai_ready_pages(route)`,
|
|
@@ -462,17 +495,19 @@ export async function countPagesNeedingIndexNowSync(event) {
|
|
|
462
495
|
export async function markIndexNowSynced(event, routes) {
|
|
463
496
|
if (routes.length === 0)
|
|
464
497
|
return;
|
|
465
|
-
const client = await useDrizzle(event);
|
|
466
498
|
const now = Date.now();
|
|
499
|
+
const db = await useRawDb(event);
|
|
500
|
+
const stmts = [];
|
|
467
501
|
const D1_MAX_IN_ROUTES = 99;
|
|
468
502
|
for (let i = 0; i < routes.length; i += D1_MAX_IN_ROUTES) {
|
|
469
503
|
const batch = routes.slice(i, i + D1_MAX_IN_ROUTES);
|
|
470
504
|
const placeholders = batch.map(() => "?").join(",");
|
|
471
|
-
|
|
472
|
-
sql
|
|
473
|
-
[now, ...batch]
|
|
474
|
-
);
|
|
505
|
+
stmts.push({
|
|
506
|
+
sql: `UPDATE ai_ready_pages SET indexnow_synced_at = ? WHERE route IN (${placeholders})`,
|
|
507
|
+
params: [now, ...batch]
|
|
508
|
+
});
|
|
475
509
|
}
|
|
510
|
+
await db.batch(stmts);
|
|
476
511
|
}
|
|
477
512
|
function rowToCronRun(row) {
|
|
478
513
|
return {
|
|
@@ -517,19 +552,24 @@ export async function getRecentCronRuns(event, limit = 10) {
|
|
|
517
552
|
return rows.map(rowToCronRun);
|
|
518
553
|
}
|
|
519
554
|
function rowToSitemap(row) {
|
|
555
|
+
const parsedState = parseSitemapCrawlState(row.crawlState ?? null);
|
|
556
|
+
if (parsedState._tag === "error")
|
|
557
|
+
throw new Error(`Invalid crawl state for sitemap ${row.name}: ${parsedState.error}`);
|
|
520
558
|
return {
|
|
521
559
|
name: row.name,
|
|
522
560
|
route: row.route,
|
|
523
561
|
lastCrawledAt: row.lastCrawledAt,
|
|
524
562
|
urlCount: row.urlCount || 0,
|
|
525
563
|
errorCount: row.errorCount || 0,
|
|
526
|
-
lastError: row.lastError
|
|
564
|
+
lastError: row.lastError,
|
|
565
|
+
crawlState: parsedState.state
|
|
527
566
|
};
|
|
528
567
|
}
|
|
529
568
|
export async function syncSitemaps(event, sitemapList) {
|
|
530
569
|
const client = await useDrizzle(event);
|
|
531
|
-
const existing = await client.db.select({ name: sitemaps.name }).from(sitemaps);
|
|
570
|
+
const existing = await client.db.select({ name: sitemaps.name, route: sitemaps.route }).from(sitemaps);
|
|
532
571
|
const existingNames = new Set(existing.map((r) => r.name));
|
|
572
|
+
const existingRoutes = new Map(existing.map((row) => [row.name, row.route]));
|
|
533
573
|
const configNames = new Set(sitemapList.map((s) => s.name));
|
|
534
574
|
let added = 0;
|
|
535
575
|
let removed = 0;
|
|
@@ -537,6 +577,15 @@ export async function syncSitemaps(event, sitemapList) {
|
|
|
537
577
|
if (!existingNames.has(sitemap.name)) {
|
|
538
578
|
await client.db.insert(sitemaps).values({ name: sitemap.name, route: sitemap.route });
|
|
539
579
|
added++;
|
|
580
|
+
} else if (existingRoutes.get(sitemap.name) !== sitemap.route) {
|
|
581
|
+
await client.db.update(sitemaps).set({
|
|
582
|
+
route: sitemap.route,
|
|
583
|
+
lastCrawledAt: null,
|
|
584
|
+
urlCount: 0,
|
|
585
|
+
errorCount: 0,
|
|
586
|
+
lastError: null,
|
|
587
|
+
crawlState: null
|
|
588
|
+
}).where(eq(sitemaps.name, sitemap.name));
|
|
540
589
|
}
|
|
541
590
|
}
|
|
542
591
|
for (const name of existingNames) {
|
|
@@ -550,6 +599,12 @@ export async function syncSitemaps(event, sitemapList) {
|
|
|
550
599
|
export async function getNextSitemapToCrawl(event, minIntervalMinutes = 5) {
|
|
551
600
|
const client = await useDrizzle(event);
|
|
552
601
|
const threshold = Date.now() - minIntervalMinutes * 60 * 1e3;
|
|
602
|
+
const continuationRow = await client.db.select().from(sitemaps).where(and(
|
|
603
|
+
isNotNull(sitemaps.crawlState),
|
|
604
|
+
eq(sitemaps.errorCount, 0)
|
|
605
|
+
)).orderBy(sitemaps.lastCrawledAt).limit(1);
|
|
606
|
+
if (continuationRow.length)
|
|
607
|
+
return rowToSitemap(continuationRow[0]);
|
|
553
608
|
const errorRow = await client.db.select().from(sitemaps).where(
|
|
554
609
|
and(
|
|
555
610
|
gt(sitemaps.errorCount, 0),
|
|
@@ -574,12 +629,37 @@ export async function getNextSitemapToCrawl(event, minIntervalMinutes = 5) {
|
|
|
574
629
|
return row.length ? rowToSitemap(row[0]) : null;
|
|
575
630
|
}
|
|
576
631
|
export async function markSitemapCrawled(event, name, urlCount) {
|
|
632
|
+
const client = await useDrizzle(event);
|
|
633
|
+
await client.db.update(sitemaps).set({
|
|
634
|
+
lastCrawledAt: Date.now(),
|
|
635
|
+
urlCount,
|
|
636
|
+
errorCount: 0,
|
|
637
|
+
lastError: null,
|
|
638
|
+
crawlState: null
|
|
639
|
+
}).where(eq(sitemaps.name, name));
|
|
640
|
+
}
|
|
641
|
+
export async function markSitemapSeeded(event, name, urlCount, expectedLastCrawledAt) {
|
|
577
642
|
const client = await useDrizzle(event);
|
|
578
643
|
await client.db.update(sitemaps).set({
|
|
579
644
|
lastCrawledAt: Date.now(),
|
|
580
645
|
urlCount,
|
|
581
646
|
errorCount: 0,
|
|
582
647
|
lastError: null
|
|
648
|
+
}).where(and(
|
|
649
|
+
eq(sitemaps.name, name),
|
|
650
|
+
isNull(sitemaps.crawlState),
|
|
651
|
+
eq(sitemaps.errorCount, 0),
|
|
652
|
+
expectedLastCrawledAt === null ? isNull(sitemaps.lastCrawledAt) : eq(sitemaps.lastCrawledAt, expectedLastCrawledAt)
|
|
653
|
+
));
|
|
654
|
+
}
|
|
655
|
+
export async function markSitemapCrawlPartial(event, name, state) {
|
|
656
|
+
const client = await useDrizzle(event);
|
|
657
|
+
await client.db.update(sitemaps).set({
|
|
658
|
+
lastCrawledAt: Date.now(),
|
|
659
|
+
urlCount: state.urlsObserved,
|
|
660
|
+
errorCount: 0,
|
|
661
|
+
lastError: null,
|
|
662
|
+
crawlState: serializeSitemapCrawlState(state)
|
|
583
663
|
}).where(eq(sitemaps.name, name));
|
|
584
664
|
}
|
|
585
665
|
export async function markSitemapError(event, name, error) {
|
|
@@ -587,23 +667,31 @@ export async function markSitemapError(event, name, error) {
|
|
|
587
667
|
await client.db.update(sitemaps).set({
|
|
588
668
|
lastCrawledAt: Date.now(),
|
|
589
669
|
errorCount: sql`${sitemaps.errorCount} + 1`,
|
|
590
|
-
lastError: error
|
|
670
|
+
lastError: error,
|
|
671
|
+
crawlState: null
|
|
591
672
|
}).where(eq(sitemaps.name, name));
|
|
592
673
|
}
|
|
593
674
|
export async function getSitemapStatus(event) {
|
|
594
675
|
const client = await useDrizzle(event);
|
|
595
676
|
const rows = await client.db.select().from(sitemaps).orderBy(sitemaps.name);
|
|
596
|
-
return rows.map(
|
|
677
|
+
return rows.map((row) => {
|
|
678
|
+
const { crawlState, ...entry } = rowToSitemap(row);
|
|
679
|
+
return { ...entry, continuing: crawlState !== null };
|
|
680
|
+
});
|
|
597
681
|
}
|
|
598
682
|
export async function resetSitemapErrors(event) {
|
|
599
683
|
const client = await useDrizzle(event);
|
|
600
|
-
const countResult = await client.db.select({ count: count() }).from(sitemaps).where(
|
|
684
|
+
const countResult = await client.db.select({ count: count() }).from(sitemaps).where(or(
|
|
685
|
+
gt(sitemaps.errorCount, 0),
|
|
686
|
+
sql`${sitemaps.crawlState} IS NOT NULL`
|
|
687
|
+
));
|
|
601
688
|
const errorCount = countResult[0]?.count || 0;
|
|
602
689
|
if (errorCount > 0) {
|
|
603
690
|
await client.db.update(sitemaps).set({
|
|
604
691
|
errorCount: 0,
|
|
605
692
|
lastError: null,
|
|
606
|
-
lastCrawledAt: null
|
|
693
|
+
lastCrawledAt: null,
|
|
694
|
+
crawlState: null
|
|
607
695
|
});
|
|
608
696
|
}
|
|
609
697
|
return errorCount;
|
|
@@ -611,29 +699,28 @@ export async function resetSitemapErrors(event) {
|
|
|
611
699
|
export async function seedRoutes(event, routes) {
|
|
612
700
|
if (routes.length === 0)
|
|
613
701
|
return 0;
|
|
614
|
-
const
|
|
702
|
+
const byRoute = /* @__PURE__ */ new Map();
|
|
703
|
+
for (const route of routes)
|
|
704
|
+
byRoute.set(route, normalizeRouteKey(route));
|
|
705
|
+
const ROWS_PER_INSERT = 20;
|
|
706
|
+
const db = await useRawDb(event);
|
|
615
707
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
616
708
|
const nowMs = Date.now();
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
source: "runtime",
|
|
631
|
-
lastSeenAt: nowMs
|
|
632
|
-
};
|
|
633
|
-
await client.db.insert(pages).values(values).onConflictDoUpdate({
|
|
634
|
-
target: pages.route,
|
|
635
|
-
set: { lastSeenAt: nowMs }
|
|
709
|
+
const entries = [...byRoute.entries()];
|
|
710
|
+
const stmts = [];
|
|
711
|
+
for (let i = 0; i < entries.length; i += ROWS_PER_INSERT) {
|
|
712
|
+
const batch = entries.slice(i, i + ROWS_PER_INSERT);
|
|
713
|
+
const valuesSql = batch.map(() => `(?, ?, '', '', '', '[]', '[]', ?, 0, 0, 0, 'runtime', ?)`).join(", ");
|
|
714
|
+
const params = batch.flatMap(([route, routeKey]) => [route, routeKey, now, nowMs]);
|
|
715
|
+
stmts.push({
|
|
716
|
+
sql: `
|
|
717
|
+
INSERT INTO ai_ready_pages (route, route_key, title, description, markdown, headings, keywords, updated_at, indexed_at, is_error, indexed, source, last_seen_at)
|
|
718
|
+
VALUES ${valuesSql}
|
|
719
|
+
ON CONFLICT(route) DO UPDATE SET last_seen_at = excluded.last_seen_at
|
|
720
|
+
`,
|
|
721
|
+
params
|
|
636
722
|
});
|
|
637
723
|
}
|
|
638
|
-
|
|
724
|
+
await db.batch(stmts);
|
|
725
|
+
return byRoute.size;
|
|
639
726
|
}
|
|
@@ -16,6 +16,14 @@ export declare function getRawExecutor(client: DrizzleDatabase): {
|
|
|
16
16
|
all<T = Record<string, unknown>>(query: string, params?: unknown[]): Promise<T[]>;
|
|
17
17
|
first<T = Record<string, unknown>>(query: string, params?: unknown[]): Promise<T | undefined>;
|
|
18
18
|
exec(query: string, params?: unknown[]): Promise<void>;
|
|
19
|
+
/**
|
|
20
|
+
* Execute many statements in as few round-trips as the driver allows.
|
|
21
|
+
* Statements run in order; on a remote driver each chunk is one request.
|
|
22
|
+
*/
|
|
23
|
+
batch(queries: {
|
|
24
|
+
sql: string;
|
|
25
|
+
params?: unknown[];
|
|
26
|
+
}[]): Promise<void>;
|
|
19
27
|
};
|
|
20
28
|
export type RawExecutor = ReturnType<typeof getRawExecutor>;
|
|
21
29
|
/**
|
|
@@ -4,6 +4,7 @@ export function registerDriver(db, type, driver) {
|
|
|
4
4
|
driverCache.set(db, { type, driver });
|
|
5
5
|
}
|
|
6
6
|
const RE_PARAM_PLACEHOLDER = /\?/g;
|
|
7
|
+
const MAX_BATCH_STATEMENTS = 100;
|
|
7
8
|
export function getRawExecutor(client) {
|
|
8
9
|
const cached = driverCache.get(client.db);
|
|
9
10
|
if (!cached) {
|
|
@@ -66,6 +67,54 @@ export function getRawExecutor(client) {
|
|
|
66
67
|
break;
|
|
67
68
|
}
|
|
68
69
|
}
|
|
70
|
+
},
|
|
71
|
+
/**
|
|
72
|
+
* Execute many statements in as few round-trips as the driver allows.
|
|
73
|
+
* Statements run in order; on a remote driver each chunk is one request.
|
|
74
|
+
*/
|
|
75
|
+
async batch(queries) {
|
|
76
|
+
if (queries.length === 0)
|
|
77
|
+
return;
|
|
78
|
+
switch (type) {
|
|
79
|
+
case "better-sqlite3": {
|
|
80
|
+
const sqlite = driver;
|
|
81
|
+
const tx = sqlite.transaction(() => {
|
|
82
|
+
for (const q of queries)
|
|
83
|
+
sqlite.prepare(q.sql).run(...q.params || []);
|
|
84
|
+
});
|
|
85
|
+
tx();
|
|
86
|
+
break;
|
|
87
|
+
}
|
|
88
|
+
case "libsql": {
|
|
89
|
+
const client2 = driver;
|
|
90
|
+
for (let i = 0; i < queries.length; i += MAX_BATCH_STATEMENTS) {
|
|
91
|
+
const chunk = queries.slice(i, i + MAX_BATCH_STATEMENTS);
|
|
92
|
+
await client2.batch(chunk.map((q) => ({ sql: q.sql, args: q.params || [] })));
|
|
93
|
+
}
|
|
94
|
+
break;
|
|
95
|
+
}
|
|
96
|
+
case "d1": {
|
|
97
|
+
const db = driver;
|
|
98
|
+
for (let i = 0; i < queries.length; i += MAX_BATCH_STATEMENTS) {
|
|
99
|
+
const chunk = queries.slice(i, i + MAX_BATCH_STATEMENTS);
|
|
100
|
+
await db.batch(chunk.map((q) => db.prepare(q.sql).bind(...q.params || [])));
|
|
101
|
+
}
|
|
102
|
+
break;
|
|
103
|
+
}
|
|
104
|
+
case "neon": {
|
|
105
|
+
const sqlFn = driver;
|
|
106
|
+
for (let i = 0; i < queries.length; i += MAX_BATCH_STATEMENTS) {
|
|
107
|
+
const chunk = queries.slice(i, i + MAX_BATCH_STATEMENTS);
|
|
108
|
+
const pgQueries = chunk.map((q) => {
|
|
109
|
+
let idx = 0;
|
|
110
|
+
const pgQuery = q.sql.replace(RE_PARAM_PLACEHOLDER, () => `$${++idx}`);
|
|
111
|
+
return sqlFn.query(pgQuery, q.params || []);
|
|
112
|
+
});
|
|
113
|
+
await sqlFn.transaction(pgQueries);
|
|
114
|
+
}
|
|
115
|
+
break;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
69
118
|
}
|
|
70
119
|
};
|
|
71
120
|
}
|