介绍

IndexNow 是 Bing 等搜索引擎联合推出的实时索引协议,hexo的插件已经老了,装上之后直接报错,所以干脆整了一个脚本上传。

该脚本可以从 sitemap.xml 自动提取所有 URL,批量提交到 IndexNow。纯 Python 标准库。

  • sitemap.xml 递归提取所有 URL(支持 sitemapindex 嵌套)
  • 自动检测域名,自动匹配 IndexNow 的 host 字段
  • 提交前预检密钥文件是否真的托管在站点上(避免被 403 拒绝)
  • 网络抖动自动重试(TLS 中断、超时等)
  • 分批提交,支持并发

前置准备

根据 Bing IndexNow 官方指南,配置分为四步:

1. 获取密钥

访问 Bing IndexNow 官方页面,页面会直接提供一个 API 密钥,例如:

1
f57e273e7d004be28fcedd87d7c6341a

也可以自行生成密钥,要求(官方规范):

  • 长度:8-128 个字符
  • 字符集:小写字母(a-z)、大写字母(A-Z)、数字(0-9)、连字符(-)

生成 32 位密钥:

1
python -c "import secrets; print(secrets.token_hex(16))"

2. 托管密钥文件

必须将密钥文件托管在站点上,搜索引擎会抓取该文件以验证域名所有权。

Option 1:托管在站点根目录(推荐)

把密钥存成文本文件,文件名 = 密钥 + .txt内容 = 密钥本身,上传到站点根目录。

例如密钥是 f57e273e7d004be28fcedd87d7c6341a,则:

  • 文件名:f57e273e7d004be28fcedd87d7c6341a.txt
  • 内容:f57e273e7d004be28fcedd87d7c6341a
  • 访问地址:https://你的域名.com/f57e273e7d004be28fcedd87d7c6341a.txt

Option 2:托管在站点其他位置

也可以将密钥文件托管在站点的其他路径下,例如 https://你的域名.com/myIndexNowKey.txt。这种情况下,提交时需要通过 keyLocation 参数指定文件位置。

3. 把同一个 .txt 文件放到脚本同级目录

脚本会自动读取密钥文件。

4. 验证提交结果

提交后可通过 Bing Webmaster Tools 查看搜索引擎是否成功接收了你的 URL。

完整脚本

把下面代码保存为 indexnow_submit.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
#!/usr/bin/env python3
"""
IndexNow URL 提交脚本 — 自动从 sitemap.xml 提取 URL 并提交到 IndexNow API

用法:
python indexnow_submit.py # 从 sitemap 获取 URL 并提交
python indexnow_submit.py --urls URL1 URL2 # 手动指定 URL
python indexnow_submit.py --dry-run # 仅查看要提交的 URL,不实际提交
python indexnow_submit.py --batch 100 # 每批 100 条(默认 50)
python indexnow_submit.py --workers 4 # 4 个并发任务并行提交
python indexnow_submit.py --verbose # 显示详细请求/响应信息

环境变量:
INDEXNOW_HOST 域名(默认 your-site.com,请改成你自己的)
INDEXNOW_SITEMAP sitemap URL
INDEXNOW_KEY 直接指定密钥(跳过文件查找)
INDEXNOW_KEY_DIR 密钥文件所在目录(默认当前目录)
"""

from __future__ import annotations

import argparse
import glob
import io
import json
import logging
import os
import random
import re
import ssl
import sys
import time
import xml.etree.ElementTree as ET
from concurrent.futures import FIRST_EXCEPTION, ThreadPoolExecutor, wait
from dataclasses import dataclass, field
from typing import List, Optional, Set
from urllib.error import HTTPError, URLError
from urllib.parse import urlparse
from urllib.request import Request, urlopen

__all__ = ["main"]

# ── 终端编码兼容(Windows GBK 终端不丢中文) ──
if sys.platform == "win32" and sys.stdout.encoding and sys.stdout.encoding.lower() in ("gbk", "gb2312", "gb18030"):
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8", errors="replace")

log = logging.getLogger("indexnow")
SITEMAP_NS = {"sm": "http://www.sitemaps.org/schemas/sitemap/0.9"}
DEFAULT_USER_AGENT = "Mozilla/5.0 (compatible; IndexNowSubmitter/2.0)"

# ── 共享 SSL 上下文 ──
_SSL_CTX = ssl.create_default_context()


@dataclass
class Config:
"""全部配置,支持环境变量覆盖。"""
host: str = field(default_factory=lambda: os.getenv("INDEXNOW_HOST", "your-site.com"))
sitemap_url: str = field(default_factory=lambda: os.getenv(
"INDEXNOW_SITEMAP",
f"https://{os.getenv('INDEXNOW_HOST', 'your-site.com')}/sitemap.xml",
))
key: Optional[str] = field(default_factory=lambda: os.getenv("INDEXNOW_KEY"))
key_dir: str = field(default_factory=lambda: os.getenv("INDEXNOW_KEY_DIR", "."))
endpoint: str = "https://api.indexnow.org/IndexNow"
max_batch_size: int = 10000
max_url_length: int = 2048
batch_size: int = 50
workers: int = 1
dry_run: bool = False
verbose: bool = False

@property
def effective_batch_size(self) -> int:
return min(self.batch_size, self.max_batch_size)


def _sep(title: str = "", width: int = 56) -> None:
if title:
print(f"\n{'=' * width}\n {title}\n{'=' * width}")
else:
print(f"{'=' * width}")


def _step(num: int, total: int, label: str, detail: str = "") -> None:
line = f"[{num}/{total}] {label}" + (f" {detail}" if detail else "")
print(f"\n{line}\n{'-' * len(line)}")


# ════════════ 密钥发现 ════════════

KEY_HEX_RE = re.compile(r"^[a-zA-Z0-9-]{8,128}$")


def find_key_file(directory: str = ".") -> Optional[str]:
"""在当前目录查找 IndexNow 密钥文件(.txt)。"""
txt_files = sorted(glob.glob(os.path.join(directory, "*.txt")))
if not txt_files:
return None
seen: Set[str] = set()

def _read_key(path: str) -> Optional[str]:
try:
with open(path, "r", encoding="utf-8") as fh:
return fh.read().strip()
except OSError:
return None

# 优先:文件名本身是 32 位十六进制(即密钥本身)
for f in txt_files:
basename = os.path.splitext(os.path.basename(f))[0]
if KEY_HEX_RE.match(basename):
key = _read_key(f)
if key:
return key
seen.add(f)
# 兜底:任意 .txt 文件,内容非注释
for f in txt_files:
if f in seen:
continue
key = _read_key(f)
if key and not key.startswith("#"):
return key
return None


# ════════════ Sitemap 解析 ════════════

def fetch_xml(url: str, max_retries: int = 4) -> bytes:
"""获取 XML,对 TLS 中断 / 瞬时网络错误做指数退避重试。"""
print(f" [请求] {url}")
req = Request(url, headers={"User-Agent": DEFAULT_USER_AGENT})
last_err: Optional[Exception] = None
for attempt in range(1, max_retries + 1):
try:
with urlopen(req, timeout=30, context=_SSL_CTX) as resp:
return resp.read()
except HTTPError:
raise # HTTP 4xx/5xx 不重试,直接上抛
except (URLError, ssl.SSLError, ConnectionError, TimeoutError) as e:
last_err = e
if attempt >= max_retries:
break
delay = 2 ** attempt + random.uniform(0.5, 1.5)
print(f" [重试] 第 {attempt}/{max_retries} 次失败 ({type(e).__name__}),等待 {delay:.1f}s…")
time.sleep(delay)
raise last_err # type: ignore[misc]


def extract_urls_from_sitemap(url: str, seen: Optional[Set[str]] = None) -> List[str]:
"""递归提取 sitemap 中的所有 URL(支持 sitemapindex → urlset)。"""
if seen is None:
seen = set()
urls: List[str] = []
try:
data = fetch_xml(url)
except Exception as e:
print(f" [错误] 无法获取 {url}: {e}")
return urls
try:
root = ET.fromstring(data)
except ET.ParseError as e:
print(f" [错误] XML 解析失败 {url}: {e}")
return urls
tag = root.tag.rsplit("}", 1)[-1]
if tag == "sitemapindex":
for sitemap in root.findall("sm:sitemap", SITEMAP_NS):
loc = sitemap.find("sm:loc", SITEMAP_NS)
if loc is not None and loc.text and loc.text not in seen:
seen.add(loc.text)
urls.extend(extract_urls_from_sitemap(loc.text, seen))
elif tag == "urlset":
for url_elem in root.findall("sm:url", SITEMAP_NS):
loc = url_elem.find("sm:loc", SITEMAP_NS)
if loc is not None and loc.text:
urls.append(loc.text.strip())
return urls


# ════════════ IndexNow API ════════════

@dataclass
class SubmitResult:
success: bool
status: int
reason: str
body: str
batch_index: int = 0
url_count: int = 0


def verify_key_location(key: str, key_location: str, max_retries: int = 3) -> tuple[bool, str]:
"""预检:访问 keyLocation,确认内容包含 key,避免提交后被 403 拒绝。"""
req = Request(key_location, headers={"User-Agent": DEFAULT_USER_AGENT})
last_err: Optional[Exception] = None
for attempt in range(1, max_retries + 1):
try:
with urlopen(req, timeout=20, context=_SSL_CTX) as resp:
content = resp.read().decode("utf-8", errors="replace").strip()
if key in content:
return True, f"已托管,内容匹配 ({len(content)} 字符)"
return False, f"文件已存在但内容不包含密钥(前 80 字符: {content[:80]!r})"
except HTTPError as e:
return False, f"HTTP {e.code} — keyLocation 不可访问 ({e.reason})"
except (URLError, ssl.SSLError, ConnectionError, TimeoutError) as e:
last_err = e
if attempt >= max_retries:
break
time.sleep(2 ** attempt + random.uniform(0.5, 1.0))
return False, f"网络错误,无法验证 keyLocation: {last_err}"


def send_request(host: str, key: str, key_location: str,
url_batch: List[str], endpoint: str) -> SubmitResult:
payload = {"host": host, "key": key, "keyLocation": key_location, "urlList": url_batch}
body = json.dumps(payload, ensure_ascii=False).encode("utf-8")
req = Request(
endpoint, data=body,
headers={"Content-Type": "application/json; charset=utf-8", "User-Agent": DEFAULT_USER_AGENT},
method="POST",
)
try:
with urlopen(req, timeout=60, context=_SSL_CTX) as resp:
return SubmitResult(True, resp.status, "OK",
resp.read().decode("utf-8", errors="replace"))
except HTTPError as e:
return SubmitResult(False, e.code, str(e.reason),
e.read().decode("utf-8", errors="replace"))
except URLError as e:
return SubmitResult(False, 0, f"网络错误: {e.reason}", "")


def submit_batch(host, key, key_location, url_batch, batch_index, total_batches, config) -> SubmitResult:
b = len(url_batch)
print()
_sep(f"批次 {batch_index}/{total_batches} —— 共 {b} 条 URL")

if config.verbose:
for idx, u in enumerate(url_batch, 1):
print(f" [{idx}/{b}] {u}")
else:
for idx, u in enumerate(url_batch[:3], 1):
print(f" [{idx}/{b}] {u}")
if b > 3:
print(f" ... 还有 {b - 3} 条")

print(f"\n POST {config.endpoint}")
if config.verbose:
payload_display = {"host": host, "key": key, "keyLocation": key_location, "urlList": url_batch}
print(f" Body: {json.dumps(payload_display, ensure_ascii=False)}")

if config.dry_run:
print(f"\n [DRY RUN] 模拟完成,未实际发送 ✓")
return SubmitResult(True, 0, "DRY RUN", "", batch_index, b)

max_retries = 3
result = SubmitResult(False, 0, "", "", batch_index, b)
for attempt in range(1, max_retries + 1):
if attempt > 1:
delay = 2 ** attempt + random.uniform(1, 2)
print(f"\n [重试] 第 {attempt}/{max_retries} 次,等待 {delay:.0f} 秒…")
time.sleep(delay)
else:
print(f" 发送中…")

result = send_request(host, key, key_location, url_batch, config.endpoint)
result.batch_index = batch_index
result.url_count = b

print(f" HTTP {result.status}" + (f" {result.reason}" if result.reason else ""))
if config.verbose and result.body:
try:
print(f" 响应: {json.dumps(json.loads(result.body), ensure_ascii=False)}")
except json.JSONDecodeError:
print(f" 响应: {result.body}")

if result.success:
print(f" ✓ 成功")
return result

should_retry = False
if result.status == 429 and attempt < max_retries:
should_retry = True
print(f" ⚠ 触发频率限制")
elif result.status == 403 and "UserForbiddedToAccessSite" in result.body and attempt < max_retries:
should_retry = True
print(f" ⚠ 密钥验证未通过")

if not should_retry:
print(f" ✗ 失败 (HTTP {result.status})")
return result

print(f" ✗ 重试 {max_retries} 次后仍失败")
return result


# ════════════ URL 验证 ════════════

def validate_urls(urls: List[str], max_length: int = 2048) -> List[str]:
valid: List[str] = []
seen: Set[str] = set()
for u in urls:
u = u.strip()
if not u.startswith("https://"):
continue
if len(u) > max_length:
print(f" [跳过] URL 过长 ({len(u)} 字符): {u[:80]}…")
continue
if u not in seen:
seen.add(u)
valid.append(u)
dup_or_skip = len(urls) - len(valid)
print(f" ✓ 有效 URL: {len(valid)} 条" + (f",跳过/去重: {dup_or_skip} 条" if dup_or_skip else ""))
return valid


# ════════════ 命令行 ════════════

def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(
description="IndexNow URL 提交工具",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog=__doc__,
)
parser.add_argument("--urls", nargs="+", help="手动指定要提交的 URL 列表")
parser.add_argument("--dry-run", action="store_true", help="模拟运行,不实际提交")
parser.add_argument("--batch", type=int, default=50, help="每批提交的 URL 数量(默认 50)")
parser.add_argument("--workers", type=int, default=1, help="并发工作数(默认 1)")
parser.add_argument("--sitemap", type=str, default=None, help="sitemap URL")
parser.add_argument("--host", type=str, default=None, help="网站域名(默认 your-site.com 或 INDEXNOW_HOST)")
parser.add_argument("--key", type=str, default=None, help="直接指定密钥(跳过文件查找)")
parser.add_argument("--key-dir", type=str, default=".", help="密钥文件所在目录(默认当前目录)")
parser.add_argument("--verbose", "-v", action="store_true", help="显示每个 URL 和请求/响应详情")
return parser


def build_config(args: argparse.Namespace) -> Config:
cfg = Config()
if args.host:
cfg.host = args.host
if args.sitemap:
cfg.sitemap_url = args.sitemap
elif args.host or not os.getenv("INDEXNOW_SITEMAP"):
cfg.sitemap_url = f"https://{cfg.host}/sitemap.xml"
if args.key:
cfg.key = args.key
if args.key_dir:
cfg.key_dir = args.key_dir
cfg.batch_size = args.batch
cfg.workers = max(1, args.workers)
cfg.dry_run = args.dry_run
cfg.verbose = args.verbose
return cfg


def setup_logging(verbose: bool = False) -> None:
logging.basicConfig(
level=logging.DEBUG if verbose else logging.WARNING,
format="%(message)s", stream=sys.stdout,
)


# ════════════ main ════════════

def main(argv: Optional[List[str]] = None) -> int:
parser = build_parser()
args = parser.parse_args(argv)
config = build_config(args)
setup_logging(config.verbose)

_sep("IndexNow URL 提交工具")

# 1. 读取密钥
_step(1, 4, "读取密钥")
key = config.key or find_key_file(config.key_dir)
if not key:
print(" [错误] 未找到有效的密钥文件。")
print(f" 请将 .txt 密钥文件放在 {config.key_dir} 目录下,")
print(" 或通过 --key / INDEXNOW_KEY 环境变量指定。")
return 1
print(f" ✓ 密钥: {key}")

# 2. 获取 URL
_step(2, 4, "获取 URL")
if args.urls:
raw_urls = args.urls
print(f" ✓ 命令行提供 {len(raw_urls)} 个 URL")
detected_hosts: Set[str] = set()
for u in raw_urls:
parsed = urlparse(u)
if parsed.hostname:
detected_hosts.add(parsed.hostname)
actual_host = detected_hosts.pop() if len(detected_hosts) == 1 else config.host
if len(detected_hosts) > 1:
print(f" ℹ 检测到多个域名,使用 --host 指定的: {actual_host}")
else:
sitemap = config.sitemap_url
print(f" 从 sitemap 获取: {sitemap}")
raw_urls = extract_urls_from_sitemap(sitemap)
if not raw_urls:
print(" [错误] 未能从 sitemap 获取任何 URL")
return 1
print(f" ✓ 提取 {len(raw_urls)} 个 URL")
if config.verbose:
for u in raw_urls[:5]:
print(f" {u}")
if len(raw_urls) > 5:
print(f" … 共 {len(raw_urls)} 条")
actual_host = urlparse(raw_urls[0]).hostname or config.host

key_location = f"https://{actual_host}/{key}.txt"
print(f" ✓ 域名: {actual_host}")
print(f" ✓ keyLocation: {key_location}")

# 预检 keyLocation
print(f" 验证 keyLocation 可访问…")
ok, detail = verify_key_location(key, key_location)
if ok:
print(f" ✓ {detail}")
else:
print(f" ✗ keyLocation 预检失败:{detail}")
print(f" 提交后会被搜索引擎以 403 拒绝,已中止。")
print(f" 请把 {key}.txt(内容为密钥)上传到站点根目录:")
print(f" {key_location}")
return 1

# 3. 验证 URL
_step(3, 4, "验证 URL")
valid_urls = validate_urls(raw_urls, config.max_url_length)
if not valid_urls:
print(" [错误] 没有有效的 URL 可提交")
return 1

# 4. 分批提交
batch_size = config.effective_batch_size
_step(4, 4, f"提交到 IndexNow", f"每批 {batch_size} 条 × {config.workers} 个并发")
if config.dry_run:
print(" [模式] DRY RUN —— 不会实际发送请求")

batches = [valid_urls[i:i + batch_size] for i in range(0, len(valid_urls), batch_size)]
total_batches = len(batches)
print(f" 共 {len(valid_urls)} 条 URL,分为 {total_batches}{',开始提交…' if not config.dry_run else ''}")

success_count = 0
fail_count = 0
interrupted = False
first_error: Optional[SubmitResult] = None

if config.workers > 1 and not config.dry_run:
print(f"\n 并行模式:{config.workers} 个 worker 同时工作\n")
with ThreadPoolExecutor(max_workers=config.workers) as executor:
future_map = {
executor.submit(submit_batch, actual_host, key, key_location,
batch, i, total_batches, config): (i, batch)
for i, batch in enumerate(batches, 1)
}
done, _ = wait(future_map.keys(), return_when=FIRST_EXCEPTION)
for fut in done:
try:
result = fut.result()
if result.success:
success_count += result.url_count
else:
fail_count += result.url_count
first_error = result
break
except KeyboardInterrupt:
interrupted = True
break
except Exception as exc:
log.error("并发任务异常: %s", exc)
fail_count += 1
first_error = SubmitResult(False, 0, str(exc), "", url_count=1)
if interrupted or fail_count:
executor.shutdown(wait=False, cancel_futures=True)
else:
for i, batch in enumerate(batches, 1):
result = submit_batch(actual_host, key, key_location, batch, i, total_batches, config)
if result.success:
success_count += result.url_count
else:
fail_count += result.url_count
first_error = result
break
if not config.dry_run and i < total_batches:
delay = random.uniform(1.5, 3)
print(f"\n --- 等待 {delay:.1f} 秒后提交下一批 ---")
time.sleep(delay)

# 汇总
if fail_count and first_error and not config.dry_run:
_sep("提交失败")
print(f" 批次: {first_error.batch_index}")
print(f" 原因: HTTP {first_error.status} {first_error.reason}")
if first_error.body:
try:
print(f" 详情: {json.dumps(json.loads(first_error.body), ensure_ascii=False)}")
except json.JSONDecodeError:
print(f" 响应: {first_error.body}")
print(f" 已成功: {success_count} 条 ✓")
print(f" 已失败: {fail_count} 条 ✗")
_sep()
else:
_sep("提交报告")
print(f" 总计: {success_count + fail_count} 条")
print(f" 成功: {success_count} 条 ✓")
if config.dry_run:
print(f" 模式: DRY RUN(未实际提交)")
if interrupted:
print(f" 状态: 用户中断")
_sep()

if fail_count and first_error and first_error.status > 0:
return first_error.status
return 1 if fail_count else 0


if __name__ == "__main__":
exit(main())

一键运行

1
python indexnow_submit.py

脚本会自动:读密钥 → 拉 sitemap → 预检密钥托管 → 分批提交。

先预览不实际提交可添加参数(–dry-run):

1
python indexnow_submit.py --dry-run

命令参数

参数 说明
--dry-run 模拟运行,不实际提交
--batch N 每批 N 条(默认 50,IndexNow 上限 10000)
--workers N N 个并发(默认 1 串行)
--urls URL1 URL2 ... 手动指定 URL,跳过 sitemap
--sitemap URL 指定 sitemap 地址
--host your-site.com 指定域名
--key 你的密钥 直接传密钥,跳过文件查找
--key-dir PATH 密钥文件目录(默认当前目录)
--verbose / -v 显示每个 URL 和完整请求/响应

也可用环境变量:INDEXNOW_HOSTINDEXNOW_SITEMAPINDEXNOW_KEYINDEXNOW_KEY_DIR

多个站点分别提交

每个站点一个目录,各放各自的密钥文件:

1
2
3
4
5
6
7
my-sites/
├── site-a/
│ ├── indexnow_submit.py
│ └── aaaaaaaa密钥.txt
└── site-b/
├── indexnow_submit.py
└── bbbbbbbb密钥.txt

分别进入各自目录运行即可,脚本会自动读当前目录的密钥文件。

工作原理

  1. 从当前目录读 .txt 密钥文件(文件名是 32 位十六进制即密钥本身)
  2. 拉取 sitemap.xml,递归提取所有 URL
  3. 从 URL 自动推断域名作为 IndexNow host
  4. 预检:访问 https://你的域名/密钥.txt,确认内容包含密钥(不通过则中止,避免 403)
  5. 分批 POST 到 https://api.indexnow.org/IndexNow
  6. 输出每批结果

常见问题

问题 解决方法
返回 403 / 提示密钥验证未通过 密钥文件没正确托管。检查 https://你的域名/密钥.txt 是否可访问、内容是否就是密钥本身。
SSL / TLS 错误 脚本已内置重试(最多 4 次,指数退避),偶发抖动会自动恢复。持续失败检查网络。
返回 429 提交太频繁,触发限流。脚本会自动重试;建议拉长提交间隔或减少并发。
返回 422 URL 不属于 host 字段指定的域名。检查 sitemap 里的 URL 是否都是你的域名。

参考