适用环境:PHP + PDO SQLite 自建 BBS(本站架构),伪静态已开启,密钥已生成。 效果:后台保存文章(新增或修改)后自动向 IndexNow 提交该文章 URL,无需手动跑脚本。 涉及文件:indexnow_push.php(新建)、indexnow_worker.php(新建)、admin/indexnow_log.php(新建)、config.php(加 1 行)、admin/edit.php(加 3 行)。


一、为什么原来的手动脚本不够用

你现有的 push_indexnow.php 是访问一次推一批的模式,存在三个问题:

  1. 要手动打开浏览器访问,发完文章容易忘
  2. 推的是最新 100 条,包含大量早就提交过的旧 URL,属于无效请求
  3. 没有留痕,推成功还是失败、哪条失败,事后无从查证

IndexNow 的机制是"URL 有变化时通知一次",所以最合理的触发点就是文章保存的那一刻——新增时提交新 URL,修改时重新提交触发更新索引。


二、方案设计

2.1 整体思路

website/edit.php 保存成功
        ↓
indexNowAutoPush($db, $artId)
        ↓
环境判断 → 去重检查 → POST api.indexnow.org → 写库留痕 + 文本日志

2.2 五个关键设计

设计 解决的问题
自动去重(同一 URL 3600 秒内不重推) 连续点保存、微调正文反复提交,避免被 IndexNow 限流
推送留痕(写入 indexnow_push 表) 后台可查每篇推没推、HTTP 状态码是什么
环境判断(localhost 自动跳过) 本地调试不污染线上,也不会把 localhost 的 URL 提交上去
失败降级(curl 不可用回退 file_get_contents) 服务器没装 curl 也能工作;请求失败不影响文章保存
可异步(INDEXNOW_ASYNC) 开启后编辑页零等待,推送交给后台子进程

2.3 数据表

首次调用自动建表,无需手动执行:

CREATE TABLE IF NOT EXISTS indexnow_push (
    id         INTEGER PRIMARY KEY AUTOINCREMENT,
    url        TEXT NOT NULL,
    art_id     INTEGER,
    status     INTEGER DEFAULT 0,
    http_code  TEXT,
    note       TEXT,
    push_count INTEGER DEFAULT 1,
    push_time  DATETIME DEFAULT CURRENT_TIMESTAMP
);

三、文件一:indexnow_push.php(根目录)

放到 bbs/ 根目录,与 config.php 平级。

<?php
/**
 * indexnow_push.php —— 发布/修改文章时自动推送 IndexNow
 * ------------------------------------------------------------
 * 部署:放到 bbs 根目录(与 config.php 平级)
 * 接入:admin/edit.php 保存成功后调用 indexNowAutoPush($db, $artId);
 *
 * 特性:
 *   1. 自动去重:同一 URL 在 INDEXNOW_MIN_INTERVAL 秒内不重复推送,避免被限流
 *   2. 推送留痕:写入 indexnow_push 表,后台 admin/indexnow_log.php 可查看
 *   3. 环境判断:localhost / 127.0.0.1 自动跳过,本地调试不污染线上
 *   4. 失败降级:curl 不可用时自动回退 file_get_contents,失败不影响文章保存
 *   5. 可异步:设 INDEXNOW_ASYNC=true 后台静默推送,编辑页零等待
 */
if (!defined('IN_BBS')) exit;

/* ====================== 配置区 ====================== */
define('INDEXNOW_HOST',         'bbs.website.com');
define('INDEXNOW_KEY',          'b5125678633f0224f4d034576818ahgjgthfc');
define('INDEXNOW_KEY_LOCATION', 'https://bbs.website.com/b5125678633f0224f4d034576818ahgjgthfc.txt');
define('INDEXNOW_ENABLED',      true);   // 总开关:临时关闭改 false
define('INDEXNOW_MIN_INTERVAL', 3600);   // 同一URL最小推送间隔(秒),0=每次都推
define('INDEXNOW_TIMEOUT',      5);      // 单次请求超时(秒)
define('INDEXNOW_ASYNC',        false);  // true=后台异步推送(Windows XAMPP 需 php.exe 在 PATH)
define('INDEXNOW_LOG_FILE',     __DIR__ . '/indexnow.log'); // 文本日志,排查用
/* ==================================================== */

/** 文章 URL */
if (!function_exists('indexNowArticleUrl')) {
function indexNowArticleUrl(int $id): string
{
    return 'https://' . INDEXNOW_HOST . '/article-' . $id . '.html';
}
}

/** 是否允许推送(总开关 + 排除本地环境) */
if (!function_exists('indexNowCanPush')) {
function indexNowCanPush(): bool
{
    if (!INDEXNOW_ENABLED) return false;
    $host = strtolower($_SERVER['HTTP_HOST'] ?? '');
    if ($host === '') return true; // CLI 环境,放行
    $host = preg_replace('/:\d+$/', '', $host);
    if (in_array($host, ['localhost', '127.0.0.1', '::1'], true)) return false;
    return true;
}
}

/** 建表(重复运行无害) */
if (!function_exists('indexNowEnsureTable')) {
function indexNowEnsureTable(PDO $db): void
{
    $db->exec("
    CREATE TABLE IF NOT EXISTS indexnow_push (
        id         INTEGER PRIMARY KEY AUTOINCREMENT,
        url        TEXT NOT NULL,
        art_id     INTEGER,
        status     INTEGER DEFAULT 0,
        http_code  TEXT,
        note       TEXT,
        push_count INTEGER DEFAULT 1,
        push_time  DATETIME DEFAULT CURRENT_TIMESTAMP
    )");
    $db->exec("CREATE INDEX IF NOT EXISTS idx_inx_url ON indexnow_push(url)");
    $db->exec("CREATE INDEX IF NOT EXISTS idx_inx_time ON indexnow_push(push_time)");
}
}

/** 查询该 URL 上次推送记录 */
if (!function_exists('indexNowLastPush')) {
function indexNowLastPush(PDO $db, string $url): ?array
{
    $st = $db->prepare("SELECT * FROM indexnow_push WHERE url = :u ORDER BY id DESC LIMIT 1");
    $st->bindValue(':u', $url);
    $st->execute();
    $row = $st->fetch(PDO::FETCH_ASSOC);
    return $row ?: null;
}
}

/** 记录推送结果 */
if (!function_exists('indexNowSaveLog')) {
function indexNowSaveLog(PDO $db, string $url, ?int $artId, int $status, string $httpCode, string $note): void
{
    $last = indexNowLastPush($db, $url);
    try {
        if ($last) {
            $st = $db->prepare("UPDATE indexnow_push
                SET status=:s, http_code=:c, note=:n, push_time=CURRENT_TIMESTAMP,
                    push_count = push_count + 1, art_id=:a
                WHERE id=:id");
            $st->execute([':s'=>$status, ':c'=>$httpCode, ':n'=>$note, ':a'=>$artId, ':id'=>$last['id']]);
        } else {
            $st = $db->prepare("INSERT INTO indexnow_push (url, art_id, status, http_code, note)
                                VALUES (:u, :a, :s, :c, :n)");
            $st->execute([':u'=>$url, ':a'=>$artId, ':s'=>$status, ':c'=>$httpCode, ':n'=>$note]);
        }
    } catch (Exception $e) {
        // 日志写失败不影响主流程
    }
}
}

/** 文本日志 */
if (!function_exists('indexNowFileLog')) {
function indexNowFileLog(string $line): void
{
    @file_put_contents(INDEXNOW_LOG_FILE,
        '[' . date('Y-m-d H:i:s') . '] ' . $line . PHP_EOL, FILE_APPEND | LOCK_EX);
}
}

/**
 * 真正发送 POST 到 IndexNow
 * @return array ['ok'=>bool,'code'=>int,'msg'=>string]
 */
if (!function_exists('indexNowSend')) {
function indexNowSend(array $urls): array
{
    $urls = array_values(array_unique(array_filter($urls)));
    if (empty($urls)) return ['ok'=>false, 'code'=>0, 'msg'=>'URL 列表为空'];

    $data = json_encode([
        'host'        => INDEXNOW_HOST,
        'key'         => INDEXNOW_KEY,
        'keyLocation' => INDEXNOW_KEY_LOCATION,
        'urlList'     => $urls,
    ], JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);

    // 优先 curl:能拿到精确状态码
    if (function_exists('curl_init')) {
        $ch = curl_init('https://api.indexnow.org/indexnow');
        curl_setopt_array($ch, [
            CURLOPT_POST           => true,
            CURLOPT_POSTFIELDS     => $data,
            CURLOPT_HTTPHEADER     => ['Content-Type: application/json; charset=utf-8'],
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_TIMEOUT        => INDEXNOW_TIMEOUT,
            CURLOPT_CONNECTTIMEOUT => 3,
            CURLOPT_SSL_VERIFYPEER => true,
        ]);
        $body = curl_exec($ch);
        $code = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
        $err  = curl_error($ch);
        curl_close($ch);
        if ($err !== '') {
            return ['ok'=>false, 'code'=>$code, 'msg'=>'curl 错误:' . $err];
        }
        return ['ok'=>($code >= 200 && $code < 300), 'code'=>$code, 'msg'=>'HTTP ' . $code];
    }

    // 回退 file_get_contents
    $opts = ['http' => [
        'method'  => 'POST',
        'header'  => "Content-Type: application/json; charset=utf-8\r\n",
        'content' => $data,
        'timeout' => INDEXNOW_TIMEOUT,
        'ignore_errors' => true,
    ]];
    $ctx  = stream_context_create($opts);
    $body = @file_get_contents('https://api.indexnow.org/indexnow', false, $ctx);
    $line = isset($http_response_header[0]) ? $http_response_header[0] : '';
    $code = 0;
    if (preg_match('/\s(\d{3})\s/', $line, $m)) $code = (int)$m[1];
    return ['ok'=>($code >= 200 && $code < 300), 'code'=>$code, 'msg'=>$line ?: '无响应'];
}
}

/**
 * 【主入口】发布或修改文章后调用
 *
 * @param PDO    $db
 * @param int    $artId   文章ID
 * @param array  $extra   额外要推的URL(如首页、分页)
 * @param bool   $force   true=忽略最小间隔,强制推送
 * @return array ['ok'=>bool,'skipped'=>bool,'msg'=>string]
 */
if (!function_exists('indexNowAutoPush')) {
function indexNowAutoPush(PDO $db, int $artId, array $extra = [], bool $force = false): array
{
    if (!indexNowCanPush()) {
        return ['ok'=>false, 'skipped'=>true, 'msg'=>'当前环境未启用推送(本地或总开关关闭)'];
    }
    if ($artId <= 0) {
        return ['ok'=>false, 'skipped'=>true, 'msg'=>'文章ID无效'];
    }

    indexNowEnsureTable($db);
    $url = indexNowArticleUrl($artId);

    // 去重:距上次推送太近就跳过(修改文章连点保存时很有用)
    if (!$force && INDEXNOW_MIN_INTERVAL > 0) {
        $last = indexNowLastPush($db, $url);
        if ($last && !empty($last['push_time'])) {
            $gap = time() - strtotime((string)$last['push_time']);
            if ($gap >= 0 && $gap < INDEXNOW_MIN_INTERVAL) {
                return ['ok'=>false, 'skipped'=>true,
                        'msg'=>'距上次推送仅 ' . $gap . ' 秒,小于间隔 ' . INDEXNOW_MIN_INTERVAL . ' 秒,已跳过'];
            }
        }
    }

    $urls = array_merge([$url], $extra);

    // 异步模式:Windows 下用 start /B 拉起子进程,编辑页零等待
    if (INDEXNOW_ASYNC) {
        $worker = __DIR__ . '/indexnow_worker.php';
        if (is_file($worker)) {
            $cmd = 'start /B php "' . $worker . '" ' . (int)$artId . ' > NUL 2>&1';
            @pclose(@popen($cmd, 'r'));
            return ['ok'=>true, 'skipped'=>false, 'msg'=>'已提交后台异步推送'];
        }
    }

    $res = indexNowSend($urls);
    indexNowSaveLog($db, $url, $artId, $res['ok'] ? 1 : 0, (string)$res['code'], $res['msg']);
    indexNowFileLog(($res['ok'] ? 'OK  ' : 'FAIL') . " art={$artId} code={$res['code']} {$res['msg']} url={$url}");

    return ['ok'=>$res['ok'], 'skipped'=>false, 'msg'=>$res['msg']];
}
}

配置区就在这个文件顶部,六个开关按需调整:

define('INDEXNOW_ENABLED',      true);   // 总开关,临时停用改 false
define('INDEXNOW_MIN_INTERVAL', 3600);   // 同一URL最小推送间隔(秒),0=每次都推
define('INDEXNOW_TIMEOUT',      5);      // 请求超时
define('INDEXNOW_ASYNC',        false);  // true=后台异步推送

四、文件二:indexnow_worker.php(根目录)

异步模式下的工作进程,命令行调用,无需手动访问。

<?php
/**
 * indexnow_worker.php —— 异步推送工作进程
 * 用法(命令行):php indexnow_worker.php 文章ID
 * 由 indexnow_push.php 在 INDEXNOW_ASYNC=true 时后台拉起,无需手动访问。
 */
if (PHP_SAPI !== 'cli') { exit; }

define('IN_BBS', true);
require_once __DIR__ . '/config.php';
require_once __DIR__ . '/indexnow_push.php';

$artId = isset($argv[1]) ? (int)$argv[1] : 0;
if ($artId <= 0) exit;

$res = indexNowAutoPush($db, $artId, [], true); // 异步模式下强制推送
indexNowFileLog('ASYNC art=' . $artId . ' ' . ($res['ok'] ? 'OK' : 'SKIP/FAIL') . ' ' . $res['msg']);

只有把 INDEXNOW_ASYNC 改成 true 时才需要它。Windows XAMPP 需要 php.exe 在系统 PATH 里,否则异步会静默失败(不影响文章保存,只是没推)。


五、文件三:website/indexnow_log.php(后台查看页)

放到 bbs/website/,登录后访问 /website/indexnow_log.php。

功能:

  • 统计:累计推送数、成功数
  • 记录:最近 200 条,含时间、URL、文章ID、状态、HTTP码、推送次数、说明
  • 手动补推单篇(按文章ID,强制推送,忽略间隔)
  • 批量补推最新 N 条(默认 100,最大 10000)
<?php
/**
 * website/indexnow_log.php —— IndexNow 推送记录查看 + 手动补推
 * 部署:放到 bbs/website/ 目录
 * 访问:登录后打开 /website/indexnow_log.php
 *
 * 注意:本页依赖你后台的登录校验文件 _auth.php;
 *      如果登录校验方式不同,把下面 require 那行改成你后台统一的校验写法。
 */
require_once __DIR__ . '/_auth.php';
require_once dirname(__DIR__) . '/config.php';
require_once dirname(__DIR__) . '/indexnow_push.php';

indexNowEnsureTable($db);

$msg = '';
$msgType = 'info';

// 手动操作
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $act = $_POST['act'] ?? '';

    if ($act === 'push_one') {
        $artId = (int)($_POST['art_id'] ?? 0);
        if ($artId > 0) {
            $res = indexNowAutoPush($db, $artId, [], true); // 手动=强制
            $msg = '文章 #' . $artId . ':' . ($res['ok'] ? '✅ 推送成功' : '⚠️ ' . $res['msg']);
            $msgType = $res['ok'] ? 'success' : 'warning';
        }
    }

    if ($act === 'push_latest') {
        $limit = (int)($_POST['limit'] ?? 100);
        $limit = max(1, min(10000, $limit));
        $rows = $db->query("SELECT id FROM article ORDER BY id DESC LIMIT " . $limit)->fetchAll(PDO::FETCH_ASSOC);
        $urls = [];
        foreach ($rows as $r) $urls[] = indexNowArticleUrl((int)$r['id']);
        $res = indexNowSend($urls);
        indexNowFileLog(($res['ok'] ? 'OK  ' : 'FAIL') . " batch={$limit} code={$res['code']} {$res['msg']}");
        foreach ($urls as $u) {
            indexNowSaveLog($db, $u, null, $res['ok'] ? 1 : 0, (string)$res['code'], '批量补推 ' . $res['msg']);
        }
        $msg = '批量推送 ' . count($urls) . ' 条:' . ($res['ok'] ? '✅ 成功' : '❌ 失败') . '(' . $res['msg'] . ')';
        $msgType = $res['ok'] ? 'success' : 'danger';
    }
}

// 读取记录
$logs = $db->query("SELECT * FROM indexnow_push ORDER BY push_time DESC, id DESC LIMIT 200")
           ->fetchAll(PDO::FETCH_ASSOC);
$stat = $db->query("SELECT
        COUNT(*) AS total,
        SUM(CASE WHEN status=1 THEN 1 ELSE 0 END) AS ok_cnt
    FROM indexnow_push")->fetch(PDO::FETCH_ASSOC);

$h = function ($s) { return htmlspecialchars((string)$s, ENT_QUOTES, 'UTF-8'); };
?>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<title>IndexNow 推送记录 - 511遇见BBS后台</title>
<meta name="robots" content="noindex,nofollow">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.3.1/dist/css/bootstrap.min.css">
<style>body{padding:24px;background:#f5f7fb;} table{background:#fff;} .mono{font-family:Consolas,Monaco,monospace;font-size:12px;}</style>
</head>
<body>
<div class="container" style="max-width:1100px;">

<div class="d-flex align-items-center justify-content-between mb-3">
  <h4 class="mb-0">📡 IndexNow 推送记录</h4>
  <div>
    <a href="index.php" class="btn btn-sm btn-outline-secondary">← 后台首页</a>
    <a href="../index.php" target="_blank" class="btn btn-sm btn-outline-primary">前台首页</a>
  </div>
</div>

<?php if ($msg): ?>
<div class="alert alert-<?php echo $h($msgType); ?>"><?php echo $h($msg); ?></div>
<?php endif; ?>

<div class="card mb-3">
  <div class="card-body">
    <div class="row text-center">
      <div class="col-4">
        <div class="text-muted" style="font-size:13px;">累计推送</div>
        <div style="font-size:24px;font-weight:600;"><?php echo (int)$stat['total']; ?></div>
      </div>
      <div class="col-4">
        <div class="text-muted" style="font-size:13px;">成功</div>
        <div style="font-size:24px;font-weight:600;color:#28a745;"><?php echo (int)$stat['ok_cnt']; ?></div>
      </div>
      <div class="col-4">
        <div class="text-muted" style="font-size:13px;">密钥</div>
        <div class="mono" style="font-size:11px;word-break:break-all;"><?php echo $h(substr(INDEXNOW_KEY, 0, 12) . '…'); ?></div>
      </div>
    </div>
  </div>
</div>

<div class="card mb-3">
  <div class="card-body">
    <form method="post" class="form-inline mb-0">
      <label class="mr-2" style="font-size:14px;">手动补推文章ID</label>
      <input type="number" name="art_id" class="form-control form-control-sm mr-2" placeholder="如 12" min="1" required>
      <input type="hidden" name="act" value="push_one">
      <button class="btn btn-sm btn-primary mr-4">推送这篇</button>
    </form>
    <hr class="my-3">
    <form method="post" class="form-inline mb-0" onsubmit="return confirm('确认批量推送最新文章?');">
      <label class="mr-2" style="font-size:14px;">批量补推最新</label>
      <input type="number" name="limit" class="form-control form-control-sm mr-2" value="100" min="1" max="10000">
      <span class="mr-2" style="font-size:14px;">条</span>
      <input type="hidden" name="act" value="push_latest">
      <button class="btn btn-sm btn-warning">批量推送</button>
    </form>
  </div>
</div>

<div class="card">
  <div class="card-body p-0">
    <table class="table table-sm table-hover mb-0">
      <thead class="thead-light">
        <tr>
          <th style="width:120px;">时间</th>
          <th>URL</th>
          <th style="width:70px;">文章</th>
          <th style="width:60px;">状态</th>
          <th style="width:70px;">HTTP</th>
          <th style="width:60px;">次数</th>
          <th>说明</th>
        </tr>
      </thead>
      <tbody>
      <?php if (empty($logs)): ?>
        <tr><td colspan="7" class="text-center text-muted py-4">暂无推送记录,去发布或修改一篇文章试试</td></tr>
      <?php else: ?>
        <?php foreach ($logs as $l): ?>
        <tr>
          <td class="mono"><?php echo $h($l['push_time']); ?></td>
          <td class="mono"><a href="<?php echo $h($l['url']); ?>" target="_blank"><?php echo $h($l['url']); ?></a></td>
          <td><?php echo $l['art_id'] ? '#' . (int)$l['art_id'] : '-'; ?></td>
          <td>
            <?php if ((int)$l['status'] === 1): ?>
              <span class="badge badge-success">成功</span>
            <?php else: ?>
              <span class="badge badge-danger">失败</span>
            <?php endif; ?>
          </td>
          <td class="mono"><?php echo $h($l['http_code']); ?></td>
          <td><?php echo (int)$l['push_count']; ?></td>
          <td class="mono text-muted"><?php echo $h($l['note']); ?></td>
        </tr>
        <?php endforeach; ?>
      <?php endif; ?>
      </tbody>
    </table>
  </div>
</div>

<p class="text-muted mt-3" style="font-size:12px;">
  说明:发布/修改文章时会自动推送。同一 URL 默认 <?php echo (int)INDEXNOW_MIN_INTERVAL; ?> 秒内不重复推送,手动补推与批量推送不受此限制。
</p>

</div>
</body>
</html>

注意:本页 require_once __DIR__ . '/_auth.php'; 依赖你后台的登录校验文件。如果你的后台校验方式不同(比如写在 _header.php 里),把这行改成你后台统一的校验写法,切勿跳过校验直接开放。


六、接线:两处改动

6.1 config.php 加一行

在 require_once __DIR__ . '/functions.php'; 之后加:

require_once __DIR__ . '/indexnow_push.php';

这一步让全站(含后台)都能调用推送函数,后面 edit.php 就不用再 require。

6.2 admin/edit.php 的三处改动(以本站实际代码为例)

你的 admin/edit.php 里原本已经有一版推送:

// 推送 IndexNow(函数在 functions.php 里)
if (function_exists('indexNowPushSingle')) {
    try {
        $pushUrl = getStaticUrl('article', $pushId, 1, [], true);
        indexNowPushSingle($pushUrl);
    } catch (Exception $e) {}
}

这版能用,但没有去重、没有留痕。改法如下:

改动 1:顶部引入推送模块(紧跟 require_once __DIR__ . '/_auth.php';)

// ===== IndexNow 自动推送模块(根目录 indexnow_push.php)=====
// 提供去重、留痕、本地环境跳过;文件不存在时自动回退到旧的 indexNowPushSingle()
if (!defined('IN_BBS')) define('IN_BBS', true);   // 保险:_auth.php 若未间接引入 config.php
$indexNowPath = dirname(__DIR__) . '/indexnow_push.php';
if (is_file($indexNowPath)) {
    require_once $indexNowPath;
}

注意用 dirname(__DIR__) 而非 __DIR__:edit.php 在 website/ 子目录,推送文件在根目录。 is_file() 判断保证即使文件没上传,编辑页也不会报错。

改动 2:替换原来的推送块

// ===== 自动推送 IndexNow(新增/修改都会触发)=====
unset($_SESSION['indexnow_msg']);
if (function_exists('indexNowAutoPush')) {
    // 去重逻辑:同一 URL 默认 3600 秒内不重复推送,避免连点保存被限流
    try {
        $pushRes = indexNowAutoPush($db, (int)$pushId);
        if (!$pushRes['ok'] && !$pushRes['skipped']) {
            $_SESSION['indexnow_msg'] = '⚠️ IndexNow 推送失败:' . $pushRes['msg'];
        } elseif ($pushRes['ok']) {
            $_SESSION['indexnow_msg'] = '✅ IndexNow 已推送';
        } else {
            $_SESSION['indexnow_msg'] = 'ℹ️ IndexNow 本次跳过:' . $pushRes['msg'];
        }
    } catch (Exception $e) {
        // 推送异常绝不影响文章保存
        $_SESSION['indexnow_msg'] = '⚠️ IndexNow 推送异常:' . $e->getMessage();
    }
} elseif (function_exists('indexNowPushSingle')) {
    // 回退:旧的单条推送函数(functions.php 里)
    try {
        $pushUrl = getStaticUrl('article', $pushId, 1, [], true);
        indexNowPushSingle($pushUrl);
    } catch (Exception $e) {}
}

改动 3:保存后展示推送结果(原第 61 行 if (isset($_GET['ok'])) $msg = '✅保存成功';)

if (isset($_GET['ok'])) {
    $msg = '✅保存成功';
    if (!empty($_SESSION['indexnow_msg'])) {
        $msg .= ' ' . $_SESSION['indexnow_msg'];
        unset($_SESSION['indexnow_msg']);
    }
}

之所以绕一圈 session:保存后会 header() 跳走,原来的 $msg 变量丢失,只有跨请求的 session 能把结果带到跳转后的页面。

三种常见变量写法对照(如果你的 edit.php 结构与示例不同):

edit.php 的写法 对应取值
新增用 lastInsertId() (int)$db->lastInsertId()
更新时 id 来自 $_POST['aid'] (int)$_POST['aid']
更新时 id 来自 $_GET['id'] (int)$_GET['id']

本站 edit.php 的隐藏字段叫 aid(不是 id),并且新增/修改共用 $pushId 变量,所以直接 indexNowAutoPush($db, (int)$pushId) 即可。


七、上线验证

  • [ ] 上传 indexnow_push.php、indexnow_worker.php 到 website/ 根目录
  • [ ] 上传 indexnow_log.php 到 bbs/website/
  • [ ] config.php 加一行 require
  • [ ] website/edit.php 保存逻辑后加 3 行
  • [ ] 重启 Apache(清 OPcache)
  • [ ] 新建一篇测试文章,保存后打开 /website/indexnow_log.php,应看到状态"成功"、HTTP 200
  • [ ] 再改一次同一篇(间隔内),应看到记录被跳过或次数+1
  • [ ] 点"手动补推"输入该文章ID,应强制推送成功
  • [ ] 确认 indexnow.log 文本文件有记录

常见状态码

状态码 含义
200 成功接收
202 已接受,后台排队处理(也算成功)
400 JSON 格式错误,检查 urlList
403 密钥无效,检查 key 与 keyLocation 是否匹配
422 URL 不属于该 host
429 请求过频,需降低推送频率(调大 INDEXNOW_MIN_INTERVAL)

八、几个实用建议

1. **密钥文件必须外网可访问**:`https://bbs.website.com/aiodsfjh9837490q823jrfokAE456f8ff418aaefc.txt` 要能直接打开且内容为密钥本身。这是 403 报错的头号原因。
  1. 不要每次保存都推:默认 3600 秒间隔已经够用。IndexNow 本身就是"通知变化",一天推一次同一 URL 足矣。

  2. 批量脚本可以留着:push_indexnow.php 保留作为"全站补推"的兜底手段,但加访问密码,别裸奔在公网。或者直接用新的 admin/indexnow_log.php 里的批量按钮(需登录,更安全)。

  3. 发布新文章时可顺便推首页:如果你的首页/分页也想让搜索引擎尽快更新,调用时传第二个参数:

    indexNowAutoPush($db, $artId, [
        'https://bbs.website.com/',
        'https://bbs.website.com/index-website-1.html',
    ]);
  4. 确认服务器能出站:部分虚拟机会屏蔽出站请求。临时在服务器上执行 curl -I https://api.indexnow.org 验证,连不通就只能在有外网的机器跑批量脚本。


九、改动文件对照表

文件 操作 说明
indexnow_push.php 新建 核心:环境判断、去重、发送、留痕
indexnow_worker.php 新建 异步模式工作进程(可选)
website/indexnow_log.php 新建 推送记录查看 + 手动补推
config.php 加 1 行 require_once indexnow_push.php
website/edit.php 改 3 处 引入模块 + 替换推送块 + session 回显结果
functions.php 无需改动 原有 indexNowPushSingle() 保留,不影响

备份提示:改前请备份 config.php、admin/edit.php 与数据库 site.db。推送失败不会影响文章保存,可放心上线。98