nuxt-ai-ready 2.3.4 → 2.4.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.
@@ -10,6 +10,7 @@ import { normalizePagePath, toMarkdownPath } from '../../dist/runtime/markdown-p
10
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 { resolvePageUpdatedAt } from '../../dist/runtime/server/utils/page-updated-at.js';
13
14
  import { SITEMAP_MD_ROUTE, appendSitemapSection } from '../../dist/runtime/server/utils/sitemap-md.js';
14
15
  import 'node:crypto';
15
16
  import 'node:module';
@@ -150,12 +151,7 @@ function resolveRouteLocale(route, i18n) {
150
151
  async function processMarkdownRoute(state, nuxt, route, parsed, lastmod, options) {
151
152
  route = normalizePagePath(route);
152
153
  const { title, description, headings, keywords, updatedAt: metaUpdatedAt } = parsed;
153
- let updatedAt = (lastmod instanceof Date ? lastmod.toISOString() : lastmod) || (/* @__PURE__ */ new Date()).toISOString();
154
- if (metaUpdatedAt) {
155
- const parsedDate = new Date(metaUpdatedAt);
156
- if (!Number.isNaN(parsedDate.getTime()))
157
- updatedAt = parsedDate.toISOString();
158
- }
154
+ const updatedAt = resolvePageUpdatedAt(lastmod, metaUpdatedAt);
159
155
  const hookContext = { ...parsed, route };
160
156
  await nuxt.hooks.callHook("ai-ready:page:markdown", hookContext);
161
157
  const { markdown } = hookContext;
@@ -353,7 +349,7 @@ function setupPrerenderHandler(options, dbPath, siteInfo, llmsTxtConfig, extras
353
349
  markdown: "",
354
350
  headings: "",
355
351
  keywords: [],
356
- updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
352
+ updatedAt: "",
357
353
  isError: true,
358
354
  locale: resolveRouteLocale(route, state.i18n)
359
355
  });
package/dist/module.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "nuxt": ">=4.0.0"
5
5
  },
6
6
  "configKey": "aiReady",
7
- "version": "2.3.4",
7
+ "version": "2.4.0",
8
8
  "builder": {
9
9
  "@nuxt/module-builder": "1.0.3",
10
10
  "unbuild": "3.6.1"
@@ -675,7 +675,6 @@ export async function seedRoutes(event, routes, options = {}) {
675
675
  if (routes.length === 0)
676
676
  return 0;
677
677
  const refreshWindowMs = Math.max(0, Math.trunc(Number.isFinite(options.refreshWindowMs) ? options.refreshWindowMs : SEED_REFRESH_WINDOW_MS));
678
- const now = (/* @__PURE__ */ new Date()).toISOString();
679
678
  const nowMs = Date.now();
680
679
  const byRoute = /* @__PURE__ */ new Map();
681
680
  for (const entry of routes) {
@@ -688,14 +687,14 @@ export async function seedRoutes(event, routes, options = {}) {
688
687
  locale: deriveLocale(event, route, explicitLocale, entryUrl)
689
688
  });
690
689
  }
691
- const rowsPerInsert = maxRowsPerInsert(5);
690
+ const rowsPerInsert = maxRowsPerInsert(4);
692
691
  const db = await useRawDb(event);
693
692
  const entries = [...byRoute.values()];
694
693
  const stmts = [];
695
694
  for (let i = 0; i < entries.length; i += rowsPerInsert) {
696
695
  const batch = entries.slice(i, i + rowsPerInsert);
697
- const valuesSql = batch.map(() => `(?, ?, '', '', '', '[]', '[]', ?, 0, 0, 0, 'runtime', ?, ?)`).join(", ");
698
- const params = batch.flatMap((r) => [r.route, r.routeKey, now, nowMs, r.locale]);
696
+ const valuesSql = batch.map(() => `(?, ?, '', '', '', '[]', '[]', '', 0, 0, 0, 'runtime', ?, ?)`).join(", ");
697
+ const params = batch.flatMap((r) => [r.route, r.routeKey, nowMs, r.locale]);
699
698
  stmts.push({
700
699
  sql: `
701
700
  INSERT INTO ai_ready_pages (route, route_key, title, description, markdown, headings, keywords, updated_at, indexed_at, is_error, indexed, source, last_seen_at, locale)
@@ -124,6 +124,8 @@ export declare function isPageFresh(event: H3Event | undefined, route: string, t
124
124
  export interface PageIndexState {
125
125
  indexedAt: number;
126
126
  contentHash: string | null;
127
+ updatedAt: string;
128
+ source: 'prerender' | 'runtime';
127
129
  }
128
130
  /**
129
131
  * Fetch a page's index bookkeeping in one lightweight query.
@@ -324,10 +324,15 @@ export async function getPageIndexState(event, route) {
324
324
  if (!db)
325
325
  return void 0;
326
326
  const row = await db.first(
327
- "SELECT indexed_at, content_hash FROM ai_ready_pages WHERE route = ?",
327
+ "SELECT indexed_at, content_hash, updated_at, source FROM ai_ready_pages WHERE route = ?",
328
328
  [route]
329
329
  );
330
- return row ? { indexedAt: toNumber(row.indexed_at), contentHash: row.content_hash } : void 0;
330
+ return row ? {
331
+ indexedAt: toNumber(row.indexed_at),
332
+ contentHash: row.content_hash,
333
+ updatedAt: row.updated_at || "",
334
+ source: row.source === "runtime" ? "runtime" : "prerender"
335
+ } : void 0;
331
336
  }
332
337
  export async function getPageHash(event, route) {
333
338
  const db = await getDb(event);
@@ -347,7 +352,6 @@ export async function seedRoutes(event, routes, options = {}) {
347
352
  if (!db || routes.length === 0)
348
353
  return 0;
349
354
  const refreshWindowMs = Math.max(0, Math.trunc(Number.isFinite(options.refreshWindowMs) ? options.refreshWindowMs : SEED_REFRESH_WINDOW_MS));
350
- const now = (/* @__PURE__ */ new Date()).toISOString();
351
355
  const nowMs = Date.now();
352
356
  const byRoute = /* @__PURE__ */ new Map();
353
357
  for (const entry of routes) {
@@ -360,11 +364,11 @@ export async function seedRoutes(event, routes, options = {}) {
360
364
  locale: deriveLocale(event, route, explicitLocale, entryUrl)
361
365
  });
362
366
  }
363
- const rowsPerInsert = maxRowsPerInsert(5);
367
+ const rowsPerInsert = maxRowsPerInsert(4);
364
368
  const stmts = [];
365
369
  for (const batch of chunk([...byRoute.values()], rowsPerInsert)) {
366
- const valuesSql = batch.map(() => `(?, ?, '', '', '', '[]', '[]', ?, 0, 0, 0, 'runtime', ?, ?)`).join(", ");
367
- const params = batch.flatMap((r) => [r.route, r.routeKey, now, nowMs, r.locale]);
370
+ const valuesSql = batch.map(() => `(?, ?, '', '', '', '[]', '[]', '', 0, 0, 0, 'runtime', ?, ?)`).join(", ");
371
+ const params = batch.flatMap((r) => [r.route, r.routeKey, nowMs, r.locale]);
368
372
  stmts.push({
369
373
  sql: `
370
374
  INSERT INTO ai_ready_pages (route, route_key, title, description, markdown, headings, keywords, updated_at, indexed_at, is_error, indexed, source, last_seen_at, locale)
@@ -1,5 +1,8 @@
1
1
  /**
2
- * Compute content hash for change detection (first 16 chars of SHA-256)
2
+ * Compute content hash for change detection (first 16 chars of SHA-256).
3
+ * Hash the page body only. Frontmatter holds per-conversion fields
4
+ * (`canonical_url`, `last_updated`) that differ between the build and runtime
5
+ * paths, so hashing it marks unchanged pages as changed.
3
6
  */
4
7
  export declare function computeContentHash(markdown: string): Promise<string>;
5
8
  export interface DatabaseAdapter {
@@ -1,8 +1,21 @@
1
1
  import { subtle } from "uncrypto";
2
+ import { isMap, isScalar, parseDocument } from "yaml";
2
3
  import { buildSchemaSql, DROP_TABLES_SQL, resolveFtsTokenizer, SCHEMA_VERSION } from "./schema-sql.js";
4
+ const LEADING_BLOCK_RE = /^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/;
5
+ const PAGE_FRONTMATTER_KEYS = /* @__PURE__ */ new Set(["title", "description", "meta", "canonical_url", "last_updated", "locale", "alternates"]);
6
+ function stripPageFrontmatter(markdown) {
7
+ const match = markdown.match(LEADING_BLOCK_RE);
8
+ if (!match)
9
+ return markdown;
10
+ const document = parseDocument(match[1]);
11
+ if (document.errors.length || !isMap(document.contents))
12
+ return markdown;
13
+ const isPageFrontmatter = document.contents.items.some((item) => isScalar(item.key) && PAGE_FRONTMATTER_KEYS.has(String(item.key.value)));
14
+ return isPageFrontmatter ? markdown.slice(match[0].length) : markdown;
15
+ }
3
16
  export async function computeContentHash(markdown) {
4
17
  const encoder = new TextEncoder();
5
- const data = encoder.encode(markdown);
18
+ const data = encoder.encode(stripPageFrontmatter(markdown).trim());
6
19
  const hashBuffer = await subtle.digest("SHA-256", data);
7
20
  const hashArray = [...new Uint8Array(hashBuffer)];
8
21
  return hashArray.map((b) => b.toString(16).padStart(2, "0")).join("").slice(0, 16);
@@ -1,8 +1,9 @@
1
- import { createError, defineEventHandler, setHeader, setResponseStatus } from "#nuxtseo/h3";
1
+ import { createError, defineEventHandler, getRequestURL, setHeader, setResponseStatus } from "#nuxtseo/h3";
2
2
  import { useNitroApp, useRuntimeConfig } from "#nuxtseo/nitro";
3
3
  import { resolveLocaleAlternateUrl } from "../../i18n-url.js";
4
4
  import { logger } from "../logger.js";
5
5
  import { computeLocaleAlternates, resolveLocaleFromRoute } from "../utils/i18n.js";
6
+ import { resolveMarkdownRedirect } from "../utils/markdown-redirect.js";
6
7
  import { INTERNAL_HEADER } from "../utils/negotiation-decision.js";
7
8
  import { applyNegotiation, buildNegotiationContext, decideNegotiation, ensureSiteConfig, setMarkdownHeaders } from "../utils/negotiation-response.js";
8
9
  import { appendSitemapSection, isSitemapMdRequest, SITEMAP_MD_ROUTE } from "../utils/sitemap-md.js";
@@ -30,7 +31,6 @@ function notFoundMarkdown(ctx, canonicalUrl, build) {
30
31
  title: "Page not found",
31
32
  description: `No content is available at ${path}.`,
32
33
  canonical_url: canonicalUrl,
33
- last_updated: (/* @__PURE__ */ new Date()).toISOString(),
34
34
  locale,
35
35
  alternates
36
36
  });
@@ -67,7 +67,7 @@ export default defineEventHandler(async (event) => {
67
67
  title: title ?? path,
68
68
  description,
69
69
  canonical_url: canonicalUrl,
70
- last_updated: updatedAt || (/* @__PURE__ */ new Date()).toISOString()
70
+ last_updated: updatedAt
71
71
  }, markdown);
72
72
  setMarkdownHeaders(event, ctx);
73
73
  return finalizeMarkdown(responseMarkdown);
@@ -82,37 +82,49 @@ export default defineEventHandler(async (event) => {
82
82
  title: contentPage.title,
83
83
  description: contentPage.description,
84
84
  canonical_url: canonicalUrl,
85
- last_updated: contentPage.updatedAt || (/* @__PURE__ */ new Date()).toISOString()
85
+ last_updated: contentPage.updatedAt
86
86
  });
87
87
  setMarkdownHeaders(event, ctx);
88
88
  return finalizeMarkdown(`${frontmatter}
89
89
  ${contentPage.markdown}`);
90
90
  }
91
- logger.debug(`[markdown] Fetching HTML for ${path}`);
92
- const response = await fetchRawWithEvent(event, resolvePath(path), {
93
- headers: { [INTERNAL_HEADER]: "1" },
94
- redirect: "manual"
95
- }).catch((e) => {
96
- logger.error(`Failed to fetch HTML for ${path}`, e);
97
- return null;
98
- });
91
+ const origin = getRequestURL(event).origin;
92
+ const siteUrl = resolveUrl("/");
93
+ const redirectOptions = {
94
+ pageUrl: `${origin}${resolvePath(path)}`,
95
+ origins: URL.canParse(siteUrl) ? [new URL(siteUrl).origin] : []
96
+ };
97
+ let htmlPath = resolvePath(path);
98
+ let response = null;
99
+ for (let attempt = 0; attempt < 2; attempt++) {
100
+ logger.debug(`[markdown] Fetching HTML for ${htmlPath}`);
101
+ response = await fetchRawWithEvent(event, htmlPath, {
102
+ headers: { [INTERNAL_HEADER]: "1" },
103
+ redirect: "manual"
104
+ }).catch((e) => {
105
+ logger.error(`Failed to fetch HTML for ${htmlPath}`, e);
106
+ return null;
107
+ });
108
+ const location = response && response.status >= 300 && response.status < 400 ? response.headers.get("location") : null;
109
+ if (!response || !location)
110
+ break;
111
+ const redirect = resolveMarkdownRedirect(location, redirectOptions);
112
+ if (redirect._tag === "follow" && attempt === 0) {
113
+ htmlPath = redirect.path;
114
+ continue;
115
+ }
116
+ setHeader(event, "location", redirect._tag === "redirect" ? redirect.location : location);
117
+ return createError({
118
+ statusCode: response.status,
119
+ statusMessage: response.statusText
120
+ });
121
+ }
99
122
  if (!response) {
100
123
  throw createError({
101
124
  statusCode: 502,
102
125
  statusMessage: "Bad Gateway"
103
126
  });
104
127
  }
105
- if (response.status >= 300 && response.status < 400) {
106
- const location = response.headers.get("location");
107
- if (location) {
108
- const redirectTarget = location.endsWith("/") ? `${location.slice(0, -1)}.md` : `${location}.md`;
109
- setHeader(event, "location", redirectTarget);
110
- return createError({
111
- statusCode: response.status,
112
- statusMessage: response.statusText
113
- });
114
- }
115
- }
116
128
  if (!response.ok && response.status !== 404)
117
129
  return response;
118
130
  if (response.status === 404) {
@@ -139,11 +151,12 @@ ${contentPage.markdown}`);
139
151
  logger.debug(`[markdown] DB lookup failed for ${path}:`, err);
140
152
  return void 0;
141
153
  });
142
- const lastUpdated = dbPage?.updatedAt || extractLastUpdated(html) || (/* @__PURE__ */ new Date()).toISOString();
154
+ const lastUpdated = dbPage?.updatedAt || extractLastUpdated(html);
143
155
  const additionalFrontmatter = {
144
- canonical_url: canonicalUrl,
145
- last_updated: lastUpdated
156
+ canonical_url: canonicalUrl
146
157
  };
158
+ if (lastUpdated)
159
+ additionalFrontmatter.last_updated = lastUpdated;
147
160
  if (config.i18n) {
148
161
  additionalFrontmatter.locale = resolveLocaleFromRoute(path, config.i18n, routeContext).locale;
149
162
  }
@@ -45,12 +45,11 @@ export default defineEventHandler(async (event) => {
45
45
  });
46
46
  if (contentPage) {
47
47
  logger.debug(`[markdown.prerender] Using content source for ${path} (${contentPage.markdown.length} bytes)`);
48
- const lastUpdated2 = contentPage.updatedAt || (/* @__PURE__ */ new Date()).toISOString();
49
48
  const frontmatter = buildFrontmatter({
50
49
  title: contentPage.title,
51
50
  description: contentPage.description,
52
51
  canonical_url: canonicalUrl,
53
- last_updated: lastUpdated2
52
+ last_updated: contentPage.updatedAt
54
53
  });
55
54
  const markdown = `${frontmatter}
56
55
  ${contentPage.markdown}`;
@@ -62,7 +61,8 @@ ${contentPage.markdown}`;
62
61
  description: contentPage.description || "",
63
62
  headings,
64
63
  keywords: keywords2,
65
- updatedAt: lastUpdated2
64
+ // Only a real content date feeds the page index and sitemap lastmod.
65
+ ...contentPage.updatedAt && { updatedAt: contentPage.updatedAt }
66
66
  });
67
67
  }
68
68
  let html = consumePrerenderedHtml(path);
@@ -97,7 +97,7 @@ ${contentPage.markdown}`;
97
97
  message: `Page rendered as error: ${path}`
98
98
  });
99
99
  }
100
- const lastUpdated = extractLastUpdated(html) || (/* @__PURE__ */ new Date()).toISOString();
100
+ const lastUpdated = extractLastUpdated(html);
101
101
  const result = await convertHtmlToMarkdown(
102
102
  html,
103
103
  canonicalUrl,
@@ -106,7 +106,7 @@ ${contentPage.markdown}`;
106
106
  extractUpdatedAt: true,
107
107
  additionalFrontmatter: {
108
108
  canonical_url: canonicalUrl,
109
- last_updated: lastUpdated
109
+ ...lastUpdated && { last_updated: lastUpdated }
110
110
  }
111
111
  }
112
112
  );
@@ -6,6 +6,7 @@ import { convertHtmlToMarkdown } from "../utils.js";
6
6
  import { createUniversalContext } from "./context.js";
7
7
  import { extractKeywords } from "./keywords.js";
8
8
  import { INTERNAL_HEADER } from "./negotiation-decision.js";
9
+ import { resolveIndexedUpdatedAt, resolvePageUpdatedAt } from "./page-updated-at.js";
9
10
  export async function indexPage(route, html, options = {}, event) {
10
11
  const config = useRuntimeConfig()["nuxt-ai-ready"];
11
12
  const ttl = options.ttl ?? config.runtimeSync.ttl;
@@ -19,11 +20,17 @@ export async function indexPage(route, html, options = {}, event) {
19
20
  const { siteUrl } = createUniversalContext(event);
20
21
  const fullUrl = siteUrl ? `${siteUrl}${route}` : route;
21
22
  const result = await convertHtmlToMarkdown(html, fullUrl, config.mdreamOptions, { extractUpdatedAt: true });
22
- const updatedAt = result.updatedAt || (/* @__PURE__ */ new Date()).toISOString();
23
23
  const headings = JSON.stringify(result.headings);
24
24
  const keywords = extractKeywords(result.textContent, result.metaKeywords);
25
25
  const contentHash = await computeContentHash(result.markdown);
26
26
  const contentChanged = state?.contentHash !== contentHash;
27
+ const updatedAt = isError ? "" : resolveIndexedUpdatedAt({
28
+ declared: resolvePageUpdatedAt(void 0, result.updatedAt),
29
+ previous: state,
30
+ contentHash,
31
+ now: /* @__PURE__ */ new Date(),
32
+ detectDrift: config.runtimeSync.enabled
33
+ });
27
34
  await upsertPage(event, {
28
35
  route,
29
36
  title: result.title,
@@ -92,7 +99,7 @@ export async function indexPageByRoute(route, event, options = {}) {
92
99
  markdown: "",
93
100
  headings: "[]",
94
101
  keywords: [],
95
- updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
102
+ updatedAt: "",
96
103
  isError: true
97
104
  });
98
105
  }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * What a `.md` request does with an upstream redirect of its HTML page.
3
+ *
4
+ * `redirect` sends the client to `location`. `follow` means the target is the
5
+ * same markdown document as the request, as with a trailing-slash redirect, so
6
+ * the handler converts the HTML at `path` instead of redirecting to itself.
7
+ */
8
+ export type MarkdownRedirect = {
9
+ _tag: 'redirect';
10
+ location: string;
11
+ } | {
12
+ _tag: 'follow';
13
+ path: string;
14
+ };
15
+ export interface MarkdownRedirectOptions {
16
+ /** Absolute URL of the HTML page that answered with the redirect. */
17
+ pageUrl: string;
18
+ /** Other origins that serve this site, such as the configured site URL. */
19
+ origins?: readonly string[];
20
+ }
21
+ /**
22
+ * Map an upstream `Location` onto the markdown sibling of its target.
23
+ *
24
+ * A target on another origin, or one without a markdown sibling, keeps its
25
+ * original location. Query and fragment carry over to the sibling.
26
+ */
27
+ export declare function resolveMarkdownRedirect(location: string, { pageUrl, origins }: MarkdownRedirectOptions): MarkdownRedirect;
@@ -0,0 +1,18 @@
1
+ import { markdownAlternatePath, toMarkdownPath } from "../../markdown-path.js";
2
+ const RE_ABSOLUTE_URL = /^(?:[a-z][a-z\d+.-]*:|\/\/)/i;
3
+ export function resolveMarkdownRedirect(location, { pageUrl, origins = [] }) {
4
+ const page = new URL(pageUrl);
5
+ const target = URL.canParse(location, page) ? new URL(location, page) : null;
6
+ if (!target || target.origin !== page.origin && !origins.includes(target.origin))
7
+ return { _tag: "redirect", location };
8
+ const markdownPath = markdownAlternatePath(target.pathname);
9
+ if (!markdownPath)
10
+ return { _tag: "redirect", location };
11
+ if (markdownPath === toMarkdownPath(page.pathname))
12
+ return { _tag: "follow", path: `${target.pathname}${target.search}` };
13
+ const markdownLocation = `${markdownPath}${target.search}${target.hash}`;
14
+ return {
15
+ _tag: "redirect",
16
+ location: RE_ABSOLUTE_URL.test(location) ? `${target.origin}${markdownLocation}` : markdownLocation
17
+ };
18
+ }
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Resolve when a page's content last changed, from sources that know it.
3
+ *
4
+ * Returns an empty string when no source gives a valid date. The index time is
5
+ * never a fallback: it moves on every deploy or re-index, so a sitemap lastmod
6
+ * built from it tells crawlers every page changed, and they learn to ignore it.
7
+ */
8
+ export declare function resolvePageUpdatedAt(sitemapLastmod: string | Date | undefined, metaUpdatedAt: string | undefined): string;
9
+ export interface StoredPageDate {
10
+ contentHash: string | null;
11
+ updatedAt: string;
12
+ source: 'prerender' | 'runtime';
13
+ }
14
+ export interface IndexedUpdatedAtInput {
15
+ /** Date the page declares itself, already resolved. Empty when none. */
16
+ declared: string;
17
+ /** The row before this index, if any. */
18
+ previous: StoredPageDate | undefined;
19
+ contentHash: string;
20
+ now: Date;
21
+ /** Only true when runtime indexing runs, so two indexes can be compared. */
22
+ detectDrift: boolean;
23
+ }
24
+ /**
25
+ * Date a runtime-indexed page. A declared date always wins. Otherwise a page
26
+ * gets the detection time only when its body hash differs from an earlier
27
+ * runtime index of the same page. A prerender hash comes from another pipeline,
28
+ * so a difference there proves nothing and never produces a date.
29
+ */
30
+ export declare function resolveIndexedUpdatedAt(input: IndexedUpdatedAtInput): string;
@@ -0,0 +1,21 @@
1
+ function toIsoDate(value) {
2
+ if (!value)
3
+ return "";
4
+ const date = value instanceof Date ? value : new Date(value);
5
+ return Number.isNaN(date.getTime()) ? "" : date.toISOString();
6
+ }
7
+ export function resolvePageUpdatedAt(sitemapLastmod, metaUpdatedAt) {
8
+ return toIsoDate(metaUpdatedAt) || toIsoDate(sitemapLastmod);
9
+ }
10
+ export function resolveIndexedUpdatedAt(input) {
11
+ if (input.declared)
12
+ return input.declared;
13
+ if (!input.detectDrift)
14
+ return "";
15
+ const { previous } = input;
16
+ if (!previous?.contentHash)
17
+ return "";
18
+ if (previous.source === "runtime" && previous.contentHash !== input.contentHash)
19
+ return input.now.toISOString();
20
+ return previous.updatedAt;
21
+ }
@@ -452,7 +452,7 @@ export interface MarkdownSource {
452
452
  title?: string;
453
453
  /** Page description. */
454
454
  description?: string;
455
- /** ISO timestamp for `last_updated`. Defaults to now. */
455
+ /** ISO timestamp for `last_updated`. Omitted when not set. */
456
456
  updatedAt?: string;
457
457
  }
458
458
  /**
@@ -536,7 +536,7 @@ export interface PageIndexedContext {
536
536
  keywords: string[];
537
537
  /** Full markdown content */
538
538
  markdown: string;
539
- /** ISO timestamp */
539
+ /** ISO timestamp of the last content change from page meta. Empty string when the page gives none. */
540
540
  updatedAt: string;
541
541
  /** Whether this is a new page or an update */
542
542
  isUpdate: boolean;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "nuxt-ai-ready",
3
3
  "type": "module",
4
- "version": "2.3.4",
4
+ "version": "2.4.0",
5
5
  "description": "Best practice AI & LLM discoverability for Nuxt sites.",
6
6
  "author": {
7
7
  "name": "Harlan Wilton",
@@ -45,7 +45,7 @@
45
45
  "webmcp.d.ts"
46
46
  ],
47
47
  "peerDependencies": {
48
- "@libsql/client": "^0.14.0",
48
+ "@libsql/client": "^0.18.0",
49
49
  "@neondatabase/serverless": "^1.0.0",
50
50
  "@nuxtjs/sitemap": ">=8.3.1",
51
51
  "better-sqlite3": "^11.0.0 || ^12.0.0 || ^13.0.0",
@@ -66,62 +66,62 @@
66
66
  }
67
67
  },
68
68
  "dependencies": {
69
- "@mdream/js": "^1.7.1",
69
+ "@mdream/js": "^1.7.2",
70
70
  "@nuxt/kit": "^4.5.2",
71
71
  "citty": "^0.2.2",
72
72
  "consola": "^3.4.2",
73
73
  "defu": "^6.1.7",
74
74
  "drizzle-orm": "1.0.0-rc.4",
75
- "mdream": "^1.7.1",
75
+ "mdream": "^1.7.2",
76
76
  "nuxt-site-config": "^4.2.3",
77
- "nuxtseo-shared": "^5.3.15",
77
+ "nuxtseo-shared": "^5.3.16",
78
78
  "pathe": "^2.0.3",
79
- "pkg-types": "^2.3.2",
79
+ "pkg-types": "^2.3.3",
80
80
  "site-config-stack": "^4.2.3",
81
81
  "sitemapd": "^0.2.2",
82
82
  "ufo": "^1.6.4",
83
83
  "uncrypto": "^0.1.3",
84
- "yaml": "^2.9.0"
84
+ "yaml": "^2.9.1"
85
85
  },
86
86
  "devDependencies": {
87
87
  "@antfu/eslint-config": "^9.5.1",
88
88
  "@arethetypeswrong/cli": "^0.18.5",
89
89
  "@harlan-zw/comark-content": "^0.1.5",
90
90
  "@libsql/client": "^0.18.0",
91
- "@nuxt/content": "^3.16.0",
91
+ "@nuxt/content": "^3.16.1",
92
92
  "@nuxt/module-builder": "^1.0.3",
93
- "@nuxt/test-utils": "^4.2.0",
93
+ "@nuxt/test-utils": "^4.3.2",
94
94
  "@nuxtjs/eslint-config-typescript": "^12.1.0",
95
- "@nuxtjs/mcp-toolkit": "^0.19.0",
96
- "@nuxtjs/robots": "^6.2.0",
97
- "@nuxtjs/sitemap": "^8.5.0",
95
+ "@nuxtjs/mcp-toolkit": "^0.21.0",
96
+ "@nuxtjs/robots": "^6.2.3",
97
+ "@nuxtjs/sitemap": "^8.5.1",
98
98
  "@types/better-sqlite3": "^9.6.0",
99
- "@vitest/coverage-v8": "^5.0.0",
100
- "@vue/test-utils": "^2.5.0",
101
- "@vueuse/nuxt": "^14.4.0",
99
+ "@vitest/coverage-v8": "^5.0.1",
100
+ "@vue/test-utils": "^2.5.1",
101
+ "@vueuse/nuxt": "^15.0.0",
102
102
  "better-sqlite3": "^13.0.3",
103
103
  "bumpp": "^12.3.0",
104
- "eslint": "^10.10.0",
105
- "eslint-plugin-harlanzw": "^0.21.1",
104
+ "eslint": "^10.11.0",
105
+ "eslint-plugin-harlanzw": "^0.23.0",
106
106
  "execa": "^10.0.1",
107
107
  "h3": "^1.15.11",
108
- "happy-dom": "^20.14.0",
108
+ "happy-dom": "^20.14.5",
109
109
  "nitropack": "^2.13.4",
110
110
  "nuxt": "^4.5.2",
111
111
  "nuxt-site-config": "^4.2.3",
112
- "nuxtseo-layer-devtools": "^5.3.14",
112
+ "nuxtseo-layer-devtools": "^5.3.16",
113
113
  "playwright": "^1.63.0",
114
114
  "playwright-core": "^1.63.0",
115
115
  "postgres": "^3.4.9",
116
116
  "tinyglobby": "^0.2.17",
117
117
  "typescript": "6.0.3",
118
118
  "unbuild": "^3.6.1",
119
- "vitest": "^5.0.0",
120
- "vue": "^3.5.42",
119
+ "vitest": "^5.0.1",
120
+ "vue": "^3.5.43",
121
121
  "vue-router": "^5.3.1",
122
122
  "vue-tsc": "^3.3.11",
123
- "wrangler": "^4.129.0",
124
- "zod": "^4.5.4"
123
+ "wrangler": "^4.136.3",
124
+ "zod": "^4.6.5"
125
125
  },
126
126
  "scripts": {
127
127
  "lint": "eslint .",