nuxt-ai-ready 1.7.13 → 1.8.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.
@@ -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.DQXoppXw.mjs';
5
+ import { A as AGENT_SKILLS_SCHEMA } from '../shared/nuxt-ai-ready.B6l-68o5.mjs';
6
6
  import 'node:module';
7
7
  import '@nuxt/kit';
8
8
  import 'defu';
@@ -5,7 +5,7 @@ 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, withBase, joinURL } from 'ufo';
8
- import { l as logger, M as MARKDOWN_LINK_AVAILABILITY_FILE } from '../shared/nuxt-ai-ready.DQXoppXw.mjs';
8
+ import { l as logger, M as MARKDOWN_LINK_AVAILABILITY_FILE } from '../shared/nuxt-ai-ready.B6l-68o5.mjs';
9
9
  import { normalizePagePath, toMarkdownPath } from '../../dist/runtime/markdown-path.js';
10
10
  import { toLogicalRoute, toDeployedRoute } from '../../dist/runtime/route-path.js';
11
11
  import { initSchema, computeContentHash, insertPage, queryAllPages, exportDbDump } from '../../dist/runtime/server/db/shared.js';
@@ -137,38 +137,52 @@ async function initCrawler(state) {
137
137
  }
138
138
  state.initialized = true;
139
139
  }
140
- async function createPrerenderDatabase(dbPath) {
141
- const nodeVersion = Number.parseInt(process.versions.node?.split(".")[0] || "0");
142
- if (nodeVersion >= 22) {
143
- const { DatabaseSync } = await import('node:sqlite');
144
- const sqlite2 = new DatabaseSync(dbPath);
145
- return {
146
- all: async (sql, params = []) => sqlite2.prepare(sql).all(...params),
147
- first: async (sql, params = []) => sqlite2.prepare(sql).get(...params),
148
- exec: async (sql, params = []) => {
149
- if (params.length)
150
- sqlite2.prepare(sql).run(...params);
151
- else
152
- sqlite2.exec(sql);
153
- },
154
- close: async () => {
155
- sqlite2.close();
156
- }
157
- };
140
+ const PRERENDER_PRAGMAS = [
141
+ "PRAGMA synchronous = OFF",
142
+ "PRAGMA temp_store = MEMORY"
143
+ ];
144
+ function createSqliteAdapter(sqlite) {
145
+ const statements = /* @__PURE__ */ new Map();
146
+ function prepare(sql) {
147
+ let statement = statements.get(sql);
148
+ if (!statement) {
149
+ statement = sqlite.prepare(sql);
150
+ statements.set(sql, statement);
151
+ }
152
+ return statement;
158
153
  }
159
- const { default: Database } = await import('better-sqlite3');
160
- const sqlite = new Database(dbPath);
161
154
  return {
162
- all: async (sql, params = []) => sqlite.prepare(sql).all(...params),
163
- first: async (sql, params = []) => sqlite.prepare(sql).get(...params),
155
+ all: async (sql, params = []) => prepare(sql).all(...params),
156
+ first: async (sql, params = []) => prepare(sql).get(...params),
164
157
  exec: async (sql, params = []) => {
165
- sqlite.prepare(sql).run(...params);
158
+ if (params.length) {
159
+ prepare(sql).run(...params);
160
+ return;
161
+ }
162
+ statements.clear();
163
+ sqlite.exec(sql);
166
164
  },
167
165
  close: async () => {
166
+ statements.clear();
168
167
  sqlite.close();
169
168
  }
170
169
  };
171
170
  }
171
+ async function createPrerenderDatabase(dbPath) {
172
+ const nodeVersion = Number.parseInt(process.versions.node?.split(".")[0] || "0");
173
+ if (nodeVersion >= 22) {
174
+ const { DatabaseSync } = await import('node:sqlite');
175
+ const sqlite2 = new DatabaseSync(dbPath);
176
+ for (const pragma of PRERENDER_PRAGMAS)
177
+ sqlite2.exec(pragma);
178
+ return createSqliteAdapter(sqlite2);
179
+ }
180
+ const { default: Database } = await import('better-sqlite3');
181
+ const sqlite = new Database(dbPath);
182
+ for (const pragma of PRERENDER_PRAGMAS)
183
+ sqlite.exec(pragma);
184
+ return createSqliteAdapter(sqlite);
185
+ }
172
186
  function flattenHeadings(headings) {
173
187
  return (headings || []).map((h) => Object.entries(h).map(([tag, text]) => `${tag}:${text}`).join("")).join("|");
174
188
  }
@@ -224,6 +238,7 @@ async function processSitemapEntry(state, nuxt, nitro, entry) {
224
238
  logger.debug(`Fetching markdown for ${route} \u2192 ${mdUrl}`);
225
239
  const res = await globalThis.$fetch(mdUrl, {
226
240
  headers: { "x-nitro-prerender": mdRoute },
241
+ retry: 0,
227
242
  signal: AbortSignal.timeout(PRERENDER_PAGE_TIMEOUT)
228
243
  }).catch((err) => {
229
244
  if (err?.name === "TimeoutError" || err?.name === "AbortError")
package/dist/module.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "nuxt": ">=4.0.0"
5
5
  },
6
6
  "configKey": "aiReady",
7
- "version": "1.7.13",
7
+ "version": "1.8.0",
8
8
  "builder": {
9
9
  "@nuxt/module-builder": "1.0.3",
10
10
  "unbuild": "3.6.1"
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.DQXoppXw.mjs';
10
+ export { m as default } from './shared/nuxt-ai-ready.B6l-68o5.mjs';
11
11
  import '../dist/runtime/server/utils/discovery-response.js';
12
12
  import 'node:url';
13
13
  import 'pathe';
@@ -113,6 +113,19 @@ export interface ModuleOptions {
113
113
  * @default undefined
114
114
  */
115
115
  apiCatalog?: false | ApiCatalogConfig;
116
+ /**
117
+ * Read a page's Markdown from the installed content module instead of
118
+ * rendering the page and converting its HTML.
119
+ *
120
+ * Supports @nuxt/content v3 and @harlan-zw/comark-content. When a content
121
+ * module is present this skips one SSR render per route and keeps the source
122
+ * Markdown intact, so MDC blocks, tables and code fences survive.
123
+ *
124
+ * Set to false to convert the rendered HTML instead. Do that when a page's
125
+ * meaning depends on what its components render rather than on its source.
126
+ * @default true
127
+ */
128
+ contentSource?: boolean;
116
129
  /**
117
130
  * Options to pass to mdream htmlToMarkdown function
118
131
  */
@@ -2,11 +2,11 @@ import { createHash, randomBytes } from 'node:crypto';
2
2
  import { readFile, mkdir, writeFile, access } from 'node:fs/promises';
3
3
  import { createRequire } from 'node:module';
4
4
  import { dirname, basename, join } from 'node:path';
5
- import { useLogger, addTypeTemplate, addTemplate, defineNuxtModule, createResolver, addServerImports, hasNuxtModule, addServerPlugin, addServerHandler, extendRouteRules, addPlugin, addImports } from '@nuxt/kit';
5
+ import { hasNuxtModule, useNuxt, useLogger, addTypeTemplate, addTemplate, defineNuxtModule, createResolver, addServerImports, addServerPlugin, addServerHandler, extendRouteRules, addPlugin, addImports } from '@nuxt/kit';
6
6
  import defu from 'defu';
7
7
  import { installNuxtSiteConfig, useSiteConfig, withSiteUrl } from 'nuxt-site-config/kit';
8
- import { renderNitroTypeAugmentations, setupNitroRuntimeCompatibility, resolveNuxtContentVersion } from 'nuxtseo-shared/kit';
9
- import { resolvePackageJSON, readPackageJSON } from 'pkg-types';
8
+ import { resolveNuxtContentVersion, renderNitroTypeAugmentations, setupNitroRuntimeCompatibility } from 'nuxtseo-shared/kit';
9
+ import { readPackageJSON, resolvePackageJSON } from 'pkg-types';
10
10
  import { fileURLToPath, pathToFileURL } from 'node:url';
11
11
  import { MCP_SERVER_CARD_MEDIA_TYPE, AI_CATALOG_MEDIA_TYPE } from '../../dist/runtime/server/utils/discovery-response.js';
12
12
  import { isAbsolute, join as join$1 } from 'pathe';
@@ -77,6 +77,93 @@ export async function readPageDataFromFilesystem() {
77
77
  `;
78
78
  }
79
79
 
80
+ const COMARK_MODULE = "@harlan-zw/comark-content";
81
+ const COMARK_MINIMUM = [0, 1, 2];
82
+ async function comarkSatisfiesMinimum() {
83
+ const url = useNuxt().options.rootDir;
84
+ const pkg = await readPackageJSON(COMARK_MODULE, { url }).catch(() => {
85
+ return null;
86
+ });
87
+ const parts = pkg?.version?.split(".").map((part) => Number.parseInt(part, 10));
88
+ if (!parts || parts.length < 3 || parts.some((part) => !Number.isFinite(part)))
89
+ return false;
90
+ for (const [index, minimum] of COMARK_MINIMUM.entries()) {
91
+ if (parts[index] !== minimum)
92
+ return parts[index] > minimum;
93
+ }
94
+ return true;
95
+ }
96
+ async function resolveContentSource(enabled) {
97
+ if (!enabled)
98
+ return { _tag: "None" };
99
+ const contentVersion = await resolveNuxtContentVersion();
100
+ if (contentVersion && contentVersion.version === 3)
101
+ return { _tag: "NuxtContentV3" };
102
+ if (hasNuxtModule(COMARK_MODULE) && await comarkSatisfiesMinimum())
103
+ return { _tag: "ComarkContent" };
104
+ return { _tag: "None" };
105
+ }
106
+ const NUXT_CONTENT_V3_LOOKUP = `
107
+ import { queryCollection } from '@nuxt/content/server'
108
+ import manifest from '#content/manifest'
109
+ import { stringify } from 'minimark/stringify'
110
+
111
+ const pageCollections = Object.entries(manifest)
112
+ .filter(([, info]) => info.type === 'page')
113
+ .map(([name]) => name)
114
+
115
+ export async function lookupContentPage(event, path) {
116
+ if (!pageCollections.length) return null
117
+ const candidates = path === '/' ? ['/'] : [path, path.replace(/\\/$/, '')]
118
+ for (const collection of pageCollections) {
119
+ for (const candidate of candidates) {
120
+ const page = await queryCollection(event, collection).path(candidate).first().catch(() => null)
121
+ if (!page) continue
122
+ const markdown = stringify({ ...page.body, type: 'minimark' }, { format: 'markdown/html' })
123
+ return {
124
+ markdown,
125
+ title: page.title,
126
+ description: page.description,
127
+ updatedAt: page.seo?.articleModifiedTime || page.updatedAt,
128
+ }
129
+ }
130
+ }
131
+ return null
132
+ }
133
+ `;
134
+ const COMARK_LOOKUP = `
135
+ import { queryCollection, queryCollectionManifest, renderPageMarkdown } from '@harlan-zw/comark-content/server'
136
+
137
+ let manifestPromise
138
+
139
+ export async function lookupContentPage(event, path) {
140
+ manifestPromise ??= queryCollectionManifest(event)
141
+ const manifest = await manifestPromise
142
+ for (const { name } of manifest) {
143
+ const page = await queryCollection(event, name).path(path).first()
144
+ if (!page) continue
145
+ return {
146
+ markdown: await renderPageMarkdown(page.body),
147
+ title: page.title,
148
+ description: page.description,
149
+ updatedAt: page.seo?.articleModifiedTime || page.updatedAt,
150
+ }
151
+ }
152
+ return null
153
+ }
154
+ `;
155
+ const NO_CONTENT_LOOKUP = `export async function lookupContentPage() { return null }`;
156
+ function contentLookupModule(source) {
157
+ switch (source._tag) {
158
+ case "NuxtContentV3":
159
+ return NUXT_CONTENT_V3_LOOKUP;
160
+ case "ComarkContent":
161
+ return COMARK_LOOKUP;
162
+ case "None":
163
+ return NO_CONTENT_LOOKUP;
164
+ }
165
+ }
166
+
80
167
  const logger = useLogger("nuxt-ai-ready");
81
168
 
82
169
  function resolveModuleEntryUrl(url) {
@@ -1043,8 +1130,7 @@ ${details}`);
1043
1130
  if (mcpConfig.resources !== false)
1044
1131
  (paths.resources ||= []).push(`${mcpRuntimeDir}/resources`);
1045
1132
  });
1046
- const contentVersion = await resolveNuxtContentVersion();
1047
- const hasNuxtContentV3 = !!(contentVersion && contentVersion.version === 3);
1133
+ const contentSource = await resolveContentSource(config.contentSource !== false);
1048
1134
  if (typeof config.contentSignal === "object") {
1049
1135
  const robotsOpts = nuxt.options.robots || {};
1050
1136
  nuxt.options.robots = robotsOpts;
@@ -1423,34 +1509,7 @@ export const logger = createModuleLogger('nuxt-ai-ready', ${!!config.debug})
1423
1509
  mcp: { enabled: mcpAvailable, tools: mcpAvailable && config.mcp?.tools !== false && hasMcpSiteTools, resources: mcpAvailable && config.mcp?.resources !== false },
1424
1510
  cron: !!config.cron
1425
1511
  })}`;
1426
- nitroConfig.virtual["#ai-ready-virtual/content-lookup.mjs"] = hasNuxtContentV3 ? `
1427
- import { queryCollection } from '@nuxt/content/server'
1428
- import manifest from '#content/manifest'
1429
- import { stringify } from 'minimark/stringify'
1430
-
1431
- const pageCollections = Object.entries(manifest)
1432
- .filter(([, info]) => info.type === 'page')
1433
- .map(([name]) => name)
1434
-
1435
- export async function lookupContentPage(event, path) {
1436
- if (!pageCollections.length) return null
1437
- const candidates = path === '/' ? ['/'] : [path, path.replace(/\\/$/, '')]
1438
- for (const collection of pageCollections) {
1439
- for (const candidate of candidates) {
1440
- const page = await queryCollection(event, collection).path(candidate).first().catch(() => null)
1441
- if (!page) continue
1442
- const markdown = stringify({ ...page.body, type: 'minimark' }, { format: 'markdown/html' })
1443
- return {
1444
- markdown,
1445
- title: page.title,
1446
- description: page.description,
1447
- updatedAt: page.seo?.articleModifiedTime || page.updatedAt,
1448
- }
1449
- }
1450
- }
1451
- return null
1452
- }
1453
- ` : `export async function lookupContentPage() { return null }`;
1512
+ nitroConfig.virtual["#ai-ready-virtual/content-lookup.mjs"] = contentLookupModule(contentSource);
1454
1513
  });
1455
1514
  const database = refineDatabaseConfig(config.database || {}, nuxt.options.rootDir);
1456
1515
  nuxt.options.runtimeConfig["nuxt-ai-ready"] = {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "nuxt-ai-ready",
3
3
  "type": "module",
4
- "version": "1.7.13",
4
+ "version": "1.8.0",
5
5
  "description": "Best practice AI & LLM discoverability for Nuxt sites.",
6
6
  "author": {
7
7
  "name": "Harlan Wilton",
@@ -82,6 +82,7 @@
82
82
  "devDependencies": {
83
83
  "@antfu/eslint-config": "^9.3.0",
84
84
  "@arethetypeswrong/cli": "^0.18.5",
85
+ "@harlan-zw/comark-content": "0.1.2",
85
86
  "@libsql/client": "^0.17.4",
86
87
  "@nuxt/content": "^3.15.2",
87
88
  "@nuxt/module-builder": "^1.0.3",