nuxt-ai-ready 0.8.1 → 0.9.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.
- package/dist/cli.d.mts +1 -0
- package/dist/cli.mjs +299 -0
- package/dist/module.d.mts +1 -1
- package/dist/module.json +1 -1
- package/dist/module.mjs +52 -22
- package/dist/runtime/server/routes/__ai-ready/cron.get.js +12 -2
- package/dist/runtime/server/routes/__ai-ready/indexnow.post.js +1 -1
- package/dist/runtime/server/routes/__ai-ready/status.get.js +8 -2
- package/dist/runtime/server/routes/__ai-ready-debug.get.js +1 -1
- package/dist/runtime/server/routes/indexnow-key.get.js +2 -2
- package/dist/runtime/server/utils/indexPage.js +5 -2
- package/dist/runtime/server/utils/indexnow.js +2 -2
- package/dist/runtime/server/utils/runCron.js +2 -2
- package/dist/runtime/server/utils.js +2 -1
- package/dist/runtime/types.d.ts +5 -6
- package/package.json +8 -4
package/dist/cli.d.mts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
package/dist/cli.mjs
ADDED
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
|
+
import fsp from 'node:fs/promises';
|
|
4
|
+
import { defineCommand, runMain } from 'citty';
|
|
5
|
+
import { consola } from 'consola';
|
|
6
|
+
import { colors } from 'consola/utils';
|
|
7
|
+
import { resolve, join } from 'pathe';
|
|
8
|
+
|
|
9
|
+
async function getSecret(cwd) {
|
|
10
|
+
const secretPath = join(cwd, "node_modules/.cache/nuxt/ai-ready/secret");
|
|
11
|
+
if (!existsSync(secretPath)) {
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
return fsp.readFile(secretPath, "utf-8").then((s) => s.trim()).catch(() => null);
|
|
15
|
+
}
|
|
16
|
+
const main = defineCommand({
|
|
17
|
+
meta: {
|
|
18
|
+
name: "nuxt-ai-ready",
|
|
19
|
+
description: "Nuxt AI Ready CLI"
|
|
20
|
+
},
|
|
21
|
+
subCommands: {
|
|
22
|
+
status: defineCommand({
|
|
23
|
+
meta: {
|
|
24
|
+
name: "status",
|
|
25
|
+
description: "Show indexing status and IndexNow sync progress"
|
|
26
|
+
},
|
|
27
|
+
args: {
|
|
28
|
+
url: {
|
|
29
|
+
type: "string",
|
|
30
|
+
alias: "u",
|
|
31
|
+
description: "Site URL (default: http://localhost:3000)",
|
|
32
|
+
default: "http://localhost:3000"
|
|
33
|
+
},
|
|
34
|
+
cwd: {
|
|
35
|
+
type: "string",
|
|
36
|
+
description: "Working directory",
|
|
37
|
+
default: "."
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
async run({ args }) {
|
|
41
|
+
const cwd = resolve(args.cwd || ".");
|
|
42
|
+
const secret = await getSecret(cwd);
|
|
43
|
+
if (!secret) {
|
|
44
|
+
consola.error("No secret found. Run `nuxi dev` or `nuxi build` first to generate one.");
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
const url = `${args.url}/__ai-ready/status?secret=${secret}`;
|
|
48
|
+
consola.info(`Fetching status from ${args.url}...`);
|
|
49
|
+
const res = await fetch(url).then((r) => r.json()).catch((err) => {
|
|
50
|
+
consola.error(`Failed to connect: ${err.message}`);
|
|
51
|
+
return null;
|
|
52
|
+
});
|
|
53
|
+
if (!res)
|
|
54
|
+
return;
|
|
55
|
+
consola.box("AI Ready Status");
|
|
56
|
+
consola.info(`Total pages: ${colors.cyan(res.total?.toString() || "0")}`);
|
|
57
|
+
consola.info(`Indexed: ${colors.green(res.indexed?.toString() || "0")}`);
|
|
58
|
+
consola.info(`Pending: ${colors.yellow(res.pending?.toString() || "0")}`);
|
|
59
|
+
if (res.indexNow) {
|
|
60
|
+
consola.log("");
|
|
61
|
+
consola.info(colors.bold("IndexNow:"));
|
|
62
|
+
consola.info(` Pending: ${colors.yellow(res.indexNow.pending?.toString() || "0")}`);
|
|
63
|
+
consola.info(` Total submitted: ${colors.green(res.indexNow.totalSubmitted?.toString() || "0")}`);
|
|
64
|
+
if (res.indexNow.lastSubmittedAt) {
|
|
65
|
+
const date = new Date(res.indexNow.lastSubmittedAt);
|
|
66
|
+
consola.info(` Last submitted: ${colors.dim(date.toISOString())}`);
|
|
67
|
+
}
|
|
68
|
+
if (res.indexNow.lastError) {
|
|
69
|
+
consola.info(` Last error: ${colors.red(res.indexNow.lastError)}`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}),
|
|
74
|
+
poll: defineCommand({
|
|
75
|
+
meta: {
|
|
76
|
+
name: "poll",
|
|
77
|
+
description: "Trigger page indexing"
|
|
78
|
+
},
|
|
79
|
+
args: {
|
|
80
|
+
url: {
|
|
81
|
+
type: "string",
|
|
82
|
+
alias: "u",
|
|
83
|
+
description: "Site URL (default: http://localhost:3000)",
|
|
84
|
+
default: "http://localhost:3000"
|
|
85
|
+
},
|
|
86
|
+
limit: {
|
|
87
|
+
type: "string",
|
|
88
|
+
alias: "l",
|
|
89
|
+
description: "Max pages to process",
|
|
90
|
+
default: "10"
|
|
91
|
+
},
|
|
92
|
+
all: {
|
|
93
|
+
type: "boolean",
|
|
94
|
+
alias: "a",
|
|
95
|
+
description: "Process all pending pages"
|
|
96
|
+
},
|
|
97
|
+
cwd: {
|
|
98
|
+
type: "string",
|
|
99
|
+
description: "Working directory",
|
|
100
|
+
default: "."
|
|
101
|
+
}
|
|
102
|
+
},
|
|
103
|
+
async run({ args }) {
|
|
104
|
+
const cwd = resolve(args.cwd || ".");
|
|
105
|
+
const secret = await getSecret(cwd);
|
|
106
|
+
if (!secret) {
|
|
107
|
+
consola.error("No secret found. Run `nuxi dev` or `nuxi build` first.");
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
const params = new URLSearchParams({ secret });
|
|
111
|
+
if (args.all) {
|
|
112
|
+
params.set("all", "true");
|
|
113
|
+
} else {
|
|
114
|
+
params.set("limit", args.limit || "10");
|
|
115
|
+
}
|
|
116
|
+
const url = `${args.url}/__ai-ready/poll?${params}`;
|
|
117
|
+
consola.info(`Triggering poll at ${args.url}...`);
|
|
118
|
+
const res = await fetch(url, { method: "POST" }).then((r) => r.json()).catch((err) => {
|
|
119
|
+
consola.error(`Failed: ${err.message}`);
|
|
120
|
+
return null;
|
|
121
|
+
});
|
|
122
|
+
if (!res)
|
|
123
|
+
return;
|
|
124
|
+
consola.success(`Indexed: ${colors.green(res.indexed?.toString() || "0")} pages`);
|
|
125
|
+
consola.info(`Remaining: ${colors.yellow(res.remaining?.toString() || "0")}`);
|
|
126
|
+
if (res.errors?.length) {
|
|
127
|
+
consola.warn(`Errors: ${res.errors.length}`);
|
|
128
|
+
}
|
|
129
|
+
if (res.duration) {
|
|
130
|
+
consola.info(`Duration: ${colors.dim(`${res.duration}ms`)}`);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}),
|
|
134
|
+
restore: defineCommand({
|
|
135
|
+
meta: {
|
|
136
|
+
name: "restore",
|
|
137
|
+
description: "Restore database from prerendered dump"
|
|
138
|
+
},
|
|
139
|
+
args: {
|
|
140
|
+
url: {
|
|
141
|
+
type: "string",
|
|
142
|
+
alias: "u",
|
|
143
|
+
description: "Site URL (default: http://localhost:3000)",
|
|
144
|
+
default: "http://localhost:3000"
|
|
145
|
+
},
|
|
146
|
+
clear: {
|
|
147
|
+
type: "boolean",
|
|
148
|
+
description: "Clear existing pages first (default: true)",
|
|
149
|
+
default: true
|
|
150
|
+
},
|
|
151
|
+
cwd: {
|
|
152
|
+
type: "string",
|
|
153
|
+
description: "Working directory",
|
|
154
|
+
default: "."
|
|
155
|
+
}
|
|
156
|
+
},
|
|
157
|
+
async run({ args }) {
|
|
158
|
+
const cwd = resolve(args.cwd || ".");
|
|
159
|
+
const secret = await getSecret(cwd);
|
|
160
|
+
if (!secret) {
|
|
161
|
+
consola.error("No secret found. Run `nuxi dev` or `nuxi build` first.");
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
const params = new URLSearchParams({ secret });
|
|
165
|
+
if (!args.clear) {
|
|
166
|
+
params.set("clear", "false");
|
|
167
|
+
}
|
|
168
|
+
const url = `${args.url}/__ai-ready/restore?${params}`;
|
|
169
|
+
consola.info(`Restoring database at ${args.url}...`);
|
|
170
|
+
const res = await fetch(url, { method: "POST" }).then((r) => r.json()).catch((err) => {
|
|
171
|
+
consola.error(`Failed: ${err.message}`);
|
|
172
|
+
return null;
|
|
173
|
+
});
|
|
174
|
+
if (!res)
|
|
175
|
+
return;
|
|
176
|
+
consola.success(`Restored: ${colors.green(res.restored?.toString() || "0")} pages`);
|
|
177
|
+
if (res.cleared) {
|
|
178
|
+
consola.info(`Cleared: ${colors.yellow(res.cleared?.toString() || "0")} existing pages`);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}),
|
|
182
|
+
prune: defineCommand({
|
|
183
|
+
meta: {
|
|
184
|
+
name: "prune",
|
|
185
|
+
description: "Remove stale routes from database"
|
|
186
|
+
},
|
|
187
|
+
args: {
|
|
188
|
+
url: {
|
|
189
|
+
type: "string",
|
|
190
|
+
alias: "u",
|
|
191
|
+
description: "Site URL (default: http://localhost:3000)",
|
|
192
|
+
default: "http://localhost:3000"
|
|
193
|
+
},
|
|
194
|
+
dry: {
|
|
195
|
+
type: "boolean",
|
|
196
|
+
alias: "d",
|
|
197
|
+
description: "Preview without deleting"
|
|
198
|
+
},
|
|
199
|
+
ttl: {
|
|
200
|
+
type: "string",
|
|
201
|
+
description: "Override pruneTtl config"
|
|
202
|
+
},
|
|
203
|
+
cwd: {
|
|
204
|
+
type: "string",
|
|
205
|
+
description: "Working directory",
|
|
206
|
+
default: "."
|
|
207
|
+
}
|
|
208
|
+
},
|
|
209
|
+
async run({ args }) {
|
|
210
|
+
const cwd = resolve(args.cwd || ".");
|
|
211
|
+
const secret = await getSecret(cwd);
|
|
212
|
+
if (!secret && !args.dry) {
|
|
213
|
+
consola.error("No secret found. Run `nuxi dev` or `nuxi build` first.");
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
const params = new URLSearchParams();
|
|
217
|
+
if (secret)
|
|
218
|
+
params.set("secret", secret);
|
|
219
|
+
if (args.dry)
|
|
220
|
+
params.set("dry", "true");
|
|
221
|
+
if (args.ttl)
|
|
222
|
+
params.set("ttl", args.ttl);
|
|
223
|
+
const url = `${args.url}/__ai-ready/prune?${params}`;
|
|
224
|
+
consola.info(`${args.dry ? "Previewing" : "Pruning"} stale routes at ${args.url}...`);
|
|
225
|
+
const res = await fetch(url, { method: "POST" }).then((r) => r.json()).catch((err) => {
|
|
226
|
+
consola.error(`Failed: ${err.message}`);
|
|
227
|
+
return null;
|
|
228
|
+
});
|
|
229
|
+
if (!res)
|
|
230
|
+
return;
|
|
231
|
+
if (args.dry) {
|
|
232
|
+
consola.info(`Would prune: ${colors.yellow(res.count?.toString() || "0")} routes`);
|
|
233
|
+
if (res.routes?.length) {
|
|
234
|
+
for (const route of res.routes.slice(0, 20)) {
|
|
235
|
+
consola.log(` ${colors.dim("\u2022")} ${route}`);
|
|
236
|
+
}
|
|
237
|
+
if (res.routes.length > 20) {
|
|
238
|
+
consola.log(` ${colors.dim(`... and ${res.routes.length - 20} more`)}`);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
} else {
|
|
242
|
+
consola.success(`Pruned: ${colors.green(res.pruned?.toString() || "0")} routes`);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}),
|
|
246
|
+
indexnow: defineCommand({
|
|
247
|
+
meta: {
|
|
248
|
+
name: "indexnow",
|
|
249
|
+
description: "Trigger IndexNow sync"
|
|
250
|
+
},
|
|
251
|
+
args: {
|
|
252
|
+
url: {
|
|
253
|
+
type: "string",
|
|
254
|
+
alias: "u",
|
|
255
|
+
description: "Site URL (default: http://localhost:3000)",
|
|
256
|
+
default: "http://localhost:3000"
|
|
257
|
+
},
|
|
258
|
+
limit: {
|
|
259
|
+
type: "string",
|
|
260
|
+
alias: "l",
|
|
261
|
+
description: "Max URLs to submit",
|
|
262
|
+
default: "100"
|
|
263
|
+
},
|
|
264
|
+
cwd: {
|
|
265
|
+
type: "string",
|
|
266
|
+
description: "Working directory",
|
|
267
|
+
default: "."
|
|
268
|
+
}
|
|
269
|
+
},
|
|
270
|
+
async run({ args }) {
|
|
271
|
+
const cwd = resolve(args.cwd || ".");
|
|
272
|
+
const secret = await getSecret(cwd);
|
|
273
|
+
if (!secret) {
|
|
274
|
+
consola.error("No secret found. Run `nuxi dev` or `nuxi build` first.");
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
const params = new URLSearchParams({
|
|
278
|
+
secret,
|
|
279
|
+
limit: args.limit || "100"
|
|
280
|
+
});
|
|
281
|
+
const url = `${args.url}/__ai-ready/indexnow?${params}`;
|
|
282
|
+
consola.info(`Triggering IndexNow sync at ${args.url}...`);
|
|
283
|
+
const res = await fetch(url, { method: "POST" }).then((r) => r.json()).catch((err) => {
|
|
284
|
+
consola.error(`Failed: ${err.message}`);
|
|
285
|
+
return null;
|
|
286
|
+
});
|
|
287
|
+
if (!res)
|
|
288
|
+
return;
|
|
289
|
+
if (res.success) {
|
|
290
|
+
consola.success(`Submitted: ${colors.green(res.submitted?.toString() || "0")} URLs`);
|
|
291
|
+
consola.info(`Remaining: ${colors.yellow(res.remaining?.toString() || "0")}`);
|
|
292
|
+
} else {
|
|
293
|
+
consola.error(`Failed: ${res.error || "Unknown error"}`);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
})
|
|
297
|
+
}
|
|
298
|
+
});
|
|
299
|
+
runMain(main);
|
package/dist/module.d.mts
CHANGED
package/dist/module.json
CHANGED
package/dist/module.mjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createHash, randomBytes } from 'node:crypto';
|
|
1
2
|
import { mkdir, writeFile, appendFile, stat, access } from 'node:fs/promises';
|
|
2
3
|
import { join, dirname } from 'node:path';
|
|
3
4
|
import { useLogger, useNuxt, addTemplate, addTypeTemplate, defineNuxtModule, createResolver, hasNuxtModule, addServerHandler, addPlugin } from '@nuxt/kit';
|
|
@@ -17,6 +18,10 @@ import { join as join$1, isAbsolute } from 'pathe';
|
|
|
17
18
|
|
|
18
19
|
const logger = useLogger("nuxt-ai-ready");
|
|
19
20
|
|
|
21
|
+
const moduleRegistrations = [];
|
|
22
|
+
function registerModule(registration) {
|
|
23
|
+
moduleRegistrations.push(registration);
|
|
24
|
+
}
|
|
20
25
|
function hookNuxtSeoProLicense() {
|
|
21
26
|
const nuxt = useNuxt();
|
|
22
27
|
const isBuild = !nuxt.options.dev && !nuxt.options._prepare;
|
|
@@ -44,7 +49,13 @@ function hookNuxtSeoProLicense() {
|
|
|
44
49
|
const siteName = siteConfig.name || void 0;
|
|
45
50
|
const res = await $fetch("https://nuxtseo.com/api/pro/verify", {
|
|
46
51
|
method: "POST",
|
|
47
|
-
body: {
|
|
52
|
+
body: {
|
|
53
|
+
apiKey: license,
|
|
54
|
+
siteUrl,
|
|
55
|
+
siteName,
|
|
56
|
+
// Include registered modules for dashboard integration
|
|
57
|
+
modules: moduleRegistrations.length > 0 ? moduleRegistrations : void 0
|
|
58
|
+
}
|
|
48
59
|
}).catch((err) => {
|
|
49
60
|
if (err?.response?.status === 401) {
|
|
50
61
|
spinner.stop("\u274C Invalid API key");
|
|
@@ -68,7 +79,7 @@ function hookNuxtSeoProLicense() {
|
|
|
68
79
|
}
|
|
69
80
|
}
|
|
70
81
|
|
|
71
|
-
async function fetchPreviousMeta(siteUrl,
|
|
82
|
+
async function fetchPreviousMeta(siteUrl, indexNow) {
|
|
72
83
|
const metaUrl = `${siteUrl}/__ai-ready/pages.meta.json`;
|
|
73
84
|
logger.debug(`[indexnow] Fetching previous meta from ${metaUrl}`);
|
|
74
85
|
const prevMeta = await fetch(metaUrl).then((r) => r.ok ? r.json() : null).catch(() => null);
|
|
@@ -76,7 +87,7 @@ async function fetchPreviousMeta(siteUrl, indexNowKey) {
|
|
|
76
87
|
logger.info("[indexnow] First deploy or no previous meta - skipping IndexNow");
|
|
77
88
|
return null;
|
|
78
89
|
}
|
|
79
|
-
const keyUrl = `${siteUrl}/${
|
|
90
|
+
const keyUrl = `${siteUrl}/${indexNow}.txt`;
|
|
80
91
|
const keyLive = await fetch(keyUrl).then((r) => r.ok).catch(() => false);
|
|
81
92
|
if (!keyLive) {
|
|
82
93
|
logger.info("[indexnow] Key file not live yet - skipping IndexNow");
|
|
@@ -84,7 +95,7 @@ async function fetchPreviousMeta(siteUrl, indexNowKey) {
|
|
|
84
95
|
}
|
|
85
96
|
return prevMeta;
|
|
86
97
|
}
|
|
87
|
-
async function handleStaticIndexNow(currentPages, siteUrl,
|
|
98
|
+
async function handleStaticIndexNow(currentPages, siteUrl, indexNow, prevMeta) {
|
|
88
99
|
const { changed, added } = comparePageHashes(currentPages, prevMeta);
|
|
89
100
|
const totalChanged = changed.length + added.length;
|
|
90
101
|
if (totalChanged === 0) {
|
|
@@ -94,7 +105,7 @@ async function handleStaticIndexNow(currentPages, siteUrl, indexNowKey, prevMeta
|
|
|
94
105
|
logger.info(`[indexnow] Submitting ${totalChanged} changed pages (${changed.length} modified, ${added.length} new)`);
|
|
95
106
|
const result = await submitToIndexNowShared(
|
|
96
107
|
[...changed, ...added],
|
|
97
|
-
|
|
108
|
+
indexNow,
|
|
98
109
|
siteUrl,
|
|
99
110
|
{ logger }
|
|
100
111
|
);
|
|
@@ -104,7 +115,7 @@ async function handleStaticIndexNow(currentPages, siteUrl, indexNowKey, prevMeta
|
|
|
104
115
|
logger.warn(`[indexnow] Failed to submit: ${result.error}`);
|
|
105
116
|
}
|
|
106
117
|
}
|
|
107
|
-
function createCrawlerState(dbPath, llmsFullTxtPath, siteInfo, llmsTxtConfig,
|
|
118
|
+
function createCrawlerState(dbPath, llmsFullTxtPath, siteInfo, llmsTxtConfig, indexNow) {
|
|
108
119
|
return {
|
|
109
120
|
prerenderedRoutes: /* @__PURE__ */ new Set(),
|
|
110
121
|
errorRoutes: /* @__PURE__ */ new Set(),
|
|
@@ -114,7 +125,7 @@ function createCrawlerState(dbPath, llmsFullTxtPath, siteInfo, llmsTxtConfig, in
|
|
|
114
125
|
llmsFullTxtPath,
|
|
115
126
|
siteInfo,
|
|
116
127
|
llmsTxtConfig,
|
|
117
|
-
|
|
128
|
+
indexNow
|
|
118
129
|
};
|
|
119
130
|
}
|
|
120
131
|
async function initCrawler(state) {
|
|
@@ -265,11 +276,11 @@ async function prerenderRoute(nitro, route) {
|
|
|
265
276
|
nitro._prerenderedRoutes.push(_route);
|
|
266
277
|
return stat(filePath);
|
|
267
278
|
}
|
|
268
|
-
function setupPrerenderHandler(dbPath, siteInfo, llmsTxtConfig,
|
|
279
|
+
function setupPrerenderHandler(dbPath, siteInfo, llmsTxtConfig, indexNow) {
|
|
269
280
|
const nuxt = useNuxt();
|
|
270
281
|
nuxt.hooks.hook("nitro:init", async (nitro) => {
|
|
271
282
|
const llmsFullTxtPath = join(nitro.options.output.publicDir, "llms-full.txt");
|
|
272
|
-
const state = createCrawlerState(dbPath, llmsFullTxtPath, siteInfo, llmsTxtConfig,
|
|
283
|
+
const state = createCrawlerState(dbPath, llmsFullTxtPath, siteInfo, llmsTxtConfig, indexNow);
|
|
273
284
|
let initPromise = null;
|
|
274
285
|
nitro.hooks.hook("prerender:generate", async (route) => {
|
|
275
286
|
if (route.error) {
|
|
@@ -334,8 +345,8 @@ function setupPrerenderHandler(dbPath, siteInfo, llmsTxtConfig, indexNowKey) {
|
|
|
334
345
|
logger.debug(`Created database dump at __ai-ready/pages.dump (${(dumpData.length / 1024).toFixed(1)}kb compressed)`);
|
|
335
346
|
const pageHashes = pages.filter((p) => p.contentHash).map((p) => ({ route: p.route, hash: p.contentHash }));
|
|
336
347
|
let prevMeta = null;
|
|
337
|
-
if (state.
|
|
338
|
-
prevMeta = await fetchPreviousMeta(state.siteInfo.url, state.
|
|
348
|
+
if (state.indexNow && state.siteInfo?.url) {
|
|
349
|
+
prevMeta = await fetchPreviousMeta(state.siteInfo.url, state.indexNow);
|
|
339
350
|
}
|
|
340
351
|
const buildId = Date.now().toString(36);
|
|
341
352
|
const metaContent = JSON.stringify({
|
|
@@ -346,8 +357,8 @@ function setupPrerenderHandler(dbPath, siteInfo, llmsTxtConfig, indexNowKey) {
|
|
|
346
357
|
});
|
|
347
358
|
await writeFile(join(publicDataDir, "pages.meta.json"), metaContent, "utf-8");
|
|
348
359
|
logger.debug(`Wrote build metadata: buildId=${buildId}, ${pageHashes.length} page hashes`);
|
|
349
|
-
if (state.
|
|
350
|
-
await handleStaticIndexNow(pageHashes, state.siteInfo.url, state.
|
|
360
|
+
if (state.indexNow && state.siteInfo?.url && prevMeta) {
|
|
361
|
+
await handleStaticIndexNow(pageHashes, state.siteInfo.url, state.indexNow, prevMeta);
|
|
351
362
|
}
|
|
352
363
|
}
|
|
353
364
|
const llmsStats = await prerenderRoute(nitro, "/llms.txt");
|
|
@@ -624,6 +635,28 @@ const module$1 = defineNuxtModule({
|
|
|
624
635
|
mergedLlmsTxt.notes = llmsTxtPayload.notes.length > 0 ? llmsTxtPayload.notes : void 0;
|
|
625
636
|
const prerenderCacheDir = join(nuxt.options.rootDir, "node_modules/.cache/nuxt-seo/ai-ready/routes");
|
|
626
637
|
const buildDbPath = join(nuxt.options.buildDir, ".data/ai-ready/build.db");
|
|
638
|
+
const runtimeSyncConfig = typeof config.runtimeSync === "object" ? config.runtimeSync : {};
|
|
639
|
+
const runtimeSyncEnabled = !!config.runtimeSync || !!config.cron;
|
|
640
|
+
const indexNow = config.indexNow === true ? createHash("sha256").update(useSiteConfig().url || "nuxt-ai-ready").digest("hex").slice(0, 32) : config.indexNow || process.env.NUXT_AI_READY_INDEX_NOW_KEY;
|
|
641
|
+
let runtimeSyncSecret = config.runtimeSyncSecret || process.env.NUXT_AI_READY_RUNTIME_SYNC_SECRET;
|
|
642
|
+
if (!runtimeSyncSecret && runtimeSyncEnabled) {
|
|
643
|
+
runtimeSyncSecret = randomBytes(32).toString("hex");
|
|
644
|
+
logger.info(`Generated runtimeSyncSecret (use NUXT_AI_READY_RUNTIME_SYNC_SECRET env to set explicitly)`);
|
|
645
|
+
}
|
|
646
|
+
if (runtimeSyncSecret) {
|
|
647
|
+
const cacheDir = join(nuxt.options.rootDir, "node_modules/.cache/nuxt/ai-ready");
|
|
648
|
+
await mkdir(cacheDir, { recursive: true });
|
|
649
|
+
await writeFile(join(cacheDir, "secret"), runtimeSyncSecret);
|
|
650
|
+
}
|
|
651
|
+
registerModule({
|
|
652
|
+
name: "nuxt-ai-ready",
|
|
653
|
+
secret: runtimeSyncSecret,
|
|
654
|
+
features: {
|
|
655
|
+
cron: !!config.cron,
|
|
656
|
+
indexNow: !!indexNow,
|
|
657
|
+
runtimeSync: runtimeSyncEnabled
|
|
658
|
+
}
|
|
659
|
+
});
|
|
627
660
|
nuxt.hooks.hook("nitro:config", (nitroConfig) => {
|
|
628
661
|
nitroConfig.experimental = nitroConfig.experimental || {};
|
|
629
662
|
nitroConfig.experimental.asyncContext = true;
|
|
@@ -636,7 +669,7 @@ const module$1 = defineNuxtModule({
|
|
|
636
669
|
nitroConfig.vercel.config.crons = nitroConfig.vercel.config.crons || [];
|
|
637
670
|
nitroConfig.vercel.config.crons.push({
|
|
638
671
|
schedule: cronSchedule,
|
|
639
|
-
path: "/__ai-ready/cron"
|
|
672
|
+
path: runtimeSyncSecret ? `/__ai-ready/cron?secret=${runtimeSyncSecret}` : "/__ai-ready/cron"
|
|
640
673
|
});
|
|
641
674
|
} else {
|
|
642
675
|
nitroConfig.experimental.tasks = true;
|
|
@@ -697,9 +730,6 @@ export async function readPageDataFromFilesystem() {
|
|
|
697
730
|
export const errorRoutes = []`;
|
|
698
731
|
});
|
|
699
732
|
const database = refineDatabaseConfig(config.database || {}, nuxt.options.rootDir);
|
|
700
|
-
const runtimeSyncConfig = typeof config.runtimeSync === "object" ? config.runtimeSync : {};
|
|
701
|
-
const runtimeSyncEnabled = !!config.runtimeSync || !!config.cron;
|
|
702
|
-
const indexNowKey = config.indexNowKey || process.env.NUXT_AI_READY_INDEX_NOW_KEY;
|
|
703
733
|
nuxt.options.runtimeConfig["nuxt-ai-ready"] = {
|
|
704
734
|
version: version || "0.0.0",
|
|
705
735
|
debug: config.debug || false,
|
|
@@ -718,8 +748,8 @@ export const errorRoutes = []`;
|
|
|
718
748
|
batchSize: runtimeSyncConfig.batchSize ?? 20,
|
|
719
749
|
pruneTtl: runtimeSyncConfig.pruneTtl ?? 0
|
|
720
750
|
},
|
|
721
|
-
runtimeSyncSecret
|
|
722
|
-
|
|
751
|
+
runtimeSyncSecret,
|
|
752
|
+
indexNow
|
|
723
753
|
};
|
|
724
754
|
addServerHandler({
|
|
725
755
|
middleware: true,
|
|
@@ -746,8 +776,8 @@ export const errorRoutes = []`;
|
|
|
746
776
|
addServerHandler({ route: "/__ai-ready/prune", method: "post", handler: resolve("./runtime/server/routes/__ai-ready/prune.post") });
|
|
747
777
|
addServerHandler({ route: "/__ai-ready/restore", method: "post", handler: resolve("./runtime/server/routes/__ai-ready/restore.post") });
|
|
748
778
|
}
|
|
749
|
-
if (
|
|
750
|
-
addServerHandler({ route: `/${
|
|
779
|
+
if (indexNow) {
|
|
780
|
+
addServerHandler({ route: `/${indexNow}.txt`, handler: resolve("./runtime/server/routes/indexnow-key.get") });
|
|
751
781
|
addServerHandler({ route: "/__ai-ready/indexnow", method: "post", handler: resolve("./runtime/server/routes/__ai-ready/indexnow.post") });
|
|
752
782
|
if (!runtimeSyncEnabled) {
|
|
753
783
|
addServerHandler({ route: "/__ai-ready/status", handler: resolve("./runtime/server/routes/__ai-ready/status.get") });
|
|
@@ -773,7 +803,7 @@ export const errorRoutes = []`;
|
|
|
773
803
|
name: siteConfig.name,
|
|
774
804
|
url: siteConfig.url,
|
|
775
805
|
description: siteConfig.description
|
|
776
|
-
}, mergedLlmsTxt,
|
|
806
|
+
}, mergedLlmsTxt, indexNow);
|
|
777
807
|
}
|
|
778
808
|
nuxt.options.nitro.routeRules = nuxt.options.nitro.routeRules || {};
|
|
779
809
|
nuxt.options.nitro.routeRules["/llms.txt"] = { headers: { "Content-Type": "text/plain; charset=utf-8" } };
|
|
@@ -1,3 +1,13 @@
|
|
|
1
|
-
import { eventHandler } from "h3";
|
|
1
|
+
import { createError, eventHandler, getQuery } from "h3";
|
|
2
|
+
import { useRuntimeConfig } from "nitropack/runtime";
|
|
2
3
|
import { runCron } from "../../utils/runCron.js";
|
|
3
|
-
export default eventHandler((event) =>
|
|
4
|
+
export default eventHandler((event) => {
|
|
5
|
+
const config = useRuntimeConfig(event)["nuxt-ai-ready"];
|
|
6
|
+
if (config.runtimeSyncSecret) {
|
|
7
|
+
const { secret } = getQuery(event);
|
|
8
|
+
if (secret !== config.runtimeSyncSecret) {
|
|
9
|
+
throw createError({ statusCode: 401, message: "Unauthorized" });
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
return runCron(event);
|
|
13
|
+
});
|
|
@@ -3,7 +3,7 @@ import { useRuntimeConfig } from "nitropack/runtime";
|
|
|
3
3
|
import { syncToIndexNow } from "../../utils/indexnow.js";
|
|
4
4
|
export default eventHandler(async (event) => {
|
|
5
5
|
const config = useRuntimeConfig(event)["nuxt-ai-ready"];
|
|
6
|
-
if (!config.
|
|
6
|
+
if (!config.indexNow) {
|
|
7
7
|
throw createError({ statusCode: 400, message: "IndexNow not configured" });
|
|
8
8
|
}
|
|
9
9
|
const query = getQuery(event);
|
|
@@ -1,8 +1,14 @@
|
|
|
1
|
-
import { eventHandler } from "h3";
|
|
1
|
+
import { createError, eventHandler, getQuery } from "h3";
|
|
2
2
|
import { useRuntimeConfig } from "nitropack/runtime";
|
|
3
3
|
import { countPages, countPagesNeedingIndexNowSync, getIndexNowStats } from "../../db/queries.js";
|
|
4
4
|
export default eventHandler(async (event) => {
|
|
5
5
|
const config = useRuntimeConfig(event)["nuxt-ai-ready"];
|
|
6
|
+
if (config.runtimeSyncSecret) {
|
|
7
|
+
const { secret } = getQuery(event);
|
|
8
|
+
if (secret !== config.runtimeSyncSecret) {
|
|
9
|
+
throw createError({ statusCode: 401, message: "Unauthorized" });
|
|
10
|
+
}
|
|
11
|
+
}
|
|
6
12
|
const [total, pending] = await Promise.all([
|
|
7
13
|
countPages(event),
|
|
8
14
|
countPages(event, { where: { pending: true } })
|
|
@@ -12,7 +18,7 @@ export default eventHandler(async (event) => {
|
|
|
12
18
|
indexed: total - pending,
|
|
13
19
|
pending
|
|
14
20
|
};
|
|
15
|
-
if (config.
|
|
21
|
+
if (config.indexNow) {
|
|
16
22
|
const [indexNowPending, indexNowStats] = await Promise.all([
|
|
17
23
|
countPagesNeedingIndexNowSync(event),
|
|
18
24
|
getIndexNowStats(event)
|
|
@@ -135,7 +135,7 @@ export default eventHandler(async (event) => {
|
|
|
135
135
|
indexNowRemaining: run.indexNowRemaining,
|
|
136
136
|
errors: run.errors
|
|
137
137
|
}));
|
|
138
|
-
if (runtimeConfig.
|
|
138
|
+
if (runtimeConfig.indexNow) {
|
|
139
139
|
const [indexNowPending, indexNowStats, indexNowLogEntries] = await Promise.all([
|
|
140
140
|
countPagesNeedingIndexNowSync(event),
|
|
141
141
|
getIndexNowStats(event),
|
|
@@ -2,8 +2,8 @@ import { eventHandler, setHeader } from "h3";
|
|
|
2
2
|
import { useRuntimeConfig } from "nitropack/runtime";
|
|
3
3
|
export default eventHandler((event) => {
|
|
4
4
|
const config = useRuntimeConfig(event)["nuxt-ai-ready"];
|
|
5
|
-
if (!config.
|
|
5
|
+
if (!config.indexNow)
|
|
6
6
|
return null;
|
|
7
7
|
setHeader(event, "Content-Type", "text/plain; charset=utf-8");
|
|
8
|
-
return config.
|
|
8
|
+
return config.indexNow;
|
|
9
9
|
});
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { useSiteConfig } from "#site-config/server/composables/useSiteConfig";
|
|
2
|
+
import { useEvent, useNitroApp, useRuntimeConfig } from "nitropack/runtime";
|
|
2
3
|
import { getPageHash, isPageFresh, queryPages, upsertPage } from "../db/queries.js";
|
|
3
4
|
import { computeContentHash } from "../db/shared.js";
|
|
4
5
|
import { logger } from "../logger.js";
|
|
@@ -15,7 +16,9 @@ export async function indexPage(route, html, options = {}, event) {
|
|
|
15
16
|
const existing = await queryPages(event, { route });
|
|
16
17
|
const isUpdate = !!existing;
|
|
17
18
|
const isError = html.includes("__NUXT_ERROR__") || html.includes("nuxt-error-page");
|
|
18
|
-
const
|
|
19
|
+
const siteConfig = useSiteConfig(event || useEvent());
|
|
20
|
+
const fullUrl = `${siteConfig.url}${route}`;
|
|
21
|
+
const result = await convertHtmlToMarkdown(html, fullUrl, config.mdreamOptions, { extractUpdatedAt: true });
|
|
19
22
|
const updatedAt = result.updatedAt || (/* @__PURE__ */ new Date()).toISOString();
|
|
20
23
|
const headings = JSON.stringify(result.headings);
|
|
21
24
|
const keywords = extractKeywords(result.textContent, result.metaKeywords);
|
|
@@ -53,7 +53,7 @@ export async function submitToIndexNow(routes, config, siteUrl) {
|
|
|
53
53
|
export async function syncToIndexNow(event, limit = 100, options) {
|
|
54
54
|
const config = useRuntimeConfig(event)["nuxt-ai-ready"];
|
|
55
55
|
const siteConfig = getSiteConfig(event);
|
|
56
|
-
if (!config.
|
|
56
|
+
if (!config.indexNow) {
|
|
57
57
|
return { success: false, submitted: 0, remaining: 0, error: "IndexNow not configured" };
|
|
58
58
|
}
|
|
59
59
|
if (!siteConfig.url) {
|
|
@@ -80,7 +80,7 @@ export async function syncToIndexNow(event, limit = 100, options) {
|
|
|
80
80
|
return { success: true, submitted: 0, remaining: 0 };
|
|
81
81
|
}
|
|
82
82
|
const routes = pages.map((p) => p.route);
|
|
83
|
-
const result = await submitToIndexNow(routes, { key: config.
|
|
83
|
+
const result = await submitToIndexNow(routes, { key: config.indexNow }, siteConfig.url);
|
|
84
84
|
const dbUpdates = async () => {
|
|
85
85
|
if (config.debug) {
|
|
86
86
|
await logIndexNowSubmission(event, routes.length, result.success, result.error);
|
|
@@ -28,7 +28,7 @@ export async function runCron(providedEvent, options) {
|
|
|
28
28
|
const results = {};
|
|
29
29
|
const allErrors = [];
|
|
30
30
|
if (debug) {
|
|
31
|
-
logger.info(`[cron] Starting cron run (batchSize: ${options?.batchSize ?? config.runtimeSync.batchSize}, indexNow: ${!!config.
|
|
31
|
+
logger.info(`[cron] Starting cron run (batchSize: ${options?.batchSize ?? config.runtimeSync.batchSize}, indexNow: ${!!config.indexNow})`);
|
|
32
32
|
}
|
|
33
33
|
if (config.runtimeSync.enabled) {
|
|
34
34
|
results.stale = await checkAndHandleStale(event).catch((err) => {
|
|
@@ -72,7 +72,7 @@ export async function runCron(providedEvent, options) {
|
|
|
72
72
|
logger.info(`[cron] Index: ${indexResult.indexed} pages (${indexResult.remaining} remaining${indexResult.errors.length > 0 ? `, ${indexResult.errors.length} errors` : ""})`);
|
|
73
73
|
}
|
|
74
74
|
}
|
|
75
|
-
if (config.
|
|
75
|
+
if (config.indexNow) {
|
|
76
76
|
const indexNowResult = await syncToIndexNow(event, 100).catch((err) => {
|
|
77
77
|
console.warn("[ai-ready:cron] IndexNow sync failed:", err.message);
|
|
78
78
|
return { success: false, submitted: 0, remaining: 0, error: err.message };
|
|
@@ -35,7 +35,8 @@ function buildMdreamOptions(url, mdreamOptions, meta, extractUpdatedAt = false)
|
|
|
35
35
|
}
|
|
36
36
|
}
|
|
37
37
|
});
|
|
38
|
-
|
|
38
|
+
const origin = new URL(url).origin;
|
|
39
|
+
let options = { origin, ...mdreamOptions };
|
|
39
40
|
if (mdreamOptions?.preset === "minimal") {
|
|
40
41
|
options = withMinimalPreset(options);
|
|
41
42
|
}
|
package/dist/runtime/types.d.ts
CHANGED
|
@@ -106,16 +106,15 @@ export interface ModuleOptions {
|
|
|
106
106
|
/**
|
|
107
107
|
* Enable scheduled cron task (runs every minute)
|
|
108
108
|
* When true, automatically enables runtimeSync for background indexing
|
|
109
|
-
* Also runs IndexNow sync if
|
|
109
|
+
* Also runs IndexNow sync if indexNow is enabled
|
|
110
110
|
*/
|
|
111
111
|
cron?: boolean;
|
|
112
112
|
/**
|
|
113
|
-
* IndexNow
|
|
114
|
-
*
|
|
115
|
-
*
|
|
116
|
-
* Can also be set via NUXT_AI_READY_INDEX_NOW_KEY env var
|
|
113
|
+
* Enable IndexNow for instant search engine notifications
|
|
114
|
+
* Submits to Bing, Yandex, Naver, Seznam when content changes
|
|
115
|
+
* Set to `true` to derive key from site URL, or provide your own string
|
|
117
116
|
*/
|
|
118
|
-
|
|
117
|
+
indexNow?: boolean | string;
|
|
119
118
|
/**
|
|
120
119
|
* Secret token for authenticating runtime sync endpoints
|
|
121
120
|
* When set, requires ?secret=<token> query param for poll/prune/indexnow endpoints
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nuxt-ai-ready",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.9.0",
|
|
5
5
|
"description": "Best practice AI & LLM discoverability for Nuxt sites.",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "Harlan Wilton",
|
|
@@ -32,6 +32,9 @@
|
|
|
32
32
|
}
|
|
33
33
|
},
|
|
34
34
|
"main": "./dist/module.mjs",
|
|
35
|
+
"bin": {
|
|
36
|
+
"nuxt-ai-ready": "./dist/cli.mjs"
|
|
37
|
+
},
|
|
35
38
|
"files": [
|
|
36
39
|
"dist",
|
|
37
40
|
"mcp.d.ts"
|
|
@@ -48,6 +51,7 @@
|
|
|
48
51
|
"dependencies": {
|
|
49
52
|
"@clack/prompts": "^0.11.0",
|
|
50
53
|
"@nuxt/kit": "^4.2.2",
|
|
54
|
+
"citty": "^0.1.6",
|
|
51
55
|
"consola": "^3.4.2",
|
|
52
56
|
"db0": "^0.3.4",
|
|
53
57
|
"defu": "^6.1.4",
|
|
@@ -78,12 +82,12 @@
|
|
|
78
82
|
"@vue/test-utils": "^2.4.6",
|
|
79
83
|
"@vueuse/nuxt": "^14.1.0",
|
|
80
84
|
"agents": "^0.3.5",
|
|
81
|
-
"ai": "^6.0.
|
|
85
|
+
"ai": "^6.0.38",
|
|
82
86
|
"better-sqlite3": "^12.6.0",
|
|
83
87
|
"bumpp": "^10.4.0",
|
|
84
88
|
"eslint": "^9.39.2",
|
|
85
89
|
"execa": "^9.6.1",
|
|
86
|
-
"happy-dom": "^20.3.
|
|
90
|
+
"happy-dom": "^20.3.1",
|
|
87
91
|
"nuxt": "^4.2.2",
|
|
88
92
|
"nuxt-site-config": "^3.2.17",
|
|
89
93
|
"playwright": "^1.57.0",
|
|
@@ -94,7 +98,7 @@
|
|
|
94
98
|
"vue": "^3.5.26",
|
|
95
99
|
"vue-router": "^4.6.4",
|
|
96
100
|
"vue-tsc": "^3.2.2",
|
|
97
|
-
"wrangler": "^4.59.
|
|
101
|
+
"wrangler": "^4.59.2",
|
|
98
102
|
"zod": "^4.3.5"
|
|
99
103
|
},
|
|
100
104
|
"resolutions": {
|