nuxt-ai-ready 0.8.0 → 0.8.1
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/module.json +1 -1
- package/dist/module.mjs +2 -79
- package/dist/runtime/server/utils/indexnow-shared.d.ts +57 -0
- package/dist/runtime/server/utils/indexnow-shared.js +77 -0
- package/dist/runtime/server/utils/indexnow.d.ts +1 -1
- package/dist/runtime/server/utils/indexnow.js +1 -1
- package/package.json +1 -1
package/dist/module.json
CHANGED
package/dist/module.mjs
CHANGED
|
@@ -11,6 +11,7 @@ import { parseSitemapXml } from '@nuxtjs/sitemap/utils';
|
|
|
11
11
|
import { colorize } from 'consola/utils';
|
|
12
12
|
import { withBase } from 'ufo';
|
|
13
13
|
import { createAdapter, initSchema, computeContentHash, insertPage, queryAllPages, exportDbDump } from '../dist/runtime/server/db/shared.js';
|
|
14
|
+
import { comparePageHashes, submitToIndexNowShared } from '../dist/runtime/server/utils/indexnow-shared.js';
|
|
14
15
|
import { buildLlmsFullTxtHeader, formatPageForLlmsFullTxt } from '../dist/runtime/server/utils/llms-full.js';
|
|
15
16
|
import { join as join$1, isAbsolute } from 'pathe';
|
|
16
17
|
|
|
@@ -67,84 +68,6 @@ function hookNuxtSeoProLicense() {
|
|
|
67
68
|
}
|
|
68
69
|
}
|
|
69
70
|
|
|
70
|
-
const INDEXNOW_HOSTS = ["api.indexnow.org", "www.bing.com"];
|
|
71
|
-
function getIndexNowEndpoints() {
|
|
72
|
-
const testEndpoint = process.env.INDEXNOW_TEST_ENDPOINT;
|
|
73
|
-
if (testEndpoint) {
|
|
74
|
-
return [testEndpoint];
|
|
75
|
-
}
|
|
76
|
-
return INDEXNOW_HOSTS.map((host) => `https://${host}/indexnow`);
|
|
77
|
-
}
|
|
78
|
-
function buildIndexNowBody(routes, key, siteUrl) {
|
|
79
|
-
const urlList = routes.map(
|
|
80
|
-
(route) => route.startsWith("http") ? route : `${siteUrl}${route}`
|
|
81
|
-
);
|
|
82
|
-
return {
|
|
83
|
-
host: new URL(siteUrl).host,
|
|
84
|
-
key,
|
|
85
|
-
keyLocation: `${siteUrl}/${key}.txt`,
|
|
86
|
-
urlList
|
|
87
|
-
};
|
|
88
|
-
}
|
|
89
|
-
async function submitToIndexNow(routes, key, siteUrl, options) {
|
|
90
|
-
if (!siteUrl) {
|
|
91
|
-
return { success: false, error: "Site URL not configured" };
|
|
92
|
-
}
|
|
93
|
-
const fetchFn = options?.fetchFn ?? globalThis.fetch;
|
|
94
|
-
const log = options?.logger;
|
|
95
|
-
const body = buildIndexNowBody(routes, key, siteUrl);
|
|
96
|
-
const endpoints = getIndexNowEndpoints();
|
|
97
|
-
let lastError;
|
|
98
|
-
for (const endpoint of endpoints) {
|
|
99
|
-
log?.debug(`[indexnow] Submitting ${body.urlList.length} URLs to ${endpoint}`);
|
|
100
|
-
const response = await fetchFn(endpoint, {
|
|
101
|
-
method: "POST",
|
|
102
|
-
headers: { "Content-Type": "application/json" },
|
|
103
|
-
body: JSON.stringify(body)
|
|
104
|
-
}).then((r) => r.ok ? { ok: true } : { error: `HTTP ${r.status}` }).catch((err) => ({ error: err.message }));
|
|
105
|
-
if ("error" in response) {
|
|
106
|
-
lastError = response.error;
|
|
107
|
-
if (lastError?.includes("429")) {
|
|
108
|
-
log?.warn(`[indexnow] Rate limited on ${endpoint}, trying fallback...`);
|
|
109
|
-
continue;
|
|
110
|
-
}
|
|
111
|
-
log?.warn(`[indexnow] Submission failed on ${endpoint}: ${lastError}`);
|
|
112
|
-
return { success: false, error: lastError, host: endpoint };
|
|
113
|
-
}
|
|
114
|
-
log?.debug(`[indexnow] Successfully submitted ${body.urlList.length} URLs via ${endpoint}`);
|
|
115
|
-
return { success: true, host: endpoint };
|
|
116
|
-
}
|
|
117
|
-
return {
|
|
118
|
-
success: false,
|
|
119
|
-
error: lastError || "All endpoints rate limited",
|
|
120
|
-
host: endpoints[endpoints.length - 1]
|
|
121
|
-
};
|
|
122
|
-
}
|
|
123
|
-
function comparePageHashes(currentPages, prevMeta) {
|
|
124
|
-
if (!prevMeta?.pages) {
|
|
125
|
-
return { changed: [], added: [], removed: [] };
|
|
126
|
-
}
|
|
127
|
-
const prevHashes = new Map(prevMeta.pages.map((p) => [p.route, p.hash]));
|
|
128
|
-
const currentRoutes = new Set(currentPages.map((p) => p.route));
|
|
129
|
-
const changed = [];
|
|
130
|
-
const added = [];
|
|
131
|
-
for (const page of currentPages) {
|
|
132
|
-
const prevHash = prevHashes.get(page.route);
|
|
133
|
-
if (!prevHash) {
|
|
134
|
-
added.push(page.route);
|
|
135
|
-
} else if (prevHash !== page.hash) {
|
|
136
|
-
changed.push(page.route);
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
const removed = [];
|
|
140
|
-
for (const route of prevHashes.keys()) {
|
|
141
|
-
if (!currentRoutes.has(route)) {
|
|
142
|
-
removed.push(route);
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
return { changed, added, removed };
|
|
146
|
-
}
|
|
147
|
-
|
|
148
71
|
async function fetchPreviousMeta(siteUrl, indexNowKey) {
|
|
149
72
|
const metaUrl = `${siteUrl}/__ai-ready/pages.meta.json`;
|
|
150
73
|
logger.debug(`[indexnow] Fetching previous meta from ${metaUrl}`);
|
|
@@ -169,7 +92,7 @@ async function handleStaticIndexNow(currentPages, siteUrl, indexNowKey, prevMeta
|
|
|
169
92
|
return;
|
|
170
93
|
}
|
|
171
94
|
logger.info(`[indexnow] Submitting ${totalChanged} changed pages (${changed.length} modified, ${added.length} new)`);
|
|
172
|
-
const result = await
|
|
95
|
+
const result = await submitToIndexNowShared(
|
|
173
96
|
[...changed, ...added],
|
|
174
97
|
indexNowKey,
|
|
175
98
|
siteUrl,
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared IndexNow utilities for build-time and runtime
|
|
3
|
+
* This module has no Nuxt/Nitro dependencies so it can be used in both contexts
|
|
4
|
+
*/
|
|
5
|
+
export declare const INDEXNOW_HOSTS: string[];
|
|
6
|
+
/**
|
|
7
|
+
* Get IndexNow endpoints, with test override support
|
|
8
|
+
*/
|
|
9
|
+
export declare function getIndexNowEndpoints(): string[];
|
|
10
|
+
export interface IndexNowSubmitResult {
|
|
11
|
+
success: boolean;
|
|
12
|
+
error?: string;
|
|
13
|
+
host?: string;
|
|
14
|
+
}
|
|
15
|
+
export interface IndexNowRequestBody {
|
|
16
|
+
host: string;
|
|
17
|
+
key: string;
|
|
18
|
+
keyLocation: string;
|
|
19
|
+
urlList: string[];
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Build the IndexNow API request body
|
|
23
|
+
*/
|
|
24
|
+
export declare function buildIndexNowBody(routes: string[], key: string, siteUrl: string): IndexNowRequestBody;
|
|
25
|
+
export interface SubmitOptions {
|
|
26
|
+
/** Custom fetch implementation (defaults to globalThis.fetch) */
|
|
27
|
+
fetchFn?: typeof fetch;
|
|
28
|
+
/** Logger for debug/warn messages (optional) */
|
|
29
|
+
logger?: {
|
|
30
|
+
debug: (msg: string) => void;
|
|
31
|
+
warn: (msg: string) => void;
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Submit URLs to IndexNow API with fallback on rate limit
|
|
36
|
+
* Works in both build-time (native fetch) and runtime ($fetch) contexts
|
|
37
|
+
*/
|
|
38
|
+
export declare function submitToIndexNowShared(routes: string[], key: string, siteUrl: string, options?: SubmitOptions): Promise<IndexNowSubmitResult>;
|
|
39
|
+
export interface PageHashMeta {
|
|
40
|
+
route: string;
|
|
41
|
+
hash: string;
|
|
42
|
+
}
|
|
43
|
+
export interface BuildMeta {
|
|
44
|
+
buildId: string;
|
|
45
|
+
pageCount: number;
|
|
46
|
+
createdAt: string;
|
|
47
|
+
pages: PageHashMeta[];
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Compare page hashes between current and previous builds
|
|
51
|
+
* Returns changed, added, and removed routes
|
|
52
|
+
*/
|
|
53
|
+
export declare function comparePageHashes(currentPages: PageHashMeta[], prevMeta: BuildMeta | null | undefined): {
|
|
54
|
+
changed: string[];
|
|
55
|
+
added: string[];
|
|
56
|
+
removed: string[];
|
|
57
|
+
};
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
export const INDEXNOW_HOSTS = ["api.indexnow.org", "www.bing.com"];
|
|
2
|
+
export function getIndexNowEndpoints() {
|
|
3
|
+
const testEndpoint = process.env.INDEXNOW_TEST_ENDPOINT;
|
|
4
|
+
if (testEndpoint) {
|
|
5
|
+
return [testEndpoint];
|
|
6
|
+
}
|
|
7
|
+
return INDEXNOW_HOSTS.map((host) => `https://${host}/indexnow`);
|
|
8
|
+
}
|
|
9
|
+
export function buildIndexNowBody(routes, key, siteUrl) {
|
|
10
|
+
const urlList = routes.map(
|
|
11
|
+
(route) => route.startsWith("http") ? route : `${siteUrl}${route}`
|
|
12
|
+
);
|
|
13
|
+
return {
|
|
14
|
+
host: new URL(siteUrl).host,
|
|
15
|
+
key,
|
|
16
|
+
keyLocation: `${siteUrl}/${key}.txt`,
|
|
17
|
+
urlList
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
export async function submitToIndexNowShared(routes, key, siteUrl, options) {
|
|
21
|
+
if (!siteUrl) {
|
|
22
|
+
return { success: false, error: "Site URL not configured" };
|
|
23
|
+
}
|
|
24
|
+
const fetchFn = options?.fetchFn ?? globalThis.fetch;
|
|
25
|
+
const log = options?.logger;
|
|
26
|
+
const body = buildIndexNowBody(routes, key, siteUrl);
|
|
27
|
+
const endpoints = getIndexNowEndpoints();
|
|
28
|
+
let lastError;
|
|
29
|
+
for (const endpoint of endpoints) {
|
|
30
|
+
log?.debug(`[indexnow] Submitting ${body.urlList.length} URLs to ${endpoint}`);
|
|
31
|
+
const response = await fetchFn(endpoint, {
|
|
32
|
+
method: "POST",
|
|
33
|
+
headers: { "Content-Type": "application/json" },
|
|
34
|
+
body: JSON.stringify(body)
|
|
35
|
+
}).then((r) => r.ok ? { ok: true } : { error: `HTTP ${r.status}` }).catch((err) => ({ error: err.message }));
|
|
36
|
+
if ("error" in response) {
|
|
37
|
+
lastError = response.error;
|
|
38
|
+
if (lastError?.includes("429")) {
|
|
39
|
+
log?.warn(`[indexnow] Rate limited on ${endpoint}, trying fallback...`);
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
log?.warn(`[indexnow] Submission failed on ${endpoint}: ${lastError}`);
|
|
43
|
+
return { success: false, error: lastError, host: endpoint };
|
|
44
|
+
}
|
|
45
|
+
log?.debug(`[indexnow] Successfully submitted ${body.urlList.length} URLs via ${endpoint}`);
|
|
46
|
+
return { success: true, host: endpoint };
|
|
47
|
+
}
|
|
48
|
+
return {
|
|
49
|
+
success: false,
|
|
50
|
+
error: lastError || "All endpoints rate limited",
|
|
51
|
+
host: endpoints[endpoints.length - 1]
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
export function comparePageHashes(currentPages, prevMeta) {
|
|
55
|
+
if (!prevMeta?.pages) {
|
|
56
|
+
return { changed: [], added: [], removed: [] };
|
|
57
|
+
}
|
|
58
|
+
const prevHashes = new Map(prevMeta.pages.map((p) => [p.route, p.hash]));
|
|
59
|
+
const currentRoutes = new Set(currentPages.map((p) => p.route));
|
|
60
|
+
const changed = [];
|
|
61
|
+
const added = [];
|
|
62
|
+
for (const page of currentPages) {
|
|
63
|
+
const prevHash = prevHashes.get(page.route);
|
|
64
|
+
if (!prevHash) {
|
|
65
|
+
added.push(page.route);
|
|
66
|
+
} else if (prevHash !== page.hash) {
|
|
67
|
+
changed.push(page.route);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
const removed = [];
|
|
71
|
+
for (const route of prevHashes.keys()) {
|
|
72
|
+
if (!currentRoutes.has(route)) {
|
|
73
|
+
removed.push(route);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return { changed, added, removed };
|
|
77
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { H3Event } from 'h3';
|
|
2
|
-
export type { BuildMeta, IndexNowSubmitResult, PageHashMeta } from '
|
|
2
|
+
export type { BuildMeta, IndexNowSubmitResult, PageHashMeta } from './indexnow-shared.js';
|
|
3
3
|
export interface IndexNowResult {
|
|
4
4
|
success: boolean;
|
|
5
5
|
submitted: number;
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { getSiteConfig } from "#site-config/server/composables";
|
|
2
2
|
import { useRuntimeConfig } from "nitropack/runtime";
|
|
3
|
-
import { submitToIndexNow as submitToIndexNowShared } from "../../../shared/indexnow";
|
|
4
3
|
import { useDatabase } from "../db/index.js";
|
|
5
4
|
import {
|
|
6
5
|
batchIndexNowUpdate,
|
|
@@ -10,6 +9,7 @@ import {
|
|
|
10
9
|
updateIndexNowStats
|
|
11
10
|
} from "../db/queries.js";
|
|
12
11
|
import { logger } from "../logger.js";
|
|
12
|
+
import { submitToIndexNowShared } from "./indexnow-shared.js";
|
|
13
13
|
const BACKOFF_MINUTES = [5, 10, 20, 40, 60];
|
|
14
14
|
async function getBackoffInfo(event) {
|
|
15
15
|
const db = await useDatabase(event).catch(() => null);
|