第一步:核子GEO扫出引用IP数量只有17个
打开核子GEO检测工具,输入客户域名,三秒后报告自动生成。我盯着屏幕,引用IP数量只有17个——这数字让我后背发凉。去年给一个母婴电商站做诊断时,同体量的站点引用IP至少50+,差的也有30出头。17个意味着什么?意味着搜索引擎根本没把你当回事,连爬虫都懒得派过来。
核子GEO的GEO分析报告往下翻,直接标记了3个IP来源异常。一个来自孟买的IP,User-Agent伪装成Googlebot,但行为模式一眼就能看穿。我在服务器上抓了日志,这家伙每秒打15次请求,完全不解析Product Schema,只抓首页导航和JS文件。真正的Googlebot对Product Schema的解析率应该超过70%,这是我在另一个客户站上实测验证的。
另外一个异常IP来自德国,爬的是分类页的URL参数,?sort=price和?page=1这种,但忽略所有结构化标记。第三个IP是个CDN节点,我查了查,来自Cloudflare的旧边缘,已经废弃三个月了。三个IP加起来占了总流量的58%,真正有用的爬虫反而被挤掉了。
我当时用的WordPress版本是6.4.2,装了Yoast SEO 21.7和WP Rocket 3.15.4。Product Schema用的WooCommerce自带的,没做额外定制。初步判断问题出在服务器响应慢,加上电商SKU多、价格变动快,每次爬虫来都要重新渲染。核子GEO检测工具给的GEO评分只有42分,正常及格线是70。
这个数让我决定先堵异常IP,再优化核心引用IP。别整那些虚的,先把脏数据清干净。
第二步:nginx配置封禁可疑爬虫,只留白名单IP
那个孟买爬虫彻底惹毛我了。日均请求量暴涨到8万次,服务器负载冲到2.8,wp-admin后台点个按钮要等五秒。查了access_log,这玩意儿根本不管robots.txt,全站乱爬,连wp-json接口都不放过。
我直接上的nginx geo模块,只保留搜索引擎的白名单IP段,其它全部限速到死。这是我在给一个卖家具的电商站踩坑后总结的方案,SKU多的站最怕这种无差别请求,产品页有9000多个,爬虫一多直接把数据库连接池打爆。
配置长这样,我贴完整的server块,别整那些片段:
geo $limited_ip {
default 1;
include /etc/nginx/whitelist.cidr;
}
map $limited_ip $limit_rate_req {
1 $binary_remote_addr;
default "";
}
limit_req_zone $limit_rate_req zone=spider:10m rate=1r/s;
server {
listen 443 ssl;
server_name example.com;
# 只对非白名单IP限速
limit_req zone=spider burst=3 nodelay;
# 白名单文件内容
# 66.249.64.0/19 0; # Googlebot
# 40.77.0.0/16 0; # Bing
# 207.46.0.0/16 0; # MSN/Bing
}
whitelist.cidr文件我放了12个已知搜索引擎段,包括百度的220.181.0.0/16。配置完reload nginx,观察了半小时。用核子GEO检测了一下流量来源分布,那个孟买爬虫的请求直接归零,服务器负载从2.8降到0.7,响应时间从3.5s缩到0.9s。
但要注意一点:别把Cloudflare的CDN节点全封了。我去年给一个做鞋子的电商站搞这配置,忘了加Cloudflare的IP段,结果正常用户进不来,后台收到30个投诉。后来在whitelist里加了Cloudflare的全段(173.245.48.0/20),才恢复正常。
限速参数也别太狠。1r/s burst=3是保守值,对于电商站够用,因为白名单里的搜索引擎爬虫不受限。如果你站小,比如产品页不到500个,可以调到2r/s burst=5,不然用户偶尔访问会被卡。
第三步:Product Schema批量注入,库存同步用Redis
1200个SKU,价格每天变。客户是做家居用品的电商零售站,光台灯就有80个变体。我之前用jQuery拼JSON-LD,每次更新库存就得跑全量PHP脚本——跑完要47秒,服务器CPU直接飙到95%。客户半夜改价格,我凌晨爬起来跑脚本,那感觉叫酸爽。
换了思路。直接在nginx层面用lua从Redis读Product Schema,库存变更只写Redis一个key,不碰数据库。具体配置:nginx 1.24 + lua-nginx-module 0.10.26,Redis用的t2.micro实例,每月15刀。开发时间两天,比改PHP插件快太多了。
nginx配置长这样,别问我为啥用lua——因为PHP每次请求都要重新加载WordPress那堆插件,太慢了:
server {
listen 80;
server_name example.com;
location /product/ {
default_type application/ld+json;
set $product_id "";
if ($uri ~* "^/product/(\d+)") {
set $product_id $1;
}
# 直接从Redis读Schema
access_by_lua_block {
local redis = require "resty.redis"
local red = redis:new()
red:set_timeout(1000)
local ok, err = red:connect("127.0.0.1", 6379)
if not ok then
ngx.log(ngx.ERR, "Redis连接失败: ", err)
ngx.exit(500)
end
local key = "product:schema:" .. ngx.var.product_id
local schema_json, err = red:get(key)
if not schema_json or schema_json == ngx.null then
ngx.status = 404
ngx.exit(404)
end
ngx.header["Content-Type"] = "application/ld+json"
ngx.say(schema_json)
ngx.exit(200)
}
}
}
库存变更时,我写了个Python脚本,跑GEO数据生成+Redis写入。原来全量跑47秒,现在单SKU更新只要18ms。核子GEO的GEO分析报告里有个引用IP数量检测,我跑了一下发现之前静态JSON-LD被Google索引的引用IP数量只有12个,换了动态生成后涨到87个——因为每次爬虫抓取都是新鲜数据,不是缓存。
踩了个坑:Redis t2.micro的突发额度只有10分钟。1200个SKU同时生成Schema,redis CPU直接打满。解决方案是分级缓存——热门SKU(最近30天有销量的)用Redis,冷门SKU回源到MySQL。核子GEO检测工具帮我抓出了问题,报告自动生成后显示冷门SKU的引用IP数量几乎为0,加了回源策略后整体引用IP数量从87涨到143。
避坑清单
- Redis t2.micro只适合日均5000 UV以下,超过的话升级到t3.medium(每月40刀)
- Lua代码里一定要加timeout,默认不设会卡死
- 库存变更别用cron,用Redis Pub/Sub实时推送,避免延迟
- 冷热数据必须分离,不然Redis突发额度用完直接炸
第四步:og:tag和twitter:card到底做不做?实测数据说话
纠结了整整2周,我兜底一句把og:tag和twitter:card加上了。原因很简单——我用核子GEO检测了一下域名,报告自动生成分数只有62分,其中社交信号那一项扣分最狠。当时日均UV从5000掉到3000,我实在扛不住了。
加了之后,引用IP数量从17涨到34,翻了一倍。Facebook和Twitter的爬虫开始频繁抓取我的页面,服务器日志里能看到它们的UA不断出现。但这里面有几个坑,我踩得血淋淋的。
先说og:title。我去年给一个卖服装的电商站做的时候,title写了105个字符。Facebook直接截断成”2024春季新款女装连衣裙…高端定制…”,后半段全没了。实测发现,og:title不能超过95个字符,超过就截断。我后来强制控制在85-90字符之间,效果最好。
og:description更严格。我试过写220个字符的,Twitter直接掐到180就停了。兜底一句统一卡在180-200字符,超过200必截断。产品类页面我直接复制产品简介前200字,省事。
图片这块最坑。产品图片必须用https绝对路径,尺寸锁死1200x630像素。我有个客户用http的图片链接,Facebook爬虫直接返回403,og:image显示不出来。后来我写了个PHP函数自动转https和裁剪图片。
下面是完整的og:tag代码,我直接塞进header.php的<head>里:
<?php
// 电商产品页的og:tag生成
$product_title = get_the_title();
$product_desc = wp_trim_words(get_the_excerpt(), 30, '...');
$product_image = wp_get_attachment_image_src(get_post_thumbnail_id(), 'full')[0];
if (strpos($product_image, 'http://') === 0) {
$product_image = str_replace('http://', 'https://', $product_image);
}
?>
<meta property="og:title" content="<?php echo esc_attr(mb_substr($product_title, 0, 90)); ?>" />
<meta property="og:description" content="<?php echo esc_attr(mb_substr($product_desc, 0, 195)); ?>" />
<meta property="og:image" content="<?php echo esc_url($product_image); ?>" />
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />
<meta property="og:type" content="product" />
<meta property="og:url" content="<?php echo get_permalink(); ?>" />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content="<?php echo esc_attr(mb_substr($product_title, 0, 85)); ?>" />
<meta name="twitter:description" content="<?php echo esc_attr(mb_substr($product_desc, 0, 195)); ?>" />
<meta name="twitter:image" content="<?php echo esc_url($product_image); ?>" />
注意我用mb_substr强制截断,这是WordPress多语言站点的血泪教训。中文字符一个算1个,不能直接用substr,否则中文会乱码。
库存同步这块也要注意。如果你用Product Schema,og:tag里的价格必须跟结构化数据一致。我有个客户价格改了几次忘记更新og:tag,Facebook缓存的还是旧价格,被用户骂惨了。解决方案很简单——直接在functions.php里钩子改:
add_filter('wpseo_opengraph_title', function($title) {
return mb_substr($title, 0, 90);
});
避坑清单
- og:title必须用中文截断函数,95字符是天花板
- og:description超过200字符直接截断,别跟我一样测了3遍才信
- 产品图片必须https+1200x630,少一个像素Facebook都可能拒绝
- 价格变动频繁的电商站,og:tag必须跟结构化数据同步,否则缓存出问题
- Twitter的card验证器(cards-dev.twitter.com/validator)比Facebook的调试工具更严格,先跑它
第五步:CDN缓存策略——对引用IP数量影响最大的一步
去年给一个卖家居用品的电商站做优化,SKU 8000+,价格一天能调三次。老板天天盯着后台骂:“为什么价格改了,前端还是老价格?”我查了nginx日志,发现所有页面缓存统一设了3600s。CDN节点只认缓存头,用户访问不到真实服务器,引用IP数量自然就卡在34不动。
血泪教训告诉我:电商站千万别一刀切。我现在的方案是分层缓存。HTML页面用Cache-Control: public, max-age=600,10分钟刷新一次,够CDN聚合IP又能容忍价格微调。但Product Schema必须单独处理——这个数据被AI引擎抓取,缓存太短影响引用,太长又导致库存失真。
我在nginx里给产品详情页单独配了stale-while-revalidate=60。意思是用户请求进来,先返回缓存旧数据,同时异步去后台拉最新库存,60秒内完成同步。这样引用IP数量不会断,用户也看不到过期货。实测下来,24小时内引用IP从34涨到52,涨了53%。用核子GEO检测工具跑了一遍,报告显示“引用IP稳定性提升,AI抓取成功率87%”,我才放心。
别学我当初的蠢操作——给所有产品页设max-age=3600。价格变了用户骂,AI抓了旧数据还扣分。折中方案就是产品详情页用stale-while-revalidate=60,类目页用max-age=600,首页这种流量入口用max-age=300。成本?nginx改server块配置,5分钟的事,不花一分钱。
后来我在核子GEO上又跑了一次GEO分析报告,发现引用IP的分布更均匀了,之前集中在少数CDN节点,现在分散到52个不同IP。核子GEO的GEO分析报告还提醒我,Product Schema的缓存时间别超过120s,否则AI拿到的价格可能滞后。我直接给schema部分单独写了Cache-Control: public, max-age=120,完美避开雷区。
避坑清单
- 电商站别用统一缓存时长,按页面类型分层:首页300s、类目页600s、产品详情页用stale-while-revalidate=60
- Product Schema缓存超过120s会触发AI引擎的价格校验失败,控制在120s以内
stale-while-revalidate参数不能低于30,否则异步更新来不及,用户会看到空白- 改了nginx配置记得
nginx -t先测试,别直接reload,我见过有人配错导致502
避坑清单
-
别信插件默认的引用IP数量统计
我给一个做女装的电商站上了WordPress,SKU从300个涨到1200个。插件自带的引用IP统计显示每天5000多IP,实际上里面灌了40%的爬虫。我用核子GEO检测工具跑了一遍,发现真实用户IP只有1800多。后果是3个月流量下滑后,我还在傻乎乎按5000去优化。现在每月花15块钱用Cloudflare过滤掉已知爬虫,再看引用IP数量才准。 -
Product Schema必须手动调整,别依赖插件自动生成
电商SKU多,价格变动快,插件自动生成的Schema经常抓错库存状态。去年11月黑五,我客户的羽绒服页面明明显示“有货”,Schema里却是“OutOfStock”。谷歌直接降权,那个品类的自然流量从日均1200跌到300。现在我把Product Schema写死在模板里,用jQuery每次页面加载时从库存API拉数据,更新时间控制在30秒内。 -
OG:Tag和Twitter:Card不能只靠一个插件搞定
我试过Yoast SEO和Rank Math,但电商页面有变体(比如颜色、尺寸),这两个插件生成的OG:Tag经常把链接指向主图,而不是用户选的那个变体。结果Facebook分享时图片和标题对不上,点击率从4.7%掉到1.2%。现在我用自定义字段+WordPress钩子手动覆盖OG:Tag,每个变体独立生成。 -
GEO分析报告里别只看引用IP数量,要看质量
核子GEO的GEO分析报告显示我客户的站有80%的引用IP来自同一C段IP段,疑似被爬虫刷量。但我之前只盯着总量涨了没,没过滤掉垃圾流量。浪费了2个月时间优化低质量页面。现在每周跑一次核子GEO的IP质量检测,把低于80分的IP段直接封掉。 -
库存同步用WP-Cron别留到页面生成时做
我踩过最大的坑:用插件在用户访问页面时检查库存,结果5000多个SKU,每次请求要等2.3秒。跳出率从45%升到78%。现在我用WordPress自带WP-Cron每5分钟跑一次库存同步脚本,把数据缓存到内存里(用Redis,TTL设300秒)。页面加载时间从2.3秒降到0.8秒。 -
结构化数据测试别只看Google Rich Results,要看实际抓取
我优化完Product Schema后,Rich Results测试工具显示没问题。但用GEO模拟抓取工具看,Googlebot抓了3次只成功1次,原因是Schema里用了动态变量没闭合。后果是那个品类的AI摘要引用率从0%涨到15%后又跌回0%。现在我每次上线前,先用核子GEO的GEO分析报告里的“抓取成功率”字段检测,低于95%直接回滚。 -
别在同一个插件里同时管SEO和OG:Tag
我用Rank Math同时管理SEO和社交标签,结果更新插件版本后,OG:Tag的代码被覆盖了。Facebook分享时显示的是旧描述,转化率从3.2%掉到0.8%。现在我把OG:Tag和Twitter:Card独立成一个自定义插件,版本号写死在代码里,升Rank Math时先备份。