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
@@ -0,0 +1,65 @@
1
+ import { toMarkdownPath } from "../../markdown-path.js";
2
+ import { toDeployedRoute } from "../../route-path.js";
3
+ export const SITEMAP_MD_ROUTE = "/sitemap.md";
4
+ function topSegment(route) {
5
+ return route.split("/").filter(Boolean)[0] || "";
6
+ }
7
+ function escapeTitle(title) {
8
+ return title.replaceAll("[", "\\[").replaceAll("]", "\\]");
9
+ }
10
+ function formatEntry(entry, resolveHref) {
11
+ const title = escapeTitle(entry.title?.trim() || entry.route);
12
+ const href = resolveHref(entry.route);
13
+ if (entry.updatedAt) {
14
+ const parsed = new Date(entry.updatedAt);
15
+ if (!Number.isNaN(parsed.getTime()))
16
+ return `- [${title}](${href} "${parsed.toISOString()}")`;
17
+ }
18
+ return `- [${title}](${href})`;
19
+ }
20
+ export function buildSitemapMd(entries, options = {}) {
21
+ const resolveHref = options.resolveHref || toMarkdownPath;
22
+ const groups = /* @__PURE__ */ new Map();
23
+ for (const entry of [...entries].sort((a, b) => a.route.localeCompare(b.route))) {
24
+ const key = topSegment(entry.route);
25
+ const group = groups.get(key);
26
+ if (group)
27
+ group.push(entry);
28
+ else
29
+ groups.set(key, [entry]);
30
+ }
31
+ const keys = [...groups.keys()].sort((a, b) => {
32
+ if (a === "")
33
+ return -1;
34
+ if (b === "")
35
+ return 1;
36
+ return a.localeCompare(b);
37
+ });
38
+ const parts = [`# ${options.siteName || "Site"} Sitemap`, "", "All pages in Markdown format."];
39
+ for (const key of keys) {
40
+ parts.push("", `## ${key || "Root"}`);
41
+ for (const entry of groups.get(key))
42
+ parts.push(formatEntry(entry, resolveHref));
43
+ }
44
+ return `${parts.join("\n")}
45
+ `;
46
+ }
47
+ export function appendSitemapSection(markdown, sitemapHref = SITEMAP_MD_ROUTE) {
48
+ const section = `## Sitemap
49
+
50
+ See the full [sitemap](${sitemapHref}) for all pages.`;
51
+ const trimmed = markdown.trimEnd();
52
+ if (trimmed.endsWith(section))
53
+ return `${trimmed}
54
+ `;
55
+ return `${trimmed}
56
+
57
+ ${section}
58
+ `;
59
+ }
60
+ export function isSitemapMdRequest(path, baseURL, enabled) {
61
+ if (!enabled)
62
+ return false;
63
+ const withoutQuery = path.split("?")[0] || path;
64
+ return withoutQuery === SITEMAP_MD_ROUTE || withoutQuery === toDeployedRoute(SITEMAP_MD_ROUTE, baseURL);
65
+ }
@@ -149,6 +149,19 @@ export interface ModuleOptions {
149
149
  * Structured llms.txt configuration
150
150
  */
151
151
  llmsTxt?: LlmsTxtConfig;
152
+ /**
153
+ * Generate a Markdown sitemap at `/sitemap.md` from indexed pages and end
154
+ * every Markdown page with a `## Sitemap` section linking to it.
155
+ * @default true
156
+ */
157
+ sitemapMd?: boolean;
158
+ /**
159
+ * Advertise llms.txt as `rel="describedby"` from HTML and Markdown
160
+ * responses, as the llms.txt v2 spec requires. Emits an HTML `<link>` tag
161
+ * and a Link header entry pointing at the llms.txt route.
162
+ * @default true
163
+ */
164
+ describedby?: boolean;
152
165
  /**
153
166
  * Content Signal Directives
154
167
  */
@@ -231,7 +244,7 @@ export interface ModuleOptions {
231
244
  llmsTxtCacheSeconds?: number;
232
245
  /**
233
246
  * Database configuration for page storage
234
- * Supports SQLite, LibSQL/Turso, Cloudflare D1, and Neon Postgres
247
+ * Supports SQLite, LibSQL/Turso, Cloudflare D1, Neon, and PostgreSQL
235
248
  *
236
249
  * Storage stays off until a runtime feature or page tool needs it.
237
250
  * Set to `false` (or `{ type: 'none' }`) to force it off.
@@ -247,9 +260,10 @@ export interface ModuleOptions {
247
260
  * - 'bun': Bun SQLite via bun:sqlite (auto-detected on Bun) [experimental]
248
261
  * - 'libsql': Turso/LibSQL [experimental]
249
262
  * - 'neon': Vercel Postgres via Neon serverless (auto-detected on Vercel with POSTGRES_URL) [experimental]
263
+ * - 'postgres': PostgreSQL via Postgres.js [experimental]
250
264
  * @default 'sqlite' when a requested feature needs storage
251
265
  */
252
- type?: 'none' | 'sqlite' | 'bun' | 'd1' | 'libsql' | 'neon';
266
+ type?: 'none' | 'sqlite' | 'bun' | 'd1' | 'libsql' | 'neon' | 'postgres';
253
267
  /**
254
268
  * SQLite filename (relative to rootDir or absolute)
255
269
  * @default '.data/ai-ready/pages.db'
@@ -257,12 +271,12 @@ export interface ModuleOptions {
257
271
  filename?: string;
258
272
  /**
259
273
  * D1 binding name for Cloudflare Workers/Pages
260
- * @default 'AI_READY_DB'
274
+ * @default 'DB'
261
275
  */
262
276
  bindingName?: string;
263
277
  /**
264
- * Database URL for LibSQL/Turso or Neon/Vercel Postgres
265
- * For Vercel: auto-reads from POSTGRES_URL env var
278
+ * Database URL for LibSQL/Turso, Neon, or PostgreSQL
279
+ * PostgreSQL drivers also read POSTGRES_URL or DATABASE_URL.
266
280
  */
267
281
  url?: string;
268
282
  /**
@@ -271,7 +285,7 @@ export interface ModuleOptions {
271
285
  authToken?: string;
272
286
  };
273
287
  /**
274
- * Enable scheduled cron task (runs every minute)
288
+ * Enable scheduled cron task (runs every 5 minutes)
275
289
  * When true, automatically enables runtimeSync for background indexing
276
290
  */
277
291
  cron?: boolean;
@@ -321,7 +335,7 @@ export interface ModuleOptions {
321
335
  ttl?: number;
322
336
  /**
323
337
  * Pages to index per batch
324
- * @default 20
338
+ * @default 50
325
339
  */
326
340
  batchSize?: number;
327
341
  /**
@@ -8,9 +8,12 @@ import { installNuxtSiteConfig, useSiteConfig, withSiteUrl } from 'nuxt-site-con
8
8
  import { resolveNuxtContentVersion, renderNitroTypeAugmentations, setupNitroRuntimeCompatibility } from 'nuxtseo-shared/kit';
9
9
  import { readPackageJSON, resolvePackageJSON } from 'pkg-types';
10
10
  import { fileURLToPath, pathToFileURL } from 'node:url';
11
+ import { isReservedPath, normalizePagePath, toMarkdownPath } from '../../dist/runtime/markdown-path.js';
12
+ import { SITEMAP_MD_ROUTE } from '../../dist/runtime/server/utils/sitemap-md.js';
11
13
  import { MCP_SERVER_CARD_MEDIA_TYPE, AI_CATALOG_MEDIA_TYPE } from '../../dist/runtime/server/utils/discovery-response.js';
12
14
  import { isAbsolute, join as join$1 } from 'pathe';
13
15
  import { resolveI18nConfig, toRuntimeI18nConfig as toRuntimeI18nConfig$1 } from 'nuxtseo-shared/i18n';
16
+ import { toDeployedRoute } from '../../dist/runtime/route-path.js';
14
17
 
15
18
  const SELECT_PAGE_DATA = "SELECT route, title, description, markdown, headings, keywords, updated_at, is_error, locale FROM ai_ready_pages";
16
19
  function createBuildPageDataVirtual(options) {
@@ -175,7 +178,7 @@ function resolveModuleEntryUrl(url) {
175
178
 
176
179
  const MARKDOWN_LINK_AVAILABILITY_FILE = "markdown-link-availability.json";
177
180
 
178
- const declarativeWebMcpTypes = `
181
+ const webmcpTypes = `
179
182
  import type { WebMcpToolsContext } from 'nuxt-ai-ready/webmcp'
180
183
 
181
184
  declare module '#app' {
@@ -184,37 +187,6 @@ declare module '#app' {
184
187
  'ai-ready:webmcp:tools': (context: WebMcpToolsContext) => void | Promise<void>
185
188
  }
186
189
  }
187
-
188
- declare module '@vue/runtime-dom' {
189
- interface HTMLAttributes {
190
- /** Names the tool this form exposes to agents. */
191
- toolname?: string
192
- /** Describes what submitting this form does. */
193
- tooldescription?: string
194
- /** Describes this field as a tool parameter. */
195
- toolparamdescription?: string
196
- /** Submit the form as soon as an agent fills it in. */
197
- toolautosubmit?: boolean | ''
198
- }
199
- }
200
-
201
- declare global {
202
- interface SubmitEvent {
203
- /** Whether an agent triggered this submit through a tool call. */
204
- readonly agentInvoked?: boolean
205
- /** Return a result to the agent. Call preventDefault() first. */
206
- respondWith?: (result: Promise<unknown>) => void
207
- }
208
-
209
- interface WebMcpToolEvent extends Event {
210
- readonly toolName: string
211
- }
212
-
213
- interface WindowEventMap {
214
- toolactivated: WebMcpToolEvent
215
- toolcancel: WebMcpToolEvent
216
- }
217
- }
218
190
  `;
219
191
  function registerTypeTemplates(ctx) {
220
192
  const nitroTypes = renderNitroTypeAugmentations(ctx.nitroCompatibility, {
@@ -231,7 +203,7 @@ import type { MarkdownContext, MarkdownSourceContext, PageIndexedContext } from
231
203
  import type { MdreamOptions } from 'mdream'
232
204
 
233
205
  ${nitroTypes}
234
- ${ctx.config.webmcp ? declarativeWebMcpTypes : ""}
206
+ ${ctx.config.webmcp ? webmcpTypes : ""}
235
207
  export {}
236
208
  `
237
209
  });
@@ -598,7 +570,7 @@ function resolveDatabaseConfig(input) {
598
570
  database: { _tag: "Enabled", type, bindingName: config.bindingName || "DB" }
599
571
  };
600
572
  }
601
- if (type === "neon") {
573
+ if (type === "neon" || type === "postgres") {
602
574
  return {
603
575
  _tag: "Resolved",
604
576
  logs,
@@ -892,6 +864,57 @@ function ensureStaticHeader(contents, route, name, value) {
892
864
  return `${contents.slice(0, blockStart)}${prefix} ${name}: ${value}${eol}${contents.slice(blockStart)}`;
893
865
  }
894
866
 
867
+ const RE_MD_EXT = /\.md$/;
868
+ function isStaticMarkdownSourceRoute(route) {
869
+ if (route.includes("*") || route.includes(":"))
870
+ return false;
871
+ const path = route.split("?")[0] || route;
872
+ if (isReservedPath(path))
873
+ return false;
874
+ const lastSegment = path.split("/").pop() || "";
875
+ return !lastSegment.includes(".");
876
+ }
877
+ function staticDescribedbyEntry(baseURL) {
878
+ return `<${encodeURI(toDeployedRoute("/llms.txt", baseURL))}>; rel="describedby"`;
879
+ }
880
+ function buildStaticMarkdownLinkHeader(route, baseURL, describedby) {
881
+ const htmlRoute = encodeURI(toDeployedRoute(normalizePagePath(route), baseURL));
882
+ const parts = [
883
+ `<${htmlRoute}>; rel="alternate"; type="text/html"`,
884
+ `<${htmlRoute}>; rel="canonical"`
885
+ ];
886
+ if (describedby)
887
+ parts.push(staticDescribedbyEntry(baseURL));
888
+ return parts.join(", ");
889
+ }
890
+ function pageRouteFromMarkdownTwin(fileName) {
891
+ if (!fileName?.endsWith(".md") || fileName === SITEMAP_MD_ROUTE)
892
+ return null;
893
+ const pageRoute = normalizePagePath(fileName.replace(RE_MD_EXT, ""));
894
+ if (pageRoute === "/index")
895
+ return "/";
896
+ return isStaticMarkdownSourceRoute(pageRoute) ? pageRoute : null;
897
+ }
898
+ function prerenderedMarkdownHeaderRules(prerenderedRoutes, baseURL, describedby) {
899
+ const rules = /* @__PURE__ */ new Map();
900
+ for (const entry of prerenderedRoutes) {
901
+ const pageRoute = pageRouteFromMarkdownTwin(entry.fileName);
902
+ if (pageRoute === null)
903
+ continue;
904
+ const mdRoute = toMarkdownPath(pageRoute);
905
+ if (!rules.has(mdRoute)) {
906
+ rules.set(mdRoute, {
907
+ route: mdRoute,
908
+ headers: {
909
+ "Content-Type": "text/markdown; charset=utf-8",
910
+ "Link": buildStaticMarkdownLinkHeader(pageRoute, baseURL, describedby)
911
+ }
912
+ });
913
+ }
914
+ }
915
+ return [...rules.values()];
916
+ }
917
+
895
918
  const DEFAULT_MAX_OUTPUT_CHARS = 1500;
896
919
  const DEFAULT_LIST_LIMIT = 20;
897
920
  const DEFAULT_SEARCH_LIMIT = 10;
@@ -1216,7 +1239,12 @@ ${details}`);
1216
1239
  userAgent: "*",
1217
1240
  // Preserve nuxt-robots' default wildcard rule so the injected group remains valid.
1218
1241
  disallow: [""],
1219
- contentSignal: [`ai-train=${config.contentSignal.aiTrain ? "yes" : "no"}`, `search=${config.contentSignal.search ? "yes" : "no"}`, `ai-input=${config.contentSignal.aiInput ? "yes" : "no"}`]
1242
+ // Object form renders one comma-separated line; an array renders one line per entry, which validators reject.
1243
+ contentSignal: {
1244
+ "ai-train": config.contentSignal.aiTrain ? "yes" : "no",
1245
+ "search": config.contentSignal.search ? "yes" : "no",
1246
+ "ai-input": config.contentSignal.aiInput ? "yes" : "no"
1247
+ }
1220
1248
  };
1221
1249
  if (config.contentSignal.contentUsage !== false)
1222
1250
  group.contentUsage = [`train-ai=${config.contentSignal.aiTrain ? "y" : "n"}`];
@@ -1579,12 +1607,13 @@ export const logger = createModuleLogger('nuxt-ai-ready', ${!!config.debug})
1579
1607
  bun: "#ai-ready/server/db/drizzle/providers/bun",
1580
1608
  d1: "#ai-ready/server/db/drizzle/providers/d1",
1581
1609
  libsql: "#ai-ready/server/db/drizzle/providers/libsql",
1582
- neon: "#ai-ready/server/db/drizzle/providers/neon"
1610
+ neon: "#ai-ready/server/db/drizzle/providers/neon",
1611
+ postgres: "#ai-ready/server/db/drizzle/providers/postgres"
1583
1612
  };
1584
1613
  nitroConfig.virtual["#ai-ready-virtual/db-provider.mjs"] = database._tag === "Enabled" ? `export { createClient } from '${providerMap[database.type] || providerMap.sqlite}'` : `export function createClient() {
1585
1614
  throw new Error('[nuxt-ai-ready] The database is disabled. Set \`aiReady.database\` to store pages at runtime.')
1586
1615
  }`;
1587
- const schemaPath = database._tag === "Enabled" && database.type === "neon" ? "#ai-ready/server/db/schema/postgres" : "#ai-ready/server/db/schema/sqlite";
1616
+ const schemaPath = database._tag === "Enabled" && (database.type === "neon" || database.type === "postgres") ? "#ai-ready/server/db/schema/postgres" : "#ai-ready/server/db/schema/sqlite";
1588
1617
  nitroConfig.virtual["#ai-ready-virtual/db-schema.mjs"] = `export * from '${schemaPath}'`;
1589
1618
  nitroConfig.virtual["#ai-ready-virtual/devtools-meta.mjs"] = `export const devtoolsMeta = ${JSON.stringify({
1590
1619
  contentSignal: config.contentSignal || false,
@@ -1599,6 +1628,8 @@ export const logger = createModuleLogger('nuxt-ai-ready', ${!!config.debug})
1599
1628
  debugCron: config.debugCron || false,
1600
1629
  contentNegotiation: config.contentNegotiation === void 0 ? "auto" : config.contentNegotiation ? "enabled" : "disabled",
1601
1630
  mdreamOptions: config.mdreamOptions || {},
1631
+ sitemapMd: config.sitemapMd !== false,
1632
+ describedby: config.describedby !== false,
1602
1633
  markdownCacheHeaders: defu(config.markdownCacheHeaders, {
1603
1634
  maxAge: 3600,
1604
1635
  swr: true
@@ -1642,6 +1673,9 @@ export const logger = createModuleLogger('nuxt-ai-ready', ${!!config.debug})
1642
1673
  });
1643
1674
  addServerHandler({ route: "/llms.txt", handler: resolve("./runtime/server/routes/llms.txt.get"), lazy: true });
1644
1675
  addServerHandler({ route: "/llms-full.txt", handler: resolve("./runtime/server/routes/llms-full.txt.get"), lazy: true });
1676
+ if (config.sitemapMd !== false) {
1677
+ addServerHandler({ route: SITEMAP_MD_ROUTE, handler: resolve("./runtime/server/routes/sitemap.md.get"), lazy: true });
1678
+ }
1645
1679
  if (agentSkillsResult._tag === "Enabled") {
1646
1680
  addServerHandler({ route: AGENT_SKILLS_INDEX_ROUTE, handler: resolve("./runtime/server/routes/agent-skills-index"), lazy: true });
1647
1681
  for (const route of Object.keys(agentSkillsResult.localArtifacts)) {
@@ -1688,7 +1722,9 @@ export const logger = createModuleLogger('nuxt-ai-ready', ${!!config.debug})
1688
1722
  addServerHandler({ route: "/__ai-ready/pages", handler: resolve("./runtime/server/routes/__ai-ready/pages.get"), lazy: true });
1689
1723
  }
1690
1724
  }
1691
- addServerHandler({ route: "/__ai-ready__/debug.json", handler: resolve("./runtime/server/routes/__ai-ready/devtools.get"), lazy: true });
1725
+ if (nuxt.options.dev || config.debug) {
1726
+ addServerHandler({ route: "/__ai-ready__/debug.json", handler: resolve("./runtime/server/routes/__ai-ready/devtools.get"), lazy: true });
1727
+ }
1692
1728
  if (config.debug) {
1693
1729
  addServerHandler({ route: "/__ai-ready-debug", handler: resolve("./runtime/server/routes/__ai-ready-debug.get"), lazy: true });
1694
1730
  }
@@ -1697,6 +1733,7 @@ export const logger = createModuleLogger('nuxt-ai-ready', ${!!config.debug})
1697
1733
  addServerHandler({ route: "/__ai-ready/poll", method: "post", handler: resolve("./runtime/server/routes/__ai-ready/poll.post"), lazy: true });
1698
1734
  addServerHandler({ route: "/__ai-ready/prune", method: "post", handler: resolve("./runtime/server/routes/__ai-ready/prune.post"), lazy: true });
1699
1735
  addServerHandler({ route: "/__ai-ready/restore", method: "post", handler: resolve("./runtime/server/routes/__ai-ready/restore.post"), lazy: true });
1736
+ addServerHandler({ route: "/__ai-ready/reindex", method: "post", handler: resolve("./runtime/server/routes/__ai-ready/reindex.post"), lazy: true });
1700
1737
  addServerPlugin(resolve("./runtime/server/plugins/sitemap-seeder"));
1701
1738
  }
1702
1739
  if (config.cron && !nuxt.options.dev) {
@@ -1734,19 +1771,50 @@ export const logger = createModuleLogger('nuxt-ai-ready', ${!!config.debug})
1734
1771
  for (const route of ["/llms.txt", "/llms-full.txt"]) {
1735
1772
  extendRouteRules(route, { headers: { "Content-Type": "text/plain; charset=utf-8" } });
1736
1773
  }
1774
+ if (config.sitemapMd !== false) {
1775
+ extendRouteRules(SITEMAP_MD_ROUTE, { headers: { "Content-Type": "text/markdown; charset=utf-8" } });
1776
+ }
1777
+ const staticBaseURL = nuxt.options.app.baseURL || "/";
1778
+ for (const route of nuxt.options.nitro.prerender?.routes || []) {
1779
+ if (!isStaticMarkdownSourceRoute(route))
1780
+ continue;
1781
+ extendRouteRules(toMarkdownPath(route), {
1782
+ headers: {
1783
+ "Content-Type": "text/markdown; charset=utf-8",
1784
+ "Link": buildStaticMarkdownLinkHeader(route, staticBaseURL, config.describedby !== false)
1785
+ }
1786
+ });
1787
+ }
1737
1788
  nuxt.hooks.hook("nitro:build:before", (nitro) => {
1789
+ nitro.hooks.hook("prerender:done", () => {
1790
+ for (const { route, headers } of prerenderedMarkdownHeaderRules(
1791
+ nitro._prerenderedRoutes || [],
1792
+ staticBaseURL,
1793
+ config.describedby !== false
1794
+ )) {
1795
+ nitro.options.routeRules[route] = defu({ headers }, nitro.options.routeRules[route]);
1796
+ }
1797
+ });
1738
1798
  nitro.hooks.hook("compiled", async () => {
1739
1799
  const headersPath = join(nitro.options.output.publicDir, "_headers");
1740
1800
  logger.debug(`Checking for _headers file: ${headersPath}`);
1741
1801
  const exists = await access(headersPath).then(() => true).catch(() => false);
1742
1802
  if (exists) {
1743
1803
  const headers = await readFile(headersPath, "utf8");
1744
- const mergedHeaders = ensureStaticHeader(
1804
+ let mergedHeaders = ensureStaticHeader(
1745
1805
  headers,
1746
1806
  "/*.md",
1747
1807
  "Content-Type",
1748
1808
  "text/markdown; charset=utf-8"
1749
1809
  );
1810
+ if (config.describedby !== false) {
1811
+ mergedHeaders = ensureStaticHeader(
1812
+ mergedHeaders,
1813
+ "/*.md",
1814
+ "Link",
1815
+ staticDescribedbyEntry(nitro.options.baseURL || "/")
1816
+ );
1817
+ }
1750
1818
  if (mergedHeaders !== headers) {
1751
1819
  await writeFile(headersPath, mergedHeaders);
1752
1820
  logger.debug("Merged .md charset header into _headers");
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "nuxt-ai-ready",
3
3
  "type": "module",
4
- "version": "2.0.1",
4
+ "version": "2.2.0",
5
5
  "description": "Best practice AI & LLM discoverability for Nuxt sites.",
6
6
  "author": {
7
7
  "name": "Harlan Wilton",
@@ -48,7 +48,8 @@
48
48
  "@libsql/client": "^0.14.0",
49
49
  "@neondatabase/serverless": "^1.0.0",
50
50
  "@nuxtjs/sitemap": ">=8.3.1",
51
- "better-sqlite3": "^11.0.0 || ^12.0.0 || ^13.0.0"
51
+ "better-sqlite3": "^11.0.0 || ^12.0.0 || ^13.0.0",
52
+ "postgres": "^3.4.9"
52
53
  },
53
54
  "peerDependenciesMeta": {
54
55
  "@libsql/client": {
@@ -59,20 +60,23 @@
59
60
  },
60
61
  "better-sqlite3": {
61
62
  "optional": true
63
+ },
64
+ "postgres": {
65
+ "optional": true
62
66
  }
63
67
  },
64
68
  "dependencies": {
65
- "@mdream/js": "^1.7.0",
69
+ "@mdream/js": "^1.7.1",
66
70
  "@nuxt/kit": "^4.5.2",
67
71
  "citty": "^0.2.2",
68
72
  "consola": "^3.4.2",
69
73
  "defu": "^6.1.7",
70
74
  "drizzle-orm": "1.0.0-rc.4",
71
- "mdream": "^1.7.0",
75
+ "mdream": "^1.7.1",
72
76
  "nuxt-site-config": "^4.2.3",
73
77
  "nuxtseo-shared": "^5.3.14",
74
78
  "pathe": "^2.0.3",
75
- "pkg-types": "^2.3.1",
79
+ "pkg-types": "^2.3.2",
76
80
  "site-config-stack": "^4.2.3",
77
81
  "sitemapd": "^0.2.2",
78
82
  "ufo": "^1.6.4",
@@ -80,43 +84,44 @@
80
84
  "yaml": "^2.9.0"
81
85
  },
82
86
  "devDependencies": {
83
- "@antfu/eslint-config": "^9.3.0",
87
+ "@antfu/eslint-config": "^9.5.1",
84
88
  "@arethetypeswrong/cli": "^0.18.5",
85
- "@harlan-zw/comark-content": "0.1.4",
86
- "@libsql/client": "^0.17.4",
87
- "@nuxt/content": "^3.15.2",
89
+ "@harlan-zw/comark-content": "^0.1.5",
90
+ "@libsql/client": "^0.18.0",
91
+ "@nuxt/content": "^3.16.0",
88
92
  "@nuxt/module-builder": "^1.0.3",
89
- "@nuxt/test-utils": "^4.1.0",
93
+ "@nuxt/test-utils": "^4.2.0",
90
94
  "@nuxtjs/eslint-config-typescript": "^12.1.0",
91
95
  "@nuxtjs/mcp-toolkit": "^0.19.0",
92
- "@nuxtjs/robots": "^6.1.5",
93
- "@nuxtjs/sitemap": "^8.4.0",
96
+ "@nuxtjs/robots": "^6.2.0",
97
+ "@nuxtjs/sitemap": "^8.5.0",
94
98
  "@types/better-sqlite3": "^9.6.0",
95
- "@vitest/coverage-v8": "^4.1.11",
96
- "@vue/test-utils": "^2.4.11",
99
+ "@vitest/coverage-v8": "^5.0.0",
100
+ "@vue/test-utils": "^2.5.0",
97
101
  "@vueuse/nuxt": "^14.4.0",
98
102
  "better-sqlite3": "^13.0.3",
99
- "bumpp": "^12.2.1",
100
- "eslint": "^10.8.1",
101
- "eslint-plugin-harlanzw": "^0.20.0",
103
+ "bumpp": "^12.3.0",
104
+ "eslint": "^10.10.0",
105
+ "eslint-plugin-harlanzw": "^0.21.1",
102
106
  "execa": "^10.0.1",
103
107
  "h3": "^1.15.11",
104
- "happy-dom": "^20.11.2",
108
+ "happy-dom": "^20.14.0",
105
109
  "nitropack": "^2.13.4",
106
110
  "nuxt": "^4.5.2",
107
111
  "nuxt-site-config": "^4.2.3",
108
112
  "nuxtseo-layer-devtools": "^5.3.14",
109
- "playwright": "^1.62.1",
110
- "playwright-core": "^1.62.1",
113
+ "playwright": "^1.63.0",
114
+ "playwright-core": "^1.63.0",
115
+ "postgres": "^3.4.9",
111
116
  "tinyglobby": "^0.2.17",
112
117
  "typescript": "6.0.3",
113
118
  "unbuild": "^3.6.1",
114
- "vitest": "^4.1.11",
115
- "vue": "^3.5.41",
116
- "vue-router": "^5.2.0",
117
- "vue-tsc": "^3.3.10",
118
- "wrangler": "^4.124.0",
119
- "zod": "^4.4.3"
119
+ "vitest": "^5.0.0",
120
+ "vue": "^3.5.42",
121
+ "vue-router": "^5.3.1",
122
+ "vue-tsc": "^3.3.11",
123
+ "wrangler": "^4.129.0",
124
+ "zod": "^4.5.4"
120
125
  },
121
126
  "scripts": {
122
127
  "lint": "eslint .",