nuxt-ai-ready 3.3.0 → 3.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.
Files changed (43) hide show
  1. package/dist/chunks/agent-skills.mjs +1 -1
  2. package/dist/chunks/prerender.mjs +35 -17
  3. package/dist/cli.mjs +259 -0
  4. package/dist/module.d.mts +1 -0
  5. package/dist/module.json +1 -1
  6. package/dist/module.mjs +1 -1
  7. package/dist/runtime/llms-txt-files.js +58 -3
  8. package/dist/runtime/llms-txt-format.js +5 -4
  9. package/dist/runtime/markdown-text.d.ts +2 -0
  10. package/dist/runtime/markdown-text.js +3 -0
  11. package/dist/runtime/server/db/drizzle/queries.d.ts +2 -6
  12. package/dist/runtime/server/db/drizzle/queries.js +30 -10
  13. package/dist/runtime/server/db/queries.d.ts +2 -6
  14. package/dist/runtime/server/db/queries.js +12 -7
  15. package/dist/runtime/server/db/schema/postgres.d.ts +30 -0
  16. package/dist/runtime/server/db/schema/postgres.js +1 -0
  17. package/dist/runtime/server/db/schema/sqlite.d.ts +32 -0
  18. package/dist/runtime/server/db/schema/sqlite.js +1 -0
  19. package/dist/runtime/server/db/schema-sql.d.ts +1 -1
  20. package/dist/runtime/server/db/schema-sql.js +2 -1
  21. package/dist/runtime/server/db/seed.d.ts +8 -0
  22. package/dist/runtime/server/db/seed.js +16 -0
  23. package/dist/runtime/server/db/shared.d.ts +3 -3
  24. package/dist/runtime/server/db/shared.js +19 -1
  25. package/dist/runtime/server/middleware/markdown.prerender.js +9 -1
  26. package/dist/runtime/server/plugins/content-signal-capture-nitro3.prerender.d.ts +2 -0
  27. package/dist/runtime/server/plugins/content-signal-capture-nitro3.prerender.js +13 -0
  28. package/dist/runtime/server/plugins/content-signal-capture.prerender.d.ts +2 -0
  29. package/dist/runtime/server/plugins/content-signal-capture.prerender.js +13 -0
  30. package/dist/runtime/server/plugins/sitemap-seeder.js +9 -1
  31. package/dist/runtime/server/routes/llms.txt.get.js +4 -2
  32. package/dist/runtime/server/utils/llms-full.js +31 -5
  33. package/dist/runtime/server/utils/negotiation-response.js +3 -0
  34. package/dist/runtime/server/utils/prerender-html.d.ts +3 -0
  35. package/dist/runtime/server/utils/prerender-html.js +12 -0
  36. package/dist/runtime/server/utils/runCron.js +1 -1
  37. package/dist/runtime/server/utils/sitemap-lastmod.d.ts +4 -0
  38. package/dist/runtime/server/utils/sitemap-lastmod.js +19 -0
  39. package/dist/runtime/server/utils/sitemap-md.js +3 -2
  40. package/dist/runtime/server/utils/sitemap.js +4 -2
  41. package/dist/runtime/server/utils.js +50 -2
  42. package/dist/shared/{nuxt-ai-ready.BwnSMqRn.mjs → nuxt-ai-ready.O6_9Egm8.mjs} +367 -246
  43. package/package.json +3 -3
@@ -0,0 +1,4 @@
1
+ /** Mark dates added by this module without changing the public sitemap date. */
2
+ export declare function markIndexedSitemapLastmods(xml: string, enrichedUrls: ReadonlySet<string>): string;
3
+ /** The source crawl must never treat our indexed date as a CMS change. */
4
+ export declare function stripIndexedSitemapLastmods(body: Uint8Array): Uint8Array;
@@ -0,0 +1,19 @@
1
+ const INDEXED_LASTMOD_MARKER = "<!-- nuxt-ai-ready:indexed-lastmod -->";
2
+ export function markIndexedSitemapLastmods(xml, enrichedUrls) {
3
+ const escapedUrls = new Set([...enrichedUrls].map((url) => url.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&apos;")));
4
+ return xml.replace(/<url>([\s\S]*?)<\/url>/g, (entry, body) => {
5
+ const loc = body.match(/<loc>([^<]*)<\/loc>/)?.[1];
6
+ return loc && escapedUrls.has(loc) ? entry.replace("<lastmod>", `${INDEXED_LASTMOD_MARKER}<lastmod>`) : entry;
7
+ });
8
+ }
9
+ export function stripIndexedSitemapLastmods(body) {
10
+ let xml;
11
+ try {
12
+ xml = new TextDecoder("utf-8", { fatal: true }).decode(body);
13
+ } catch {
14
+ return body;
15
+ }
16
+ if (!xml.includes(INDEXED_LASTMOD_MARKER))
17
+ return body;
18
+ return new TextEncoder().encode(xml.replace(/<!-- nuxt-ai-ready:indexed-lastmod -->\s*<lastmod>[^<]*<\/lastmod>/g, ""));
19
+ }
@@ -1,11 +1,12 @@
1
1
  import { toMarkdownPath } from "../../markdown-path.js";
2
+ import { escapeMarkdownHtml } from "../../markdown-text.js";
2
3
  import { toDeployedRoute } from "../../route-path.js";
3
4
  export const SITEMAP_MD_ROUTE = "/sitemap.md";
4
5
  function topSegment(route) {
5
6
  return route.split("/").filter(Boolean)[0] || "";
6
7
  }
7
8
  function escapeTitle(title) {
8
- return title.replaceAll("[", "\\[").replaceAll("]", "\\]");
9
+ return escapeMarkdownHtml(title).replaceAll("[", "\\[").replaceAll("]", "\\]");
9
10
  }
10
11
  function formatEntry(entry, resolveHref) {
11
12
  const title = escapeTitle(entry.title?.trim() || entry.route);
@@ -35,7 +36,7 @@ export function buildSitemapMd(entries, options = {}) {
35
36
  return 1;
36
37
  return a.localeCompare(b);
37
38
  });
38
- const parts = [`# ${options.siteName || "Site"} Sitemap`, "", "All pages in Markdown format."];
39
+ const parts = [`# ${escapeMarkdownHtml(options.siteName || "Site")} Sitemap`, "", "All pages in Markdown format."];
39
40
  for (const key of keys) {
40
41
  parts.push("", `## ${key || "Root"}`);
41
42
  for (const entry of groups.get(key))
@@ -7,6 +7,7 @@ import { logger } from "../logger.js";
7
7
  import { getCfEnv, hasAssets } from "./cloudflare.js";
8
8
  import { createUniversalContext } from "./context.js";
9
9
  import { fetchRawWithEvent } from "./fetch.js";
10
+ import { stripIndexedSitemapLastmods } from "./sitemap-lastmod.js";
10
11
  const FETCH_TIMEOUT = 15e3;
11
12
  const SITEMAP_FALLBACK_ORIGIN = "http://localhost";
12
13
  const SITEMAP_READER_LIMITS = {
@@ -221,11 +222,12 @@ function createLocalSitemapLoader(event, usePublicAsset, loadedBodies) {
221
222
  bytesRead: read.result.bytesRead
222
223
  };
223
224
  }
224
- loadedBodies.set(request.url, read.result.body);
225
+ const sourceBody = stripIndexedSitemapLastmods(read.result.body);
226
+ loadedBodies.set(request.url, sourceBody);
225
227
  return {
226
228
  _tag: "body",
227
229
  url: request.url,
228
- body: read.result.body
230
+ body: sourceBody
229
231
  };
230
232
  };
231
233
  }
@@ -3,11 +3,57 @@ import { useNitroApp } from "#nuxtseo/nitro";
3
3
  import { stripLlmsTxtDirectiveHtml } from "../llms-txt-directive.js";
4
4
  import { hoistMetaDescription } from "./utils/frontmatter.js";
5
5
  const RE_NBSP = /\u00A0/g;
6
+ const JSON_LD_MAX_BYTES = 65536;
7
+ const JSON_LD_MAX_BLOCKS = 16;
8
+ const IMAGE_META_FIELDS = [
9
+ "og:image",
10
+ "og:image:url",
11
+ "og:image:secure_url",
12
+ "og:image:type",
13
+ "og:image:width",
14
+ "og:image:height",
15
+ "og:image:alt",
16
+ "twitter:image",
17
+ "twitter:image:src",
18
+ "twitter:image:alt"
19
+ ];
20
+ function parseJsonLdBlock(text, remainingBytes) {
21
+ if (text.length > remainingBytes)
22
+ return { _tag: "Skipped" };
23
+ const content = text.trim();
24
+ if (!content)
25
+ return { _tag: "Skipped" };
26
+ let value;
27
+ try {
28
+ value = JSON.parse(text);
29
+ } catch {
30
+ return { _tag: "Skipped" };
31
+ }
32
+ const isObject = (item) => typeof item === "object" && item !== null && !Array.isArray(item);
33
+ if (!isObject(value) && !(Array.isArray(value) && value.every(isObject)))
34
+ return { _tag: "Skipped" };
35
+ const markdown = `\`\`\`json
36
+ ${content.replace(RE_NBSP, "\\u00a0")}
37
+ \`\`\``;
38
+ const bytes = new TextEncoder().encode(markdown).byteLength + 2;
39
+ if (bytes > remainingBytes)
40
+ return { _tag: "Skipped" };
41
+ return { _tag: "Preserved", markdown, bytes };
42
+ }
6
43
  function normalizeWhitespace(text) {
7
44
  return text.replace(RE_NBSP, " ");
8
45
  }
9
46
  function buildMdreamOptions(url, mdreamOptions, meta, extractUpdatedAt, additionalFields) {
10
47
  const extraction = {
48
+ "script[type]": (el) => {
49
+ if (el.attributes.type?.trim().toLowerCase() !== "application/ld+json" || meta.jsonLd.blocks.length >= JSON_LD_MAX_BLOCKS)
50
+ return;
51
+ const block = parseJsonLdBlock(el.textContent, JSON_LD_MAX_BYTES - meta.jsonLd.bytes);
52
+ if (block._tag === "Preserved") {
53
+ meta.jsonLd.blocks.push(block.markdown);
54
+ meta.jsonLd.bytes += block.bytes;
55
+ }
56
+ },
11
57
  'meta[name="keywords"]': (el) => {
12
58
  meta.metaKeywords = el.attributes.content || "";
13
59
  },
@@ -30,6 +76,7 @@ function buildMdreamOptions(url, mdreamOptions, meta, extractUpdatedAt, addition
30
76
  };
31
77
  const frontmatter = {
32
78
  additionalFields,
79
+ metaFields: [...IMAGE_META_FIELDS],
33
80
  onExtract: (fm) => {
34
81
  if (fm.title)
35
82
  meta.title = fm.title;
@@ -70,13 +117,14 @@ export function extractLastUpdated(html) {
70
117
  return void 0;
71
118
  }
72
119
  export async function convertHtmlToMarkdown(html, url, mdreamOptions, opts) {
73
- const meta = { title: "", description: "", metaKeywords: "", headings: [], textContent: [] };
120
+ const meta = { title: "", description: "", metaKeywords: "", headings: [], textContent: [], jsonLd: { blocks: [], bytes: 0 } };
74
121
  const options = buildMdreamOptions(url, mdreamOptions, meta, opts.extractUpdatedAt ?? false, opts.additionalFrontmatter ?? {});
75
122
  const nitroApp = useNitroApp();
76
123
  await nitroApp.hooks.callHook("ai-ready:mdreamConfig", options);
124
+ const convertedMarkdown = hoistMetaDescription(htmlToMarkdown(stripLlmsTxtDirectiveHtml(html), options));
77
125
  const context = {
78
126
  html,
79
- markdown: hoistMetaDescription(htmlToMarkdown(stripLlmsTxtDirectiveHtml(html), options)),
127
+ markdown: [convertedMarkdown, ...meta.jsonLd.blocks].join("\n\n"),
80
128
  route: opts.hooks.route,
81
129
  title: meta.title,
82
130
  description: meta.description,