ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

PHP8.5配置多语言SEO优化怎么实现

PHP8.5配置多语言SEO优化怎么实现 前言多语言站点上线后最常见的一类反馈是「我们的英文版怎么搜不到」打开搜索引擎查一下只有中文页被收录或者更尴尬——搜中文关键词进来的是英文页标题和描述还都是机翻的半成品。这类问题的根因通常不在翻译质量而在搜索引擎无法确定页面之间的语言对应关系。对搜索引擎来说两套内容不同、URL 不同的页面就是两个互不相干的页面。如果它们之间没有任何声明搜索引擎既不知道它们是同一内容的两个语言版本也不知道该给哪类用户展示哪一个。结果就是默认语言的页面被反复抓取其它语言版本长期停留在「已发现但未收录」。把这件事讲清楚需要三块内容URL 结构怎么定、语言对应关系怎么声明hreflang 与 x-default、PHP 侧怎么把这三件事稳定地产出路由、语言识别、元信息生成、缓存。本文基于PHP 8.5给出可直接落地的实现代码最低要求PHP 8.0如果用到Locale::acceptFromHttp()则需要intl扩展。一、URL 结构先定骨架再谈优化多语言站点的 URL 有三种主流结构它们的取舍非常清晰结构示例形态优点代价子目录域名后加/en/前缀一套域名一套证书权重集中最易维护无法针对地区做独立服务器子域名en.前缀子域名可分离部署与扩容权重分散需各自配证书与 CDN独立域名各国顶级域名对本地用户信任度最高成本最高权重最分散对绝大多数项目子目录是默认答案。它只有一个域名一次 SEO 投入可以沉淀到同一个域名上运维成本也最低。本文的实现基于子目录结构。定了结构之后另有一条必须遵守的纪律同一语言只允许一个规范 URL。要明确回答这些问题并且用 301 强制统一前缀大小写/EN/一律跳转到/en/。结尾斜杠/en与/en/只能留一个另一个 301 过去。默认语言要不要前缀/zh-cn/article/1和/article/1不能同时存在二者必须二选一。这三条不统一同一个页面就会有多个 URL 变体收录状态会一直很混乱。二、语言代码不要用zhhreflang 的值遵循「语言-地区」的写法语言部分用 ISO 639-1 两字母代码地区部分用 ISO 3166-1 Alpha-2 两字母代码。写zh只有语言、没有地区是非常常见的错误——它无法区分简体与繁体也无法告诉搜索引擎该给哪一类使用者展示。想表达的目标不推荐推荐说明简体中文中国大陆zhzh-CN最常用的写法繁体中文台湾地区zhzh-TW用地区区分中文按书写系统区分zhzh-Hans/zh-Hant书写系统子标签跨地区时更合适英文全球en-USen不限地区时用纯语言代码英文英国enen-GB有拼写差异时用地区区分另外还有一个特殊值x-default它表示「以上语言都不匹配时展示的页面」通常指向语言选择页或默认语言首页。缺少x-default时来自未覆盖地区的用户可能被送到任意一个语言版本。在 PHP 侧把所有语言元信息集中成一份配置是避免「模板里散落各种语言字符串」的第一步?php declare(strict_types1); /** * 语言配置表URL 前缀 元信息。 * 三项必须对齐URL 前缀、hreflang 值、页面语言属性。 */ const SUPPORTED_LANGS [ zh-cn [hreflang zh-CN, label 简体中文], zh-tw [hreflang zh-TW, label 繁體中文], en [hreflang en, label English], ja [hreflang ja, label 日本語], ]; const DEFAULT_LANG zh-cn; const SITE_ORIGIN https://example.com;三、hreflang 的三条铁律声明语言对应关系的主流元素是hreflang。搜索引擎支持三种声明位置页面内的链接元素、HTTPLink响应头、以及站点地图。三者任选其一即可不要重复声明重复反而容易产生冲突信号。无论用哪种方式都必须满足三条规则双向互指。A 页面声明了「我的英文版是 B」那么 B 也必须声明「我的中文版是 A」。单向声明会被直接忽略。这条是最容易漏的尤其是新增语言时只改了新页面。自引用。每个页面都要声明自己即 A 页面里包含一条指向 A 自己的hreflang。缺了它搜索引擎可能认为这一组声明不完整。x-default 唯一。一组页面里只应有一条x-default指向语言选择页或默认语言首页。再加一条工程纪律hreflang里的 URL必须与页面的规范 URL 完全一致——协议、域名、路径、结尾斜杠、大小写都要对上。https写成http或者少了一个结尾斜杠都会被当成指向另一个页面整组声明随之失效。下面这段代码负责产出「当前页面的所有语言版本」它把三条铁律直接编码进了逻辑里?php declare(strict_types1); /** * 生成当前页面的全部语言版本声明。 * * param string $pathWithoutLang 不带语言前缀的路径必须以 / 开头例如 /article/42/ * return array 每一项含 lang 与 url 两个键 */ function buildAlternates(string $pathWithoutLang): array { $path / . ltrim($pathWithoutLang, /); $out []; foreach (SUPPORTED_LANGS as $prefix $meta) { // 自引用 双向互指每个语言版本都列全所有语言天然满足双向要求 $out[] [ lang $meta[hreflang], url sprintf(%s/%s%s, SITE_ORIGIN, $prefix, $path), ]; } // x-default 指向默认语言首页 $out[] [ lang x-default, url SITE_ORIGIN . / . DEFAULT_LANG . /, ]; return $out; }四、语言识别静态命中优先兜底要克制用户第一次访问时该给他哪个语言版本原则是URL 优先其次是用户的历史选择最后才考虑浏览器语言。?php declare(strict_types1); /** * 从请求 URI 中取出语言前缀。识别不到就返回 null。 */ function detectLangFromPath(string $requestUri): ?string { $path parse_url($requestUri, PHP_URL_PATH); if (!is_string($path)) { return null; } if (preg_match(#^/([a-z]{2}(?:-[a-z]{2})?)(?:/|$)#i, $path, $m) ! 1) { return null; } $prefix strtolower($m[1]); // 必须做白名单校验否则 /blog/ 会被当成语言前缀 blog 的前两位 return isset(SUPPORTED_LANGS[$prefix]) ? $prefix : null; } /** * 解析 Accept-Language 请求头。 * Locale::acceptFromHttp 需要 intl 扩展返回的形如 zh_CN下划线分隔。 */ function pickLangFromAcceptHeader(string $acceptLanguage): string { if ($acceptLanguage ) { return DEFAULT_LANG; } $candidate class_exists(Locale) ? (string) Locale::acceptFromHttp($acceptLanguage) : ; // intl 返回的是下划线形式zh_CN配置表用的是连字符zh-cn必须归一化 $normalized strtolower(str_replace(_, -, $candidate)); if (isset(SUPPORTED_LANGS[$normalized])) { return $normalized; } // 只匹配到语言部分时退回到该语言的默认地区版本 $langOnly explode(-, $normalized)[0]; foreach (SUPPORTED_LANGS as $prefix $meta) { if (explode(-, $prefix)[0] $langOnly) { return $prefix; } } return DEFAULT_LANG; }关于「要不要自动跳转」这里有一条容易踩的规矩不要根据用户 IP 或浏览器语言把已带语言前缀的页面跳转到另一个语言。搜索引擎的抓取器大多不带明确的语言偏好一旦跳转它可能永远抓不到你精心准备的语言版本而已经通过搜索点进来的用户也会被「甩」到另一个语言页面。稳妥的策略是请求处理带有效语言前缀直接渲染不做任何跳转不带前缀的根路径/可用 302 依据浏览器语言跳到对应语言不带前缀的深层路径301 到默认语言版本不要按语言协商五、把元信息接进页面多语言 SEO 出问题的另一半原因是正文翻译了但标题与描述没翻译。每套语言都应有一份完整的元信息并且描述要针对该语言重新写而不是机翻拼接。?php declare(strict_types1); // 翻译内容放在独立文件里键名是业务语义而不是中文原文 // lang/zh-cn/article.php [title 文章详情, desc ……] // lang/en/article.php [title Article, desc ...] function t(string $lang, string $file, string $key): string { static $cache []; $path __DIR__ . /lang/{$lang}/{$file}.php; if (!isset($cache[$path])) { $cache[$path] is_file($path) ? (array) require $path : []; } return (string) ($cache[$path][$key] ?? ); } function renderPage(string $lang, string $pathWithoutLang, array $meta): void { $canonical sprintf(%s/%s%s, SITE_ORIGIN, $lang, $pathWithoutLang); // 关键一canonical 指向「本语言自己」绝对不要所有语言都指向默认语言 header(Content-Type: text/html; charsetUTF-8); header(Content-Language: . SUPPORTED_LANGS[$lang][hreflang]); // 关键二把 alternates 交给模板层渲染成 hreflang 声明页面内或站点地图均可 $alternates buildAlternates($pathWithoutLang); // 关键三缓存键必须带语言否则不同语言会互相污染 $cacheKey page: . $lang . : . md5($pathWithoutLang); printf( lang%s\ncanonical%s\ncacheKey%s\nalternates%d\n, SUPPORTED_LANGS[$lang][hreflang], $canonical, $cacheKey, count($alternates) ); printf(title%s\ndesc%s\n, $meta[title], $meta[desc]); } // 组装一次完整请求 $uri /zh-cn/article/42/; $lang detectLangFromPath($uri) ?? pickLangFromAcceptHeader($_SERVER[HTTP_ACCEPT_LANGUAGE] ?? ); $path preg_replace(#^/[a-z]{2}(?:-[a-z]{2})?#i, , (string) parse_url($uri, PHP_URL_PATH)) ?: /; renderPage($lang, $path, [ title t($lang, article, title), desc t($lang, article, desc), ]);另外别忘了结构化数据JSON-LD。它的字段名不随语言变化但headline、description这类内容字段必须跟随当前语言同时用inLanguage标注当前语言。如果所有语言版本共用同一份中文结构化数据搜索引擎拿到的就只是一份自相矛盾的信号。常见坑点1. hreflang 单向声明❌ 只在中文页声明英文版地址英文页没声明中文版 —— 整组声明被忽略英文页依旧不收录。 ✅ 所有语言版本都列出完整的语言清单如buildAlternates()的做法天然满足双向互指。2.canonical全部指向默认语言❌ 每个语言版本的页头里都写死默认语言的规范地址 —— 搜索引擎会认为英文页是中文页的重复内容英文页被剔除出索引。 ✅canonical指向本语言自己的那一个 URL。3. 用Locale::acceptFromHttp()的返回值直接匹配配置❌Locale::acceptFromHttp(zh-CN,zh;q0.9)返回的是zh_CN下划线分隔拿它和配置里的zh-cn比较永远不相等于是所有用户都被判定为默认语言。 ✅ 归一化strtolower(str_replace(_, -, $candidate))之后再匹配。4. 用正则判断语言前缀却不做白名单❌preg_match(#^/([a-z]{2})/#, $path, $m)之后直接当作语言使用 ——/blog/list会被解析出前缀bl页面直接 404 或者渲染出空白。 ✅ 取出候选前缀后必须用支持列表做白名单校验。5. 按浏览器语言把深层页面重定向到别的语言❌ 用户点开/en/article/42却因为浏览器语言是中文被 302 到/zh-cn/article/42搜索引擎抓到的永远是同一个版本。 ✅ 带语言前缀的 URL 一律原样渲染只在根路径做语言协商。6. 缓存键里没有语言❌ 页面缓存使用md5($path)作为键 —— 中文用户先访问后英文用户拿到的是中文页面且这个错误状态会被缓存到过期。 ✅ 缓存键形如lang pathCDN 侧也要把语言前缀纳入缓存维度或确保 URL 本身被当作键。7. 同一语言存在多个 URL 变体❌/EN/、/en、/en/同时可访问且都返回 200 —— 索引里出现多个重复页面权重被稀释。 ✅ 用 301 把非规范形式统一跳转到唯一形式并在代码里集中定义 URL 生成函数禁止别处手拼。8. 只翻译正文标题与描述保持原样❌ 英文页的标题仍是中文搜索结果里显示一行中文标题 —— 点击率极低用户也会怀疑页面内容不对。 ✅ 每种语言的title/description单独维护描述要重写而不是机翻。总结环节做法不做的后果URL 结构子目录 唯一规范形式重复内容权重分散语言代码zh-CN而非zh无法区分简繁与地区对应关系互指 自引用 x-default语言版本不被收录规范地址canonical指向本语言自身非默认语言页被判定为重复语言识别路径优先白名单校验误判、404、错误跳转元信息标题描述逐语言维护搜索结果展示错语言缓存缓存键包含语言跨语言内容串台多语言 SEO 的工程量其实很小难的是一致性语言的每一种表示形式URL 前缀、hreflang 值、页面语言属性、缓存键都必须从同一份配置派生。把这些字符串散落在模板和控制器里早晚会出现某处写成zh而另一处写着zh-CN的情况而这类不一致恰恰是搜索引擎最不宽容的地方。
返回列表