nuxt-ai-ready 2.0.1 → 2.2.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 (57) hide show
  1. package/dist/chunks/agent-skills.mjs +4 -1
  2. package/dist/chunks/prerender.mjs +10 -3
  3. package/dist/cli.mjs +67 -11
  4. package/dist/module.d.mts +3 -1
  5. package/dist/module.json +1 -1
  6. package/dist/module.mjs +4 -1
  7. package/dist/runtime/app/plugins/md-alternate.server.js +6 -2
  8. package/dist/runtime/llms-txt-utils.js +2 -2
  9. package/dist/runtime/markdown-path.d.ts +2 -0
  10. package/dist/runtime/markdown-path.js +4 -0
  11. package/dist/runtime/server/db/context.d.ts +2 -0
  12. package/dist/runtime/server/db/context.js +2 -0
  13. package/dist/runtime/server/db/drizzle/client.d.ts +6 -1
  14. package/dist/runtime/server/db/drizzle/client.js +66 -13
  15. package/dist/runtime/server/db/drizzle/index.d.ts +2 -2
  16. package/dist/runtime/server/db/drizzle/index.js +1 -2
  17. package/dist/runtime/server/db/drizzle/providers/dbPath.js +1 -1
  18. package/dist/runtime/server/db/drizzle/providers/postgres.d.ts +8 -0
  19. package/dist/runtime/server/db/drizzle/providers/postgres.js +17 -0
  20. package/dist/runtime/server/db/drizzle/queries.d.ts +1 -5
  21. package/dist/runtime/server/db/drizzle/queries.js +79 -38
  22. package/dist/runtime/server/db/drizzle/raw.d.ts +2 -2
  23. package/dist/runtime/server/db/drizzle/raw.js +32 -13
  24. package/dist/runtime/server/db/queries.d.ts +30 -27
  25. package/dist/runtime/server/db/queries.js +142 -91
  26. package/dist/runtime/server/db/schema/postgres.d.ts +20 -20
  27. package/dist/runtime/server/db/schema/postgres.js +6 -6
  28. package/dist/runtime/server/db/shared.d.ts +16 -0
  29. package/dist/runtime/server/db/shared.js +12 -0
  30. package/dist/runtime/server/middleware/markdown.js +10 -5
  31. package/dist/runtime/server/middleware/markdown.prerender.js +4 -1
  32. package/dist/runtime/server/plugins/db-lifecycle.js +5 -4
  33. package/dist/runtime/server/plugins/sitemap-seeder.js +9 -4
  34. package/dist/runtime/server/routes/__ai-ready/cron.get.js +10 -2
  35. package/dist/runtime/server/routes/__ai-ready/poll.post.js +30 -14
  36. package/dist/runtime/server/routes/__ai-ready/reindex.post.d.ts +2 -0
  37. package/dist/runtime/server/routes/__ai-ready/reindex.post.js +23 -0
  38. package/dist/runtime/server/routes/api-catalog.js +11 -2
  39. package/dist/runtime/server/routes/llms.txt.get.js +17 -12
  40. package/dist/runtime/server/routes/sitemap.md.get.d.ts +2 -0
  41. package/dist/runtime/server/routes/sitemap.md.get.js +51 -0
  42. package/dist/runtime/server/tasks/ai-ready-cron.js +3 -0
  43. package/dist/runtime/server/utils/i18n.d.ts +4 -0
  44. package/dist/runtime/server/utils/i18n.js +13 -0
  45. package/dist/runtime/server/utils/indexPage.d.ts +0 -1
  46. package/dist/runtime/server/utils/indexPage.js +10 -4
  47. package/dist/runtime/server/utils/link-header.d.ts +2 -0
  48. package/dist/runtime/server/utils/link-header.js +6 -0
  49. package/dist/runtime/server/utils/markdown-request.js +3 -1
  50. package/dist/runtime/server/utils/negotiation-decision.d.ts +1 -1
  51. package/dist/runtime/server/utils/runCron.d.ts +24 -0
  52. package/dist/runtime/server/utils/runCron.js +21 -4
  53. package/dist/runtime/server/utils/sitemap-md.d.ts +13 -0
  54. package/dist/runtime/server/utils/sitemap-md.js +65 -0
  55. package/dist/runtime/types.d.ts +21 -7
  56. package/dist/shared/{nuxt-ai-ready.D8qbnNrv.mjs → nuxt-ai-ready.D5URd0TD.mjs} +107 -39
  57. package/package.json +31 -26
@@ -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.D8qbnNrv.mjs';
5
+ import { A as AGENT_SKILLS_SCHEMA } from '../shared/nuxt-ai-ready.D5URd0TD.mjs';
6
6
  import 'node:module';
7
7
  import '@nuxt/kit';
8
8
  import 'defu';
@@ -10,9 +10,12 @@ import 'nuxt-site-config/kit';
10
10
  import 'nuxtseo-shared/kit';
11
11
  import 'pkg-types';
12
12
  import 'node:url';
13
+ import '../../dist/runtime/markdown-path.js';
14
+ import '../../dist/runtime/server/utils/sitemap-md.js';
13
15
  import '../../dist/runtime/server/utils/discovery-response.js';
14
16
  import 'pathe';
15
17
  import 'nuxtseo-shared/i18n';
18
+ import '../../dist/runtime/route-path.js';
16
19
 
17
20
  const namePattern = /^(?!.*--)[a-z0-9](?:[a-z0-9-]{0,62}[a-z0-9])?$/;
18
21
  const digestPattern = /^sha256:[a-f0-9]{64}$/;
@@ -5,11 +5,12 @@ import { colorize } from 'consola/utils';
5
5
  import { resolveLocaleFromRoute } from 'nuxtseo-shared/i18n-runtime';
6
6
  import { collectSitemap } from 'sitemapd/parse';
7
7
  import { withLeadingSlash, joinURL, withBase } from 'ufo';
8
- import { l as logger, s as supportsNativeNodeSqlite, M as MARKDOWN_LINK_AVAILABILITY_FILE } from '../shared/nuxt-ai-ready.D8qbnNrv.mjs';
8
+ import { l as logger, s as supportsNativeNodeSqlite, M as MARKDOWN_LINK_AVAILABILITY_FILE } from '../shared/nuxt-ai-ready.D5URd0TD.mjs';
9
9
  import { normalizePagePath, toMarkdownPath } from '../../dist/runtime/markdown-path.js';
10
- import { toLogicalRoute, toDeployedRoute } from '../../dist/runtime/route-path.js';
10
+ import { toDeployedRoute, toLogicalRoute } from '../../dist/runtime/route-path.js';
11
11
  import { initSchema, computeContentHash, insertPage, queryAllPages, exportDbDump } from '../../dist/runtime/server/db/shared.js';
12
12
  import { buildLlmsFullTxtHeader, formatPageForLlmsFullTxt } from '../../dist/runtime/server/utils/llms-full.js';
13
+ import { SITEMAP_MD_ROUTE, appendSitemapSection } from '../../dist/runtime/server/utils/sitemap-md.js';
13
14
  import 'node:crypto';
14
15
  import 'node:module';
15
16
  import 'defu';
@@ -320,6 +321,8 @@ function setupPrerenderHandler(options, dbPath, siteInfo, llmsTxtConfig, extras
320
321
  logger.debug(`Detected error page: ${pageRoute2}`);
321
322
  return;
322
323
  }
324
+ if (route.fileName === SITEMAP_MD_ROUTE)
325
+ return;
323
326
  if (!route.fileName?.endsWith(".md"))
324
327
  return;
325
328
  let pageRoute = route.route.replace(RE_MD_EXT, "");
@@ -331,7 +334,7 @@ function setupPrerenderHandler(options, dbPath, siteInfo, llmsTxtConfig, extras
331
334
  await initPromise;
332
335
  const parsed = JSON.parse(route.contents || "{}");
333
336
  await processMarkdownRoute(state, nuxt, pageRoute, parsed);
334
- route.contents = parsed.markdown;
337
+ route.contents = options.sitemapMd === false ? parsed.markdown : appendSitemapSection(parsed.markdown, toDeployedRoute(SITEMAP_MD_ROUTE, nitro.options.baseURL));
335
338
  route.contentType = "text/markdown; charset=utf-8";
336
339
  state.totalProcessingTime += Date.now() - pageStartTime;
337
340
  });
@@ -406,6 +409,10 @@ function setupPrerenderHandler(options, dbPath, siteInfo, llmsTxtConfig, extras
406
409
  const llmsStats = await prerenderRoute(nitro, "/llms.txt");
407
410
  const llmsFullStats = await stat(state.llmsFullTxtPath);
408
411
  nitro._prerenderedRoutes.push({ route: "/llms-full.txt", fileName: "/llms-full.txt" });
412
+ if (options.sitemapMd !== false) {
413
+ const sitemapMdStats = await prerenderRoute(nitro, SITEMAP_MD_ROUTE);
414
+ logger.debug(`Wrote sitemap.md (${(sitemapMdStats.size / 1024).toFixed(1)}kb)`);
415
+ }
409
416
  const kb = (b) => (b / 1024).toFixed(1);
410
417
  const totalKb = kb(llmsStats.size + llmsFullStats.size);
411
418
  const dim = (s) => colorize("dim", s);
package/dist/cli.mjs CHANGED
@@ -26,11 +26,22 @@ async function requireSecret(cwd, hint = "") {
26
26
  }
27
27
  return secret;
28
28
  }
29
+ function describeHttpError(status, body) {
30
+ const error = body;
31
+ const message = typeof error?.message === "string" && error.message ? error.message : typeof error?.statusMessage === "string" && error.statusMessage ? error.statusMessage : typeof error?.error === "string" && error.error ? error.error : null;
32
+ return message ? `${status}: ${message}` : `request failed with status ${status}`;
33
+ }
29
34
  async function fetchJson(url, init, errorLabel = "Failed") {
30
- return fetch(url, init).then((r) => r.json()).catch((err) => {
31
- consola.error(`${errorLabel}: ${err.message}`);
32
- return null;
35
+ const r = await fetch(url, init).catch((err) => {
36
+ throw new Error(`${errorLabel}: ${err instanceof Error ? err.message : String(err)}`);
33
37
  });
38
+ if (!r.ok) {
39
+ const body = await r.json().catch(() => {
40
+ return null;
41
+ });
42
+ throw new Error(`${errorLabel}: ${describeHttpError(r.status, body)}`);
43
+ }
44
+ return r.json();
34
45
  }
35
46
  const main = defineCommand({
36
47
  meta: {
@@ -65,8 +76,6 @@ const main = defineCommand({
65
76
  const url = `${args.url}/__ai-ready/status`;
66
77
  consola.info(`Fetching status from ${args.url}...`);
67
78
  const res = await fetchJson(url, { headers: authHeaders(secret) }, "Failed to connect");
68
- if (!res)
69
- return;
70
79
  consola.box("AI Ready Status");
71
80
  consola.info(`Total pages: ${colors.cyan(res.total?.toString() || "0")}`);
72
81
  consola.info(`Indexed: ${colors.green(res.indexed?.toString() || "0")}`);
@@ -151,8 +160,6 @@ const main = defineCommand({
151
160
  const url = `${args.url}/__ai-ready/poll?${params}`;
152
161
  consola.info(`Triggering poll at ${args.url}...`);
153
162
  const res = await fetchJson(url, { method: "POST", headers: authHeaders(secret) });
154
- if (!res)
155
- return;
156
163
  consola.success(`Indexed: ${colors.green(res.indexed?.toString() || "0")} pages`);
157
164
  consola.info(`Remaining: ${colors.yellow(res.remaining?.toString() || "0")}`);
158
165
  if (res.errors?.length) {
@@ -199,8 +206,6 @@ const main = defineCommand({
199
206
  const url = `${args.url}/__ai-ready/restore?${params}`;
200
207
  consola.info(`Restoring database at ${args.url}...`);
201
208
  const res = await fetchJson(url, { method: "POST", headers: authHeaders(secret) });
202
- if (!res)
203
- return;
204
209
  consola.success(`Restored: ${colors.green(res.restored?.toString() || "0")} pages`);
205
210
  if (res.cleared) {
206
211
  consola.info(`Cleared: ${colors.yellow(res.cleared?.toString() || "0")} existing pages`);
@@ -252,8 +257,6 @@ const main = defineCommand({
252
257
  method: "POST",
253
258
  headers: secret ? authHeaders(secret) : void 0
254
259
  });
255
- if (!res)
256
- return;
257
260
  if (args.dry) {
258
261
  consola.info(`Would prune: ${colors.yellow(res.count?.toString() || "0")} routes`);
259
262
  if (res.routes?.length) {
@@ -268,6 +271,59 @@ const main = defineCommand({
268
271
  consola.success(`Pruned: ${colors.green(res.pruned?.toString() || "0")} routes`);
269
272
  }
270
273
  }
274
+ }),
275
+ reindex: () => defineCommand({
276
+ meta: {
277
+ name: "reindex",
278
+ description: "Reindex a single route"
279
+ },
280
+ args: {
281
+ route: {
282
+ type: "positional",
283
+ description: "Route to reindex (e.g. /about)",
284
+ required: true
285
+ },
286
+ url: {
287
+ type: "string",
288
+ alias: "u",
289
+ description: "Site URL (default: http://localhost:3000)",
290
+ default: "http://localhost:3000"
291
+ },
292
+ force: {
293
+ type: "boolean",
294
+ description: "Index even when the page is still fresh",
295
+ default: true,
296
+ negativeDescription: "Skip indexing when the page is still fresh"
297
+ },
298
+ cwd: {
299
+ type: "string",
300
+ description: "Working directory",
301
+ default: "."
302
+ }
303
+ },
304
+ async run({ args }) {
305
+ const cwd = resolve(args.cwd || ".");
306
+ const secret = await requireSecret(cwd);
307
+ if (!secret)
308
+ return;
309
+ const params = new URLSearchParams();
310
+ params.set("route", args.route);
311
+ if (!args.force)
312
+ params.set("force", "false");
313
+ const url = `${args.url}/__ai-ready/reindex?${params}`;
314
+ consola.info(`Reindexing ${args.route} at ${args.url}...`);
315
+ const res = await fetchJson(url, { method: "POST", headers: authHeaders(secret) });
316
+ if (res.indexed) {
317
+ consola.success(`Indexed: ${colors.green(res.route)}`);
318
+ if (res.contentChanged !== void 0) {
319
+ consola.info(`Content changed: ${res.contentChanged ? colors.green("yes") : colors.yellow("no")}`);
320
+ }
321
+ } else if (res.skipped) {
322
+ consola.info(`Skipped: ${colors.yellow(res.route)} (still fresh)`);
323
+ } else {
324
+ throw new Error(`Failed to reindex ${res.route ?? args.route}${res.error ? `: ${res.error}` : ""}`);
325
+ }
326
+ }
271
327
  })
272
328
  }
273
329
  });
package/dist/module.d.mts CHANGED
@@ -31,7 +31,7 @@ interface ResolvedApiCatalogConfig {
31
31
  };
32
32
  }
33
33
 
34
- type DatabaseType = 'sqlite' | 'bun' | 'd1' | 'libsql' | 'neon';
34
+ type DatabaseType = 'sqlite' | 'bun' | 'd1' | 'libsql' | 'neon' | 'postgres';
35
35
  /**
36
36
  * Database state after parsing. Every consumer reads `_tag` first, so a
37
37
  * disabled database can never carry a driver, a path, or credentials.
@@ -85,6 +85,8 @@ interface ModulePublicRuntimeConfig {
85
85
  debugCron: boolean;
86
86
  contentNegotiation: ContentNegotiationPolicy;
87
87
  version: string;
88
+ sitemapMd: boolean;
89
+ describedby: boolean;
88
90
  mdreamOptions: ModuleOptions['mdreamOptions'];
89
91
  markdownCacheHeaders: Required<NonNullable<ModuleOptions['markdownCacheHeaders']>>;
90
92
  database: ResolvedDatabase;
package/dist/module.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "nuxt": ">=4.0.0"
5
5
  },
6
6
  "configKey": "aiReady",
7
- "version": "2.0.1",
7
+ "version": "2.2.0",
8
8
  "builder": {
9
9
  "@nuxt/module-builder": "1.0.3",
10
10
  "unbuild": "3.6.1"
package/dist/module.mjs CHANGED
@@ -7,8 +7,11 @@ 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.D8qbnNrv.mjs';
10
+ export { m as default } from './shared/nuxt-ai-ready.D5URd0TD.mjs';
11
+ import '../dist/runtime/markdown-path.js';
12
+ import '../dist/runtime/server/utils/sitemap-md.js';
11
13
  import '../dist/runtime/server/utils/discovery-response.js';
12
14
  import 'node:url';
13
15
  import 'pathe';
14
16
  import 'nuxtseo-shared/i18n';
17
+ import '../dist/runtime/route-path.js';
@@ -1,4 +1,5 @@
1
- import { defineNuxtPlugin, useHead, useRequestURL } from "nuxt/app";
1
+ import { defineNuxtPlugin, useHead, useRequestURL, useRuntimeConfig } from "nuxt/app";
2
+ import { joinURL } from "ufo";
2
3
  import { toMarkdownPath } from "../../markdown-path.js";
3
4
  export default defineNuxtPlugin({
4
5
  setup() {
@@ -7,9 +8,12 @@ export default defineNuxtPlugin({
7
8
  const lastSegment = path.split("/").pop() || "";
8
9
  if (lastSegment.includes("."))
9
10
  return;
11
+ const runtimeConfig = useRuntimeConfig();
12
+ const describedby = runtimeConfig["nuxt-ai-ready"]?.describedby !== false;
10
13
  useHead({
11
14
  link: [
12
- { rel: "alternate", type: "text/markdown", href: toMarkdownPath(path) }
15
+ { rel: "alternate", type: "text/markdown", href: toMarkdownPath(path) },
16
+ ...describedby ? [{ rel: "describedby", href: joinURL(runtimeConfig.app.baseURL, "llms.txt") }] : []
13
17
  ]
14
18
  });
15
19
  }
@@ -4,7 +4,7 @@ import { useRuntimeConfig } from "#nuxtseo/nitro";
4
4
  import { getSiteConfig } from "#site-config/server/composables/getSiteConfig";
5
5
  import { withSiteTrailingSlash, withSiteUrl } from "#site-config/server/composables/utils";
6
6
  import { formatLlmsTxtPageLink, normalizeLlmsTxtConfig } from "./llms-txt-format.js";
7
- import { toMarkdownPath } from "./markdown-path.js";
7
+ import { isReservedPath, toMarkdownPath } from "./markdown-path.js";
8
8
  import { normalizePersistedRoute, toDeployedRoute, toLogicalRoute } from "./route-path.js";
9
9
  import { queryPages } from "./server/db/queries.js";
10
10
  import { logger } from "./server/logger.js";
@@ -12,7 +12,7 @@ import { fetchSitemapUrls } from "./server/utils/sitemap.js";
12
12
  export { normalizeLlmsTxtConfig };
13
13
  function hasRuntimeMarkdownHandler(pathname) {
14
14
  const lastSegment = pathname.split("/").pop() || "";
15
- return !pathname.startsWith("/api") && !pathname.startsWith("/_") && !pathname.startsWith("/@") && !lastSegment.includes(".");
15
+ return !isReservedPath(pathname) && !lastSegment.includes(".");
16
16
  }
17
17
  async function findMarkdownLinkAvailability() {
18
18
  if (!import.meta.prerender) {
@@ -1,2 +1,4 @@
1
+ /** True for the `/api` namespace and Nitro's `/_*` and `/@*` internal paths. */
2
+ export declare function isReservedPath(path: string): boolean;
1
3
  export declare function normalizePagePath(path: string): string;
2
4
  export declare function toMarkdownPath(path: string): string;
@@ -1,4 +1,8 @@
1
1
  const RE_TRAILING_SLASHES = /\/+$/;
2
+ const RE_RESERVED_PATH = /^\/(?:api(?:\/|$)|_|@)/;
3
+ export function isReservedPath(path) {
4
+ return RE_RESERVED_PATH.test(path);
5
+ }
2
6
  export function normalizePagePath(path) {
3
7
  return path.replace(RE_TRAILING_SLASHES, "") || "/";
4
8
  }
@@ -1 +1,3 @@
1
1
  export declare const DB_CONTEXT_KEY = "_aiReadyDrizzle";
2
+ export declare const DB_PROMISE_CONTEXT_KEY = "_aiReadyDrizzlePromise";
3
+ export declare const DB_WORK_CONTEXT_KEY = "_aiReadyDrizzleWork";
@@ -1 +1,3 @@
1
1
  export const DB_CONTEXT_KEY = "_aiReadyDrizzle";
2
+ export const DB_PROMISE_CONTEXT_KEY = "_aiReadyDrizzlePromise";
3
+ export const DB_WORK_CONTEXT_KEY = "_aiReadyDrizzleWork";
@@ -4,14 +4,19 @@ import type { DrizzleD1Database } from 'drizzle-orm/d1';
4
4
  import type { LibSQLDatabase } from 'drizzle-orm/libsql';
5
5
  import type { NeonHttpDatabase } from 'drizzle-orm/neon-http';
6
6
  import type { NodeSQLiteDatabase } from 'drizzle-orm/node-sqlite';
7
+ import type { PostgresJsDatabase } from 'drizzle-orm/postgres-js';
7
8
  import type { H3Event } from '#nuxtseo/h3';
8
9
  export type DatabaseDialect = 'sqlite' | 'postgres';
9
10
  type SQLiteDB = BetterSQLite3Database | SQLiteBunDatabase | LibSQLDatabase | DrizzleD1Database | NodeSQLiteDatabase;
10
- type PostgresDB = NeonHttpDatabase;
11
+ type PostgresDB = NeonHttpDatabase | PostgresJsDatabase;
11
12
  export interface DrizzleDatabase {
12
13
  dialect: DatabaseDialect;
13
14
  db: SQLiteDB | PostgresDB;
14
15
  }
16
+ /** Keep request-scoped clients alive until deferred database work finishes. */
17
+ export declare function trackDrizzleWork<T>(event: H3Event, work: Promise<T>): Promise<T>;
18
+ /** Transfer client cleanup to deferred work after the response ends. */
19
+ export declare function finishDrizzleResponse(event: H3Event): Promise<void>;
15
20
  /**
16
21
  * Get Drizzle database instance
17
22
  */
@@ -1,29 +1,82 @@
1
- import { DB_CONTEXT_KEY } from "../context.js";
1
+ import { DB_CONTEXT_KEY, DB_PROMISE_CONTEXT_KEY, DB_WORK_CONTEXT_KEY } from "../context.js";
2
2
  import { closeDriver } from "./raw.js";
3
3
  let fallbackClient;
4
+ let fallbackClientPromise;
5
+ function createDrizzleClient(event) {
6
+ return import("#ai-ready-virtual/db-provider.mjs").then(({ createClient }) => createClient(event));
7
+ }
8
+ function getDrizzleWorkState(event) {
9
+ const context = event.context;
10
+ return context[DB_WORK_CONTEXT_KEY] ??= {
11
+ _tag: "ResponseOpen",
12
+ pending: /* @__PURE__ */ new Set()
13
+ };
14
+ }
15
+ export function trackDrizzleWork(event, work) {
16
+ const state = getDrizzleWorkState(event);
17
+ const tracked = work.finally(async () => {
18
+ state.pending.delete(tracked);
19
+ if (state._tag === "ResponseEnded" && state.pending.size === 0)
20
+ await closeDrizzle(event);
21
+ });
22
+ state.pending.add(tracked);
23
+ return tracked;
24
+ }
25
+ export async function finishDrizzleResponse(event) {
26
+ const state = getDrizzleWorkState(event);
27
+ state._tag = "ResponseEnded";
28
+ if (state.pending.size === 0)
29
+ await closeDrizzle(event);
30
+ }
4
31
  export async function useDrizzle(event) {
5
32
  if (event?.context?.[DB_CONTEXT_KEY]) {
6
33
  return event.context[DB_CONTEXT_KEY];
7
34
  }
35
+ if (event?.context?.[DB_PROMISE_CONTEXT_KEY]) {
36
+ return event.context[DB_PROMISE_CONTEXT_KEY];
37
+ }
8
38
  if (!event && fallbackClient) {
9
39
  return fallbackClient;
10
40
  }
11
- const { createClient } = await import("#ai-ready-virtual/db-provider.mjs");
12
- const client = await createClient(event);
41
+ if (!event && fallbackClientPromise)
42
+ return fallbackClientPromise;
13
43
  if (event?.context) {
14
- event.context[DB_CONTEXT_KEY] = client;
15
- } else {
16
- fallbackClient = client;
44
+ const context = event.context;
45
+ const promise2 = createDrizzleClient(event).then((client) => {
46
+ context[DB_CONTEXT_KEY] = client;
47
+ return client;
48
+ }).finally(() => {
49
+ if (context[DB_PROMISE_CONTEXT_KEY] === promise2)
50
+ delete context[DB_PROMISE_CONTEXT_KEY];
51
+ });
52
+ context[DB_PROMISE_CONTEXT_KEY] = promise2;
53
+ return promise2;
17
54
  }
18
- return client;
55
+ const promise = createDrizzleClient().then((client) => {
56
+ fallbackClient = client;
57
+ return client;
58
+ }).finally(() => {
59
+ if (fallbackClientPromise === promise)
60
+ fallbackClientPromise = void 0;
61
+ });
62
+ fallbackClientPromise = promise;
63
+ return promise;
19
64
  }
20
65
  export async function closeDrizzle(event) {
21
- if (event?.context?.[DB_CONTEXT_KEY]) {
22
- const client = event.context[DB_CONTEXT_KEY];
23
- closeDriver(client.db);
24
- delete event.context[DB_CONTEXT_KEY];
25
- } else if (!event && fallbackClient) {
26
- closeDriver(fallbackClient.db);
66
+ if (event?.context) {
67
+ const context = event.context;
68
+ const client = context[DB_CONTEXT_KEY] ?? await context[DB_PROMISE_CONTEXT_KEY];
69
+ if (!client)
70
+ return;
71
+ await closeDriver(client.db);
72
+ delete context[DB_CONTEXT_KEY];
73
+ delete context[DB_PROMISE_CONTEXT_KEY];
74
+ } else if (!event) {
75
+ const client = fallbackClient ?? await fallbackClientPromise;
76
+ if (!client)
77
+ return;
78
+ await closeDriver(client.db);
27
79
  fallbackClient = void 0;
80
+ fallbackClientPromise = void 0;
28
81
  }
29
82
  }
@@ -2,9 +2,9 @@
2
2
  * Drizzle ORM database layer for nuxt-ai-ready
3
3
  */
4
4
  export * from '#ai-ready-virtual/db-schema.mjs';
5
- export { closeDrizzle, useDrizzle } from './client.js';
5
+ export { closeDrizzle, finishDrizzleResponse, trackDrizzleWork, useDrizzle } from './client.js';
6
6
  export type { DatabaseDialect, DrizzleDatabase } from './client.js';
7
- export { completeCronRun, countPages, deleteInfoValue, deletePage, getAllPages, getContentHashes, getInfoValue, getNextSitemapToCrawl, getPageByRoute, getPageLastmods, getPendingPages, getRecentCronRuns, getSitemapStatus, initSchema, markPageIndexed, markRoutesPending, markSitemapCrawled, markSitemapCrawlPartial, markSitemapError, markSitemapSeeded, resetSitemapErrors, searchPages, seedRoutes, setInfoValue, startCronRun, syncSitemaps, upsertPage, } from './queries.js';
7
+ export { completeCronRun, countPages, deleteInfoValue, getAllPages, getContentHashes, getInfoValue, getNextSitemapToCrawl, getPageByRoute, getPageLastmods, getPendingPages, getRecentCronRuns, getSitemapStatus, initSchema, 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,10 +1,9 @@
1
1
  export * from "#ai-ready-virtual/db-schema.mjs";
2
- export { closeDrizzle, useDrizzle } from "./client.js";
2
+ export { closeDrizzle, finishDrizzleResponse, trackDrizzleWork, useDrizzle } from "./client.js";
3
3
  export {
4
4
  completeCronRun,
5
5
  countPages,
6
6
  deleteInfoValue,
7
- deletePage,
8
7
  getAllPages,
9
8
  getContentHashes,
10
9
  getInfoValue,
@@ -36,7 +36,7 @@ export async function resolveWritableDbPath(dbPath) {
36
36
  const fallbackErr = await ensureWritableDir(fallbackDir);
37
37
  if (fallbackErr) {
38
38
  throw new Error(
39
- `[ai-ready] Database directory '${dir}' is not writable (${err.code}) and the temp dir fallback ('${fallbackDir}') also failed: ${fallbackErr.message}. Set database.filename to a writable path, or use a serverless driver: database.type 'd1' (Cloudflare), 'neon' (Vercel/Postgres), or 'libsql' (Turso).`
39
+ `[ai-ready] Database directory '${dir}' is not writable (${err.code}) and the temp dir fallback ('${fallbackDir}') also failed: ${fallbackErr.message}. Set database.filename to a writable path, or use a remote driver: database.type 'd1' (Cloudflare), 'neon' (Vercel), 'postgres' (PostgreSQL), or 'libsql' (Turso).`
40
40
  );
41
41
  }
42
42
  const fallback = join(fallbackDir, "pages.db");
@@ -0,0 +1,8 @@
1
+ import type { H3Event } from '#nuxtseo/h3';
2
+ import postgres from 'postgres';
3
+ export declare function createClient(event?: H3Event): Promise<{
4
+ dialect: "postgres";
5
+ db: import("drizzle-orm/postgres-js").PostgresJsDatabase<import("drizzle-orm").EmptyRelations> & {
6
+ $client: postgres.Sql<{}>;
7
+ };
8
+ }>;
@@ -0,0 +1,17 @@
1
+ import { drizzle } from "drizzle-orm/postgres-js";
2
+ import postgres from "postgres";
3
+ import { useRuntimeConfig } from "#nuxtseo/nitro";
4
+ import { logger } from "../../../logger.js";
5
+ import { registerDriver } from "../raw.js";
6
+ export async function createClient(event) {
7
+ const config = useRuntimeConfig(event)["nuxt-ai-ready"];
8
+ const connectionString = config.database.url || process.env.POSTGRES_URL || process.env.DATABASE_URL;
9
+ if (!connectionString) {
10
+ throw new Error("[ai-ready] Missing database URL. Set DATABASE_URL or configure database.url");
11
+ }
12
+ logger.debug("[drizzle] Connecting to PostgreSQL");
13
+ const sqlClient = postgres(connectionString, { prepare: false });
14
+ const db = drizzle({ client: sqlClient });
15
+ registerDriver(db, "postgres", sqlClient);
16
+ return { dialect: "postgres", db };
17
+ }
@@ -65,10 +65,6 @@ export declare function countPages(event: H3Event | undefined, options?: {
65
65
  indexed?: boolean;
66
66
  errors?: boolean;
67
67
  }): Promise<number>;
68
- /**
69
- * Delete a page by route
70
- */
71
- export declare function deletePage(event: H3Event | undefined, route: string): Promise<void>;
72
68
  /**
73
69
  * Get pending pages (not yet indexed)
74
70
  */
@@ -100,7 +96,7 @@ export declare function setInfoValue(event: H3Event | undefined, key: string, va
100
96
  */
101
97
  export declare function deleteInfoValue(event: H3Event | undefined, key: string): Promise<void>;
102
98
  /**
103
- * Initialize database schema. Rebuilds on SCHEMA_VERSION change or when the
99
+ * Initialize database schema. Rebuilds on a schema version change or when the
104
100
  * SQLite FTS5 tokenizer differs from the one baked into the existing virtual
105
101
  * table (Postgres has no FTS5; tokenizer comparison is SQLite-only).
106
102
  */