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 CHANGED
@@ -4,7 +4,7 @@
4
4
  "nuxt": ">=4.0.0"
5
5
  },
6
6
  "configKey": "aiReady",
7
- "version": "0.8.0",
7
+ "version": "0.8.1",
8
8
  "builder": {
9
9
  "@nuxt/module-builder": "1.0.2",
10
10
  "unbuild": "3.6.1"
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 submitToIndexNow(
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 '../../../shared/indexnow.js';
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);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "nuxt-ai-ready",
3
3
  "type": "module",
4
- "version": "0.8.0",
4
+ "version": "0.8.1",
5
5
  "description": "Best practice AI & LLM discoverability for Nuxt sites.",
6
6
  "author": {
7
7
  "name": "Harlan Wilton",