上个月我们的海外站点遭遇了严重的 SEO 流量滑铁卢,Google Search Console 里的索引量突然暴跌了 40%。很多核心落地页(Landing Page)被爬虫直接判定为“已发现 - 尚未索引”或索性从索引库中剔除。
大站做 SEO,最怕的就是爬虫抓取预算(Crawl Budget)被无端浪费。为了彻底搞清楚 Googlebot 在我们站点的抓取轨迹,我们需要排查它究竟在哪些死胡同里撞墙。由于每天全站的访问日志量在亿级规模,直接用 grep 去翻 Nginx 的 raw log 根本不现实。为此,我们紧急搭建了一套基于 Kafka + Logstash + Elasticsearch (ELK) 的日志清洗清洗管线,专门过滤爬虫访问流量。
在我们的管线设计中,Nginx 访问日志通过 Filebeat 准实时地收集,并打入 Kafka 缓冲集群,以防止在大流量涌入时直接把后端的 Logstash 甚至 Elasticsearch 集群压崩。
这是我们部署在生产环境的 Logstash 日志清洗配置:
# /etc/logstash/conf.d/nginx_kafka_to_es.conf
input {
kafka {
bootstrap_servers => "kafka-node1:9092,kafka-node2:9092,kafka-node3:9092"
topics => ["nginx-access-logs"]
group_id => "logstash-seo-analyzer"
codec => "json"
consumer_threads => 4
auto_offset_reset => "latest"
client_id => "logstash-cleaner-1"
}
}
filter {
# 剔除无效的静态资源请求,只留下网页 HTML 请求进行分析
if [request_uri] =~ "\.(js|css|png|jpg|jpeg|gif|woff|svg)$" {
drop {}
}
# 判断是否为 Googlebot
if [http_user_agent] =~ "Googlebot" {
mutate {
add_field => { "is_googlebot" => "true" }
}
} else {
mutate {
add_field => { "is_googlebot" => "false" }
}
}
# 转换时间戳格式
date {
match => [ "time_local", "dd/MMM/yyyy:HH:mm:ss Z" ]
target => "@timestamp"
remove_field => [ "time_local" ]
}
}
output {
elasticsearch {
hosts => ["http://es-cluster-node1:9200", "http://es-cluster-node2:9200"]
index => "nginx-seo-crawler-%{+YYYY.MM.dd}"
document_type => "_doc"
}
}
通过 Kibana 对清洗后的日志进行聚合分析后,我们发现了一个致命漏洞:Googlebot 在拼命爬取老旧的 sitemap.xml 文件,而我们站点每次发布构建,生成的 sitemap.xml 里的 <lastmod> 属性全都被硬编码成了打包当天的系统时间。
这导致 Googlebot 每次拉取 sitemap 时,都以为全站几百万个页面全更新了。由于反复抓取那些毫无变动的旧页面,导致爬虫预算瞬间耗尽,新发布的页面根本排不上队,旧的稍微优质一点的页面也被挤出了索引库。
这纯粹是乱填 lastmod 导致的 SEO 灾难。我们需要一种极其精准的 lastmod 生成方案:只在文件内容确实发生变动时,才更新 <lastmod>。而最可靠的内容变动记录,就是 Git 提交历史。
为此,我们弃用了打包工具自带的 Sitemap 生成插件,改用 Node.js 编写了一个在构建阶段运行的脚本。该脚本使用 git log 直接读取每个文章 Markdown 文件的真实最后提交时间,并将其写入 sitemap.xml:
// scripts/generate-accurate-sitemap.js
const fs = require('fs');
const { execSync } = require('child_process');
const glob = require('glob');
/**
* 读取文件在 Git 历史中的最后一次 commit 时间
* @param {string} filePath 文件绝对或相对路径
* @returns {string} ISO 8601 格式的时间字符串
*/
function getGitLastModDate(filePath) {
try {
// %cI 格式代表 ISO 8601 提交时间 (例如 2026-07-13T15:10:31+08:00)
const stdout = execSync(`git log -1 --format=%cI -- "${filePath}"`);
const dateStr = stdout.toString().trim();
if (!dateStr) {
return new Date().toISOString();
}
return dateStr;
} catch (err) {
console.error(`Failed to get git log for ${filePath}:`, err);
return new Date().toISOString();
}
}
function run() {
const articles = glob.sync('src/content/articles/**/*.md');
let xml = '<?xml version="1.0" encoding="UTF-8"?>\n';
xml += '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n';
articles.forEach(file => {
// 假设文章路由映射为 /articles/文件名
const filename = file.split('/').pop().replace('.md', '');
const url = `https://www.ourdomain.com/articles/${filename}`;
const lastmod = getGitLastModDate(file);
xml += ' <url>\n';
xml += ` <loc>${url}</loc>\n`;
xml += ` <lastmod>${lastmod}</lastmod>\n`;
xml += ' <changefreq>weekly</changefreq>\n';
xml += ' <priority>0.8</priority>\n';
xml += ' </url>\n';
});
xml += '</urlset>\n';
fs.writeFileSync('dist/sitemap.xml', xml);
console.log(`Successfully generated sitemap.xml with ${articles.length} links.`);
}
run();
把这个脚本挂载到构建阶段(如 npm run build 执行完后)后,我们生成的 sitemap.xml 中每个链接都携带了最真实的最后修改时间。
这套逻辑上线两周后,Googlebot 访问我们站点的频率明显回归理性:它不再重复爬取未变更的页面,而是精准地只抓取新文章以及刚刚修改过的老文章。索引丢失的状况开始触底反弹,目前基本已经恢复到之前的水平。
你们在大中型多语言站点的 Sitemap 规划上,是如何保证爬虫抓取效率的?是用上述 Git 时间流的方案,还是由后端 CMS 直接存取库表更新时间戳?