astro-indexnow 1.0.0 → 2.0.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/README.md CHANGED
@@ -1,37 +1,36 @@
1
1
  # astro-indexnow
2
2
 
3
- An Astro integration that automatically submits **all built pages** to **IndexNow**
4
- after each build, helping search engines discover updates faster.
3
+ A **production‑grade Astro integration** that automatically submits **only new or changed pages**
4
+ to **IndexNow** after each build.
5
5
 
6
- Designed to follow Astro’s official integration patterns (like `@astrojs/sitemap`):
6
+ This package is designed for **modern CI/CD pipelines**, **Docker-based deployments**, and
7
+ **large static sites**, while remaining fully deterministic and explicit.
7
8
 
8
- - No interactive prompts
9
- - No automatic file mutation
10
- - Explicit, config‑driven usage
11
- - CI‑safe and deterministic
12
- - Zero runtime impact
9
+ > **Version:** v2.0.0
10
+ > **Stateful by design. Changed-only submissions. Batch-safe. CI-aware.**
13
11
 
14
12
  ---
15
13
 
16
14
  ## What is IndexNow?
17
15
 
18
- [IndexNow](https://www.indexnow.org/) is a protocol supported by search engines
19
- such as **Bing** and **Yandex** that allows websites to proactively notify them
20
- when URLs are added or updated.
16
+ [IndexNow](https://www.indexnow.org/) is a protocol supported by search engines such as **Bing**
17
+ and **Yandex** that allows websites to proactively notify search engines when URLs are **added,
18
+ updated, or removed**.
21
19
 
22
- Instead of waiting for crawlers, your site directly tells search engines
23
- which URLs should be re‑crawled.
20
+ Instead of waiting for crawlers, your site tells search engines exactly which URLs changed.
24
21
 
25
22
  ---
26
23
 
27
- ## Features
24
+ ## Key Features
28
25
 
29
26
  - 🚀 Submits URLs to IndexNow **at build time**
30
- - 🔁 Submits **all generated pages** every build (simple & reliable)
31
- - 🔒 No secrets prompted or stored automatically
32
- - 🧠 Uses Astro’s `site` config as the canonical base URL
33
- - 🧪 Safe for CI/CD pipelines
34
- - 🧩 No client‑side or runtime code added
27
+ - 🔍 **Detects changed pages automatically** using HTML hashing
28
+ - 🧠 Stores a small on-disk cache to avoid re-submitting unchanged URLs
29
+ - 📦 **Batches submissions** safely (IndexNow 10,000 URL limit)
30
+ - 🧪 Fully **CI/CD and Docker safe**
31
+ - 🔒 No secrets prompted, stored, or mutated
32
+ - 🧩 No client-side or runtime code added
33
+ - 🧼 Quiet by default, verbose when Astro logging is enabled
35
34
 
36
35
  ---
37
36
 
@@ -49,7 +48,7 @@ npx astro add astro-indexnow
49
48
 
50
49
  > **Note**
51
50
  > Like other Astro integrations, `astro add` installs the package but does **not**
52
- > inject configuration values. You must configure it manually (see below).
51
+ > inject configuration values. Configuration is always explicit.
53
52
 
54
53
  ---
55
54
 
@@ -60,8 +59,8 @@ Generate an IndexNow API key here:
60
59
  👉 https://www.bing.com/indexnow/getstarted
61
60
 
62
61
  You will receive:
63
- - an **API key** (a long string)
64
- - instructions to create a **key file**
62
+ - an **API key**
63
+ - instructions to create a **key verification file**
65
64
 
66
65
  ---
67
66
 
@@ -79,7 +78,7 @@ If your key is:
79
78
  abcd1234
80
79
  ```
81
80
 
82
- Create this file:
81
+ Create:
83
82
 
84
83
  ```
85
84
  public/abcd1234.txt
@@ -91,139 +90,169 @@ With **exactly this content**:
91
90
  abcd1234
92
91
  ```
93
92
 
94
- Astro will automatically copy this file to the build output.
93
+ Astro will automatically copy this file into the build output.
95
94
 
96
- > ⚠️ The plugin does **not** create this file for you.
95
+ > ⚠️ The integration does **not** create this file for you.
97
96
 
98
97
  ---
99
98
 
100
99
  ## Step 3 — Configure Astro
101
100
 
102
- Add **both** your site URL and the IndexNow integration
103
- to `astro.config.mjs`.
104
-
105
- ### Example
101
+ Add both your site URL and the integration to `astro.config.mjs`.
106
102
 
107
103
  ```js
108
- // @ts-check
104
+ // astro.config.mjs
109
105
  import { defineConfig } from "astro/config";
110
106
  import indexnow from "astro-indexnow";
111
107
 
112
108
  export default defineConfig({
113
109
  site: "https://example.com",
114
-
115
110
  integrations: [
116
111
  indexnow({
117
- key: "YOUR_INDEXNOW_KEY",
112
+ key: process.env.INDEXNOW_KEY,
118
113
  }),
119
114
  ],
120
115
  });
121
116
  ```
122
117
 
123
- ### Using environment variables (recommended)
118
+ Using environment variables is **strongly recommended** for CI/CD.
124
119
 
125
- ```js
126
- indexnow({
127
- key: process.env.INDEXNOW_KEY,
128
- });
129
- ```
120
+ ---
121
+
122
+ ## How It Works
123
+
124
+ On every `astro build`, the integration:
125
+
126
+ 1. Walks the final HTML output directory
127
+ 2. Hashes each generated `index.html`
128
+ 3. Compares hashes against a stored cache
129
+ 4. Detects which URLs are **new or changed**
130
+ 5. Submits **only those URLs** to IndexNow
131
+ 6. Updates the cache after a successful build
130
132
 
131
- This keeps secrets out of version control and works cleanly in CI.
133
+ This ensures:
134
+ - No duplicate submissions
135
+ - No unnecessary API calls
136
+ - Accurate change detection based on real output
132
137
 
133
138
  ---
134
139
 
135
- ## Configuration Options
140
+ ## State & Cache File (Important)
136
141
 
137
- ### `key` (required)
142
+ To detect changes between builds, the integration stores a small cache file:
138
143
 
139
- Type: `string`
144
+ ```
145
+ .astro-indexnow-cache.json
146
+ ```
140
147
 
141
- Your IndexNow API key.
148
+ This file:
149
+ - Is created automatically
150
+ - Contains only URL → hash mappings
151
+ - Contains **no secrets**
152
+ - Is safe to inspect, commit, or delete
153
+
154
+ Deleting the file simply causes the next build to treat all pages as new.
142
155
 
143
156
  ---
144
157
 
145
- ### `site` (required)
158
+ ## CI/CD & Git Deployments (Very Important)
146
159
 
147
- Type: `string`
160
+ ### If you deploy via Git (CI/CD):
148
161
 
149
- Set via Astro’s config:
162
+ You **must commit** `.astro-indexnow-cache.json` to your repository.
150
163
 
151
- ```js
152
- defineConfig({
153
- site: "https://example.com"
154
- })
164
+ Why:
165
+ - CI environments are ephemeral
166
+ - Without the cache, every build looks like a first build
167
+ - All URLs would be re-submitted
168
+
169
+ **Recommended:**
170
+
171
+ ```gitignore
172
+ # DO NOT ignore this file
173
+ !.astro-indexnow-cache.json
155
174
  ```
156
175
 
157
- Must begin with `http://` or `https://`.
176
+ ### If you deploy on a persistent server:
177
+
178
+ If your build directory persists between builds (e.g. rsync, SSH, in-place deploy),
179
+ you **should not commit** the cache file. It will persist naturally.
158
180
 
159
181
  ---
160
182
 
161
- ### `enabled` (optional)
183
+ ## Logging Behaviour
162
184
 
163
- Type: `boolean`
164
- Default: `true`
185
+ - **Info logs**: high-level results (submitted, skipped, disabled)
186
+ - **Debug logs**: cache operations, diffs, batching
187
+ - **Warnings**: network or API failures
165
188
 
166
- Disable IndexNow submissions without removing config:
189
+ Use Astro’s verbose logging to see internal details:
167
190
 
168
- ```js
169
- indexnow({
170
- key: "...",
171
- enabled: false,
172
- });
191
+ ```bash
192
+ astro build --verbose
173
193
  ```
174
194
 
175
195
  ---
176
196
 
177
- ## How It Works
197
+ ## Configuration Options
178
198
 
179
- On every `astro build`, the integration:
199
+ ### `key` (required)
180
200
 
181
- 1. Reads Astro’s build output directory
182
- 2. Detects all generated pages
183
- 3. Converts them to public URLs
184
- 4. Submits them to IndexNow via a single POST request
201
+ Type: `string`
185
202
 
186
- No state is stored and no runtime code is added to your site.
203
+ Your IndexNow API key.
187
204
 
188
205
  ---
189
206
 
190
- ## CI / Deployment
207
+ ### `enabled` (optional)
191
208
 
192
- Designed to work cleanly in CI environments.
209
+ Type: `boolean`
210
+ Default: `true`
193
211
 
194
- Example:
212
+ Set to `false` to disable submissions entirely.
195
213
 
196
- ```bash
197
- INDEXNOW_KEY=your-key astro build
214
+ ```js
215
+ indexnow({ enabled: false })
198
216
  ```
199
217
 
200
- No prompts. No interactivity. Fully deterministic.
218
+ ---
219
+
220
+ ## IndexNow Limits & Batching
221
+
222
+ - IndexNow allows **up to 10,000 URLs per request**
223
+ - This integration automatically batches submissions
224
+ - Large sites are handled safely without configuration
201
225
 
202
226
  ---
203
227
 
204
- ## Limitations
228
+ ## Behavioural Guarantees
205
229
 
206
- - Submissions only run on `astro build`
207
- - Requires a valid `site` configuration
208
- - Requires the IndexNow key file to exist
209
- - Uninstalling the package does not remove config automatically
210
- (same behaviour as other Astro integrations)
230
+ - No runtime JavaScript added
231
+ - No mutation of Astro config
232
+ - No prompts or side effects
233
+ - No reliance on Git history
234
+ - Deterministic output-based change detection
211
235
 
212
236
  ---
213
237
 
214
238
  ## Philosophy
215
239
 
216
- This integration intentionally mirrors the behaviour of
217
- official Astro integrations such as:
240
+ This integration follows the same design principles as official Astro integrations
241
+ such as `@astrojs/sitemap`:
242
+
243
+ - Explicit configuration
244
+ - Predictable behaviour
245
+ - No magic or hidden state
246
+ - Production-first design
247
+
248
+ ---
218
249
 
219
- - `@astrojs/sitemap`
250
+ ## Maintainer
220
251
 
221
- It avoids:
222
- - automatic config mutation
223
- - secret prompting
224
- - hidden side effects
252
+ Built and maintained by **Velohost**
253
+ UK-based, privacy-first infrastructure & developer tooling.
225
254
 
226
- Everything is explicit and predictable.
255
+ https://velohost.co.uk/
227
256
 
228
257
  ---
229
258
 
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,OAAO,CAAC;AAK9C,MAAM,WAAW,eAAe;IAC9B,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED,MAAM,CAAC,OAAO,UAAU,QAAQ,CAC9B,OAAO,GAAE,eAAoB,GAC5B,gBAAgB,CAkHlB"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,OAAO,CAAC;AAK9C,MAAM,WAAW,eAAe;IAC9B,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED,MAAM,CAAC,OAAO,UAAU,QAAQ,CAC9B,OAAO,GAAE,eAAoB,GAC5B,gBAAgB,CAiMlB"}
package/dist/index.js CHANGED
@@ -1,86 +1,144 @@
1
1
  import fs from "fs";
2
2
  import path from "path";
3
- import { fileURLToPath } from "url";
3
+ import crypto from "crypto";
4
4
  export default function indexNow(options = {}) {
5
5
  let site = null;
6
+ const CACHE_FILENAME = ".astro-indexnow-cache.json";
7
+ const projectRoot = process.cwd();
8
+ const cachePath = path.join(projectRoot, CACHE_FILENAME);
9
+ const INDEXNOW_ENDPOINT = "https://api.indexnow.org/indexnow";
10
+ const INDEXNOW_BATCH_SIZE = 10_000;
11
+ /* =========================================================
12
+ Helpers
13
+ ========================================================= */
14
+ function ensureCacheFile(logger) {
15
+ const exists = fs.existsSync(cachePath);
16
+ logger.debug(`[astro-indexnow] cache exists: ${exists} (${cachePath})`);
17
+ if (!exists) {
18
+ logger.debug("[astro-indexnow] creating cache file");
19
+ fs.writeFileSync(cachePath, "{}", "utf8");
20
+ }
21
+ }
22
+ function hashFile(filePath) {
23
+ const contents = fs.readFileSync(filePath);
24
+ const hash = crypto.createHash("sha256");
25
+ hash.update(contents);
26
+ return `sha256:${hash.digest("hex")}`;
27
+ }
28
+ function loadCache(logger) {
29
+ logger.debug("[astro-indexnow] loading cache file");
30
+ try {
31
+ return JSON.parse(fs.readFileSync(cachePath, "utf8"));
32
+ }
33
+ catch {
34
+ logger.warn("[astro-indexnow] cache file unreadable, resetting");
35
+ return {};
36
+ }
37
+ }
38
+ function saveCache(logger, data) {
39
+ logger.debug("[astro-indexnow] writing cache file");
40
+ fs.writeFileSync(cachePath, JSON.stringify(data, null, 2), "utf8");
41
+ }
42
+ function chunk(array, size) {
43
+ const chunks = [];
44
+ for (let i = 0; i < array.length; i += size) {
45
+ chunks.push(array.slice(i, i + size));
46
+ }
47
+ return chunks;
48
+ }
49
+ /* =========================================================
50
+ Integration
51
+ ========================================================= */
6
52
  return {
7
53
  name: "astro-indexnow",
8
54
  hooks: {
9
- "astro:config:setup": ({ config }) => {
55
+ /* -----------------------------------------------
56
+ Setup
57
+ ----------------------------------------------- */
58
+ "astro:config:setup": ({ config, logger }) => {
10
59
  site =
11
60
  options.siteUrl ??
12
61
  (config.site ? config.site.replace(/\/$/, "") : null);
62
+ logger.debug(`[astro-indexnow] project root: ${projectRoot}`);
63
+ ensureCacheFile(logger);
13
64
  },
65
+ /* -----------------------------------------------
66
+ Build done
67
+ ----------------------------------------------- */
14
68
  "astro:build:done": async ({ dir, logger }) => {
15
69
  if (options.enabled === false) {
16
70
  logger.info("[astro-indexnow] disabled");
17
71
  return;
18
72
  }
19
73
  if (!options.key) {
20
- throw new Error("[astro-indexnow] Missing IndexNow key. Provide it in astro.config.mjs.");
74
+ throw new Error("[astro-indexnow] Missing IndexNow key");
21
75
  }
22
76
  if (!site) {
23
- throw new Error("[astro-indexnow] Missing site URL. Set `site` in astro.config.mjs or pass `siteUrl`.");
77
+ throw new Error("[astro-indexnow] Missing site URL");
24
78
  }
25
- const outDir = fileURLToPath(dir);
26
- const urls = [];
79
+ ensureCacheFile(logger);
80
+ const outDir = new URL(dir).pathname;
81
+ const previousCache = loadCache(logger);
82
+ const nextCache = {};
83
+ const changedUrls = [];
27
84
  function walk(currentDir) {
28
- const entries = fs.readdirSync(currentDir, {
29
- withFileTypes: true,
30
- });
31
- for (const entry of entries) {
85
+ for (const entry of fs.readdirSync(currentDir, { withFileTypes: true })) {
32
86
  const fullPath = path.join(currentDir, entry.name);
33
- if (entry.isDirectory()) {
87
+ if (entry.isDirectory())
34
88
  walk(fullPath);
35
- }
36
89
  if (entry.isFile() && entry.name === "index.html") {
37
90
  const relativePath = path
38
91
  .relative(outDir, fullPath)
39
92
  .replace(/index\.html$/, "")
40
93
  .replace(/\\/g, "/");
41
94
  const url = site + "/" + relativePath.replace(/^\/+/, "");
42
- urls.push(url);
95
+ const hash = hashFile(fullPath);
96
+ nextCache[url] = hash;
97
+ if (previousCache[url] !== hash) {
98
+ changedUrls.push(url);
99
+ }
43
100
  }
44
101
  }
45
102
  }
46
103
  walk(outDir);
47
- logger.info("[astro-indexnow] detected pages:");
48
- for (const url of urls) {
49
- logger.info(` - ${url}`);
104
+ logger.debug("[astro-indexnow] page diff:");
105
+ for (const url of Object.keys(nextCache)) {
106
+ const state = previousCache[url] === nextCache[url]
107
+ ? "unchanged"
108
+ : "new/changed";
109
+ logger.debug(` - ${url} (${state})`);
50
110
  }
51
- if (urls.length === 0) {
52
- logger.warn("[astro-indexnow] no pages detected, skipping submission");
111
+ if (changedUrls.length === 0) {
112
+ logger.info("[astro-indexnow] no changed URLs detected, skipping submission");
113
+ saveCache(logger, nextCache);
53
114
  return;
54
115
  }
55
- // Warn if key file is missing (do not block)
56
- const keyFilePath = path.join(outDir, `${options.key}.txt`);
57
- if (!fs.existsSync(keyFilePath)) {
58
- logger.warn(`[astro-indexnow] Key file not found: /${options.key}.txt\n` +
59
- "IndexNow may reject submissions until this file exists.");
60
- }
61
- // IndexNow submission
62
- try {
63
- const response = await fetch("https://api.indexnow.org/indexnow", {
64
- method: "POST",
65
- headers: {
66
- "Content-Type": "application/json",
67
- },
68
- body: JSON.stringify({
69
- host: new URL(site).host,
70
- key: options.key,
71
- keyLocation: `${site}/${options.key}.txt`,
72
- urlList: urls,
73
- }),
74
- });
75
- if (!response.ok) {
76
- logger.warn(`[astro-indexnow] IndexNow request failed (${response.status})`);
77
- return;
116
+ const batches = chunk(changedUrls, INDEXNOW_BATCH_SIZE);
117
+ logger.info(`[astro-indexnow] submitting ${changedUrls.length} changed URLs in ${batches.length} batch(es)`);
118
+ for (let i = 0; i < batches.length; i++) {
119
+ const batch = batches[i];
120
+ logger.debug(`[astro-indexnow] submitting batch ${i + 1}/${batches.length} (${batch.length} URLs)`);
121
+ try {
122
+ const response = await fetch(INDEXNOW_ENDPOINT, {
123
+ method: "POST",
124
+ headers: { "Content-Type": "application/json" },
125
+ body: JSON.stringify({
126
+ host: new URL(site).host,
127
+ key: options.key,
128
+ keyLocation: `${site}/${options.key}.txt`,
129
+ urlList: batch,
130
+ }),
131
+ });
132
+ if (!response.ok) {
133
+ logger.warn(`[astro-indexnow] batch ${i + 1} failed (${response.status})`);
134
+ }
135
+ }
136
+ catch {
137
+ logger.warn(`[astro-indexnow] batch ${i + 1} submission failed (network error)`);
78
138
  }
79
- logger.info(`[astro-indexnow] Successfully submitted ${urls.length} URLs to IndexNow`);
80
- }
81
- catch {
82
- logger.warn("[astro-indexnow] IndexNow submission failed (network error)");
83
139
  }
140
+ saveCache(logger, nextCache);
141
+ logger.info(`[astro-indexnow] IndexNow submission complete`);
84
142
  },
85
143
  },
86
144
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "astro-indexnow",
3
- "version": "1.0.0",
3
+ "version": "2.0.0",
4
4
  "description": "Astro integration to submit pages to IndexNow automatically after build",
5
5
  "type": "module",
6
6
  "exports": {
@@ -33,7 +33,7 @@
33
33
  "type": "git",
34
34
  "url": "https://github.com/velohost/astro-indexnow"
35
35
  },
36
- "homepage": "https://velohost.co.uk/tools/astro-indexnow",
36
+ "homepage": "https://velohost.co.uk/astro-indexnow",
37
37
  "bugs": {
38
38
  "url": "https://github.com/velohost/astro-indexnow/issues"
39
39
  },