Shopify 多语言子路径产品页对 Googlebot Fallback 的真因诊断
站点:KryoZon (www.kryozon.com),Shopify Horizon 主题,Markets 子路径多语言 持续时间:约 4 小时(从首次发现到 worker 上线 + 验证全绿) 修复成本:1 个 Cloudflare Worker(约 80 行 JS)+ 1 次 DNS proxy 切换 + 0 元运营成本
一、起点:一个监控脚本暴露的怪事
为闭环验证多语言子路径是否真的渲染本地化(之前只在 Admin API 层用 detect_hidden 检查 translation 数据完整度),写了 i18n_live_diff.js:模拟 user 浏览器 + Googlebot 两种客户端,分别带和不带 Accept-Language 抓 6 语种 × 5 关键 URL,检查 <html lang>、hreflang、canonical、英文 sentinel 命中。
第一次跑就抓到:
Verdict
- user: 30/30 clean
- googlebot: 24/30 (6 红:全是 /products/...)
所有产品页在 Googlebot 模式下回 <html lang="en"> + 英文内容 + canonical 指向英文 URL。
Homepage / blog list / collection / pages 都正常。只有 product template 出问题。
SEO 影响:Google 算法里 canonical 优先级 > hreflang。爬虫拿到产品页 fallback 响应会把 /ja/products/X 当成 /products/X 的副本处理 → 6 个 locale 子路径产品页可能根本不会被独立索引。这把多语言 SEO 投资废了。
二、4 轮错误归因(每一轮都”看似有理”)
轮次 1:怀疑 Googlebot UA 触发了反爬保护
最初设想 Googlebot UA 可能让 Shopify 走不同代码路径。
推翻方式:手测 4 个组合(Chrome/Googlebot UA × 有/无 Accept-Language),发现与 UA 完全无关。决定因素只是 Accept-Language 这个 header。
轮次 2:怀疑 product 翻译没注册全
理论:Translate & Adapt 里产品的 meta_title/body_html/meta_description 没填齐 → Shopify 找不到翻译只能回源语言。
实际行动:写脚本扫了全部 9 个产品 × 6 locale × 6 字段。发现 8 个活跃产品的 4 个主翻译字段(title / body_html / meta_title / meta_description)早就翻好了。唯一缺的是 product_type 字段(5 locale × 8 产品 = 40 条)。顺手把这 40 条补齐,再测 fallback —— 依然存在。
结论:翻译数据完整度跟 fallback 无关。
轮次 3:怀疑 Shopify Markets 配置有 webPresence 缺失
调 Admin GraphQL 看 Markets,发现店铺有 3 个 Market(US / International / Japan)。诡异之处:6 个语言子路径只挂在 Japan Market 的 webPresence 下,US 和 International 都 webPresence: null。
理论:US/International 没配子路径 → Cloudflare 默认 routing 把美国/欧洲 IP 的请求路由到这两个 Market → 它们不知道 /ja/ 是啥 → fallback。
实际行动:建议用户把 6 alternate locales 加到 US/International Market。用户走得更彻底,直接收敛到 1 个 International Market,里面挂全 7 languages 的 webPresence。再测 —— fallback 依然存在。
结论:Markets 配置不是关键。
轮次 4:怀疑 Shopify 产品模板架构本身的限制
理论:“产品页要查 Market-scoped 的价格/库存,必须走完整 Market 解析;客户端无 AL 时只信 Market 主语言。Admin 没 toggle 可改,是 Shopify 平台行为。”
提出 Cloudflare Worker 在边缘注入 Accept-Language 来绕过。
写好 worker.js 部署后测试 —— lang 还是 en。但 worker metrics 显示有 168 次调用,0 errors,CPU 1ms。worker 在跑,但没改成功。
加 debug response header x-kryozon-worker + x-kryozon-path 看 worker 内部状态:
ja-prod,noAL worker=no-locale path-seen=/products/... ← 期望 /ja/products/...
ja-prod,AL=ja worker=has-al:ja path-seen=/ja/products/...
诡异:同一个 URL,不带 AL 时 worker 看到的 pathname 没有 /ja/ 前缀,带 AL 时正常。
进一步加 redirect: 'manual' 测试:
noAL → status=302
Location: https://www.kryozon.com/products/X (英文 URL)
Shopify 真的会返 302 把 /ja/products/X 重定向到 /products/X。Node fetch 默认 follow redirects → 我看到的是第二跳的响应。
但 worker 又说 has-al:ja —— 表明它检测到了 AL header。我明明没发!
最后加 x-kryozon-al-in 暴露 worker 收到的实际 AL 值:
noAL request → al-seen=* ← 上游注入了通配符 "*"
真因终于现形:Shopify CF for SaaS(在 Shopify 自家 Cloudflare 账户,挂在 KryoZon 自定义域名 SSL 证书前面那一层)对所有无 AL 的请求自动注入 Accept-Language: *。Shopify origin 收到 * 解读为”客户端无语言偏好” → 产品页触发 302 fallback 到源语言 URL。
三、最终修复
把 worker 逻辑从”看到 AL 就放手”改成”判断 AL 是否真的偏好 URL 的 locale”:
function alPrefersLocale(al, locale) {
if (!al) return false;
const top = al.split(',')[0].split(';')[0].trim().toLowerCase();
return top === locale || top.startsWith(locale + '-');
}
// 主逻辑
const alIn = request.headers.get('accept-language');
const locale = detectLocale(url.pathname);
if (!locale) return passthrough();
if (alPrefersLocale(alIn, locale)) return passthrough();
// 否则覆盖:无 AL / AL=* / AL=en-US 等都进这里
const newHeaders = new Headers(request.headers);
newHeaders.set('accept-language', LOCALE_MAP[locale]);
return fetch(url, { method, headers: newHeaders, redirect: 'manual' });
部署条件(缺一不可):
wwwCNAME 切橙云(Cloudflare Proxied)—— 否则请求绕过 user CF,worker 永远不被触发- SSL/TLS 模式 = Full(Flexible 会无限重定向)
- Worker route =
www.kryozon.com/*,Failure mode = Fail open(worker 挂了请求直接给 Shopify,网站不挂)
验证结果:
30 pages × 2 modes (user / googlebot)
- user: 29 clean, 0 HIGH, 0 MED
- googlebot: 29 clean, 0 HIGH, 0 MED ← 修复前是 24 clean, 12 HIGH
- ✓ PASS
四、关键教训(为什么这么难诊断)
1. Cloudflare for SaaS 是隐形的一层
Shopify 用 Cloudflare for SaaS 给所有自定义域名签 SSL 证书。这意味着流量路径是:
浏览器 → Shopify 自家 CF(CF for SaaS)→ [若用户启了自家 CF 代理] 用户 CF → Shopify origin
中间那一层 Shopify CF for SaaS 默认存在、不可见、文档没写、有副作用(注入 AL: *)。如果你不知道它存在,永远不会怀疑它。
判别方式:响应里的 cf-ray header。如果你的域名 DNS 是灰云(DNS only),但响应里有 cf-ray → 这个 cf-ray 来自 Shopify 的 CF 账户,不是你的。
2. Accept-Language: * 是个”未文档化”的奇葩值
HTTP 规范里 * 表示”any language is acceptable”,几乎没人显式发这个值。Shopify 把它当成”无偏好”信号,触发产品页 Markets fallback。
普通浏览器从不发 *,普通爬虫也不发。只有 CF for SaaS 这种中间层会”好心”加上。
3. 浏览器手测验证不了 SEO
整个 4 小时调试期间,无痕浏览器打开 /ja/products/X 看着永远是日文。因为:
- 浏览器默认发 Accept-Language(系统语言)
- Shopify CF for SaaS 不会覆盖客户端已有的 AL
只有不带 AL 的客户端(部分 Googlebot crawl 配置 / 某些 SEO 工具 / curl 默认)才能复现 fallback。这就是为什么 i18n_live_diff 必须模拟 googlebot 模式。
4. 多层系统调试要逐层确认状态
这个问题涉及的”层”:
- Shopify Admin API(translation data 完整度)
- Shopify Markets 配置(webPresence / locale routing)
- Shopify CF for SaaS(AL 自动注入)
- Cloudflare DNS(proxy on/off)
- Cloudflare Worker(route 是否生效 / 执行逻辑是否正确)
- Shopify origin(产品模板 locale resolver)
每一层都有自己的状态。错误归因 4 次的根本原因是:没有先把每一层的状态独立查清楚就推理。
第 4 轮加 debug header 的瞬间所有谜题都解开 —— 因为终于看到了”系统真实状态”而不是”我以为的状态”。
5. 错误归因的纠正成本
用户视角的整个过程:
- 轮次 1-2:Claude 让用户做了一堆事(补 product_type 翻译 40 条),结果跟 fallback 无关
- 轮次 3:Claude 让用户改 Markets(用户实际收敛到 1 个 Market 是大动作)
- 轮次 4:才挖到真因
期间每一轮 Claude 都更新了 memory 写”这就是真因”,每一轮都被推翻。
教训给 AI 协作者:不要在归因不稳定时急着写 memory。memory 是稳定知识沉淀,未充分验证的”理论”应该先 staging 在 handoff doc 里,验证通过才 promote 到 memory。
五、防回归
- 监控:每天跑
i18n_live_diff.js,结果写_live-diff-history.jsonl,趋势可查;HIGH 触发日报告警。 - Kill switch:worker 设
DISABLE=1环境变量瞬间 bypass;Failure mode = Fail open保证 worker 出 bug 时网站不挂。 - 状态文档:worker 改动 + DNS 切换 + SSL 模式三个前提条件写在
cloudflare-workers/inject-accept-language/README.md,未来任何人接手能直接看到部署条件。 - memory feedback:
shopify-locale-accept-language标记 RESOLVED,附完整真因 + 4 轮错误归因留档,避免下次再走弯路。
六、技术栈快速参考
- 调试入口:
E:/cursor/shopify/kryozon-i18n/scripts/i18n_live_diff.js - 真因定位:
E:/cursor/shopify/kryozon-i18n/scripts/probe_h1pro_translations.js+probe_markets_config.js - 修复代码:
E:/cursor/shopify/cloudflare-workers/inject-accept-language/worker.js(v4) - 部署文档:同目录
README.md - 验证方法:
node i18n_live_diff.js(user + googlebot 双模式)
七、SEO 时间线预期
- Worker 上线后 0-7 天:Googlebot 缓存里还是旧的英文版,GSC 看不到变化
- 7-21 天:Googlebot 重新爬完小语种产品页,开始看到 impressions 涨
- 30 天:可以判断 SEO 收益是否显著
- 90 天:日文/德文/法文/西文/意文/波兰文产品页应该开始有自然流量