双语个人博客 SEO 落地清单:Flask 里把 canonical、hreflang 和 sitemap 一次做对

很多个人博客做了中英文双语后,表面上看页面都能访问,实际上搜索引擎收到的信号却是混乱的:中文和英文文章互相抢 canonical,归档页和正文页 URL 写法不统一,sitemap 里既有首选地址也有重复地址,最后结果往往不是“两个语言版本都收录”,而是只留下一个版本,另一个版本长期不稳定。

对 Flask 博客来说,这件事并不复杂,难点在于别把几种机制混用错位。双语 SEO 的核心不是“多生成几页”,而是持续给搜索引擎明确、稳定、可重复验证的信号:每个语言版本有独立 URL、每个页面声明自己的 canonical、互译页面通过 hreflang 成对关联、站点地图输出的都是可公开抓取的首选地址。

如果你维护的是个人博客、小团队内容站,下面这套做法足够长期使用,而且和现有 Flask 模板、Nginx 反向代理、自动发布脚本都能对接。

1. 先把 URL 结构定死,别靠自动跳转切语言

双语站最常见的错误,是根据浏览器语言自动跳转,然后让同一篇内容在不同入口下返回不同语言。这种方式对用户也许友好,但对搜索引擎并不稳定。更稳妥的方案是给每个语言版本固定 URL,中文和英文都能被直接访问。

个人博客里建议像这样约定:

中文文章: /article/<slug>
英文文章: /en/article/<slug>
中文栏目: /category/tech
英文入口: 页面内切换,不做强制 302 语言跳转

Flask 路由可以明确拆开:

@app.route("/article/<slug>")
def article_detail(slug):
    return render_article_detail(slug, "zh")

@app.route("/en/article/<slug>")
def english_article_detail(slug):
    return render_article_detail(slug, "en")

这样做有两个好处。第一,分享链接时不会因为访问者语言不同而跳到别的版本;第二,后续 canonical 和 hreflang 都有稳定目标,不需要猜“当前页面到底代表哪种语言”。

2. canonical 只指向当前语言版本的首选地址

canonical 的作用不是“告诉搜索引擎只有一个语言版本应该存在”,而是告诉它“当前这页的首选 URL 是什么”。如果中文正文把 canonical 指到英文页,或者所有变体都指到首页,基本等于把自己的收录信号主动打散。

模板里应该让每个正文页输出自己的绝对地址:

<link rel="canonical" href="{{ abs_article_public_url(article) }}">

这个地址要满足三点:必须是线上公开域名、必须是最终可访问地址、必须与页面实际语言一致。对于双语文章,中文页 canonical 指向中文页,英文页 canonical 指向英文页,不要互相覆盖。

如果你站点经历过 httphttps、IP 到域名、带 www 到不带 www 的迁移,还要在 Nginx 层把非首选地址 301 到主域名,否则模板里写得再对,外部链接和历史地址也会继续制造重复页。

server {
    listen 80;
    server_name stepnex.cn www.stepnex.cn 39.106.188.160;
    return 301 https://stepnex.cn$request_uri;
}

3. 用 translation_key 或等价字段把互译文章绑成一组

双语博客不能只靠“标题看起来像同一篇”来做关联。真正稳定的做法,是在数据层保留一个跨语言共享的键,比如 translation_key。中文和英文 slug 可以不同,但 translation_key 应该相同,这样模板才能准确生成 alternate 链接,后台也能知道哪两篇是互译关系。

数据查询逻辑可以保持很简单:

def get_article_translations(article):
    return (
        Article.query.filter_by(
            translation_key=article.translation_key,
            status="published",
        )
        .order_by(Article.language.asc(), Article.id.asc())
        .all()
    )

正文模板再输出 hreflang:

{% for item in get_article_translations(article) %}
  <link rel="alternate" hreflang="{{ item.language }}" href="{{ abs_article_public_url(item) }}">
{% endfor %}
<link rel="alternate" hreflang="x-default" href="{{ abs_article_public_url(article) }}">

对个人博客来说,zhen 已经够用。只有在你明确区分 zh-CNzh-TWen-USen-GB,并且页面内容真的按地区分化时,才有必要继续细化。

4. sitemap 只输出可索引的最终 URL,不要把重复入口一起塞进去

很多人一提到 sitemap,就想“页面越全越好”。实际更应该强调的是:放进去的 URL 必须是你希望被抓取和被视为首选的地址。若 sitemap 同时包含旧域名、测试域名、短链接、分页参数页,等于主动制造噪音。

Flask 里可以直接从数据库按已发布文章生成:

urls.extend(
    sitemap_item(
        abs_article_public_url(article),
        article.updated_at or article.published_at or article.created_at,
        "weekly",
        "0.8",
    )
    for article in Article.query.filter_by(status="published").all()
)

这里要注意三件事:

  1. 输出绝对 URL,不要输出相对路径。
  2. 只放线上主域名最终地址,不放短链接 /p/<id>
  3. 中英文页面各自都是独立 URL,就各自进 sitemap,但不要再额外放一份参数化重复地址。

同时在 robots.txt 里声明 sitemap 地址,方便搜索引擎发现:

Sitemap: https://stepnex.cn/sitemap.xml

5. 发布流程里顺手校验三类信号

双语 SEO 最大的问题不是“不会写标签”,而是发文后没人检查。建议把校验并入自动发布流程,每次发布后至少检查三件事。

第一,文章 URL 是否符合约定,例如中文以 -zh 结尾、英文以 -en 结尾,避免后台后来生成出难维护的随机 slug。

第二,页面源码里是否真的出现 canonical 和 alternate。很多站在本地模板里写了标签,但线上因为站点根地址配置错误,最后输出成了 http://127.0.0.1:5000/...,这种情况提交 sitemap 也没用。

第三,sitemap 是否已经包含新文章。若发布成功但 sitemap 还是旧内容,常见原因要么是缓存没刷新,要么是线上进程没重启。

一个轻量验证命令可以这样写:

curl -s https://stepnex.cn/article/flask-bilingual-blog-seo-canonical-hreflang-sitemap-zh | grep -E "canonical|alternate"
curl -s https://stepnex.cn/sitemap.xml | grep "flask-bilingual-blog-seo-canonical-hreflang-sitemap"

6. 这些坑最容易拖慢收录

第一种坑,是中文和英文正文内容几乎一样,只改了导航和少量词语。这样即使 hreflang 正确,搜索引擎也可能把其中一页视为弱重复内容。英文文章应该是自然重写,而不是逐句替换。

第二种坑,是 canonical、内部链接和 sitemap 三套信号互相打架。正文页写的是 https://stepnex.cn/...,但站内导航还在链向 http://39.106.188.160/...,收录就会变慢。

第三种坑,是发布脚本只校验接口返回成功,不检查最终页面。SEO 是结果导向,不是 API 导向。真正应该验证的是公开页面、公开 URL 和 sitemap,而不是日志里一行“publish ok”。

总结

双语个人博客的 SEO,不是多配几个 meta 标签,而是把 URL、canonical、hreflang、sitemap 和自动发布流程统一起来。对于 Flask 项目,最重要的顺序是:先固定分语言 URL,再保证 canonical 指向当前语言首选地址,用 translation_key 绑定互译文章,最后让 sitemap 和 robots.txt 只暴露最终可索引地址。

当这四步稳定后,中英文页面会更容易形成清晰的索引关系,后续无论你继续扩写 Python/Flask、Android/Kotlin 还是部署运维内容,搜索信号都会更干净,站点长期维护成本也会明显下降。