使用Nginx完成动态SEO配置

3 分钟阅读

使用 Nginx 完成动态 SEO 配置

1. 需求背景

我们的产品以 SaaS 形式提供服务,每个客户使用自己的域名,且需要在各自的站点上添加特定的 SEO 信息(如 Google Search Console 的站点验证 meta)。

后台管理系统里虽然有动态 meta 的配置项,但对 SPA 来说这条路走不通:爬虫抓取的是服务端返回的静态 HTML,不会等待 JS 执行——JS 动态插入的 meta 标签对爬虫不可见。SEO 配置必须在 HTML 到达浏览器(爬虫)之前就已经存在。

2. 方案对比

围绕"让 meta 出现在首屏 HTML 里"这个目标,我们整理了两个方案:

方案一:后端托管页面,模板注入

前端打包后的资源上传到 S3,后台项目提供一个接口读取 S3 上的页面,并结合后台配置把 SEO 条目注入后返回。

  • 优点:可以直接复用后台现有的配置能力,灵活
  • 缺点:需要改造后端,侵入后端代码;用 Java 服务返回静态文件,服务端压力大时还会拖慢前端页面加载

方案二:Nginx 按域名替换内容

通过 Nginx 的 sub_filter 指令,根据请求的 Host 把对应的 SEO meta 替换进 HTML。

  • 优点:不侵入任何业务代码,改动成本极低
  • 缺点:无法通过后台配置,每次修改都要改 Nginx 配置并重新走部署流程。好在前端部署在 Docker 里,虽然要过一遍 CI/CD,但由于前端代码没有变化,构建产物的哈希不变,用户侧不会触发资源重新加载

考虑到 SEO 配置改动频率很低,我们最终选择了方案二——实施成本最低,且不需要任何后端排期。

3. 第一次尝试:sub_filter 写在 if 里(失败)

直觉的写法是在 location 里按域名 if 判断,分别执行替换(域名和内容已脱敏,替换成自己的即可):

nginx
server {
    listen 80 default_server;
    server_name _;
    root /bh/www ;
    index index.html index.htm;

    location / {
        if ($host ~ "new.xxx"){
            sub_filter '<head>' '<head><meta name="google-site-verification" content="xxx" />';
            sub_filter_once on;
        }
        if ($host ~ "xxxxxx"){
            sub_filter '<head>' '<head><meta name="google-site-verification" content="xxx" />';
            sub_filter_once on;
        }

        try_files $uri $uri/ /index.html;
    }

    location ^~ /static/ {
      gzip_static on;
      expires max;
      add_header Cache-Control public;
    }
    error_page 404 /index.html;
    client_max_body_size 20M;
}

sub_filter 指令接受两个参数:要替换的字符串,以及替换后的内容。提交部署后 Nginx 直接启动失败:

text
"sub_filter" directive is not allowed here

查阅文档后确认了原因:

sub_filter 指令上下文限制

sub_filter 的合法上下文是 http、server、location,不能写在 if 块内。

4. 改造:用 map 做域名到配置的映射

既然不能按域名走 if 分支,就换个思路——用 map 指令把 Host 映射成"要注入的内容",location 里只做一次无条件替换:

nginx
map $host $conditional_filter {
        ~xxx      '<head><meta name="google-site-verification" content="xxx" />';
        ~xxx.app   '<head><meta name="google-site-verification" content="xxx" />';
        ~xxxxxx  '<head><meta name="google-site-verification" content="xxx" />';
        default '<head><meta name="google-site-verification" content="xxx" />';
}

server {
    listen 80 default_server;
    server_name _;
    root /bh/www ;
    index index.html index.htm;

    location / {
        sub_filter "<head>" $conditional_filter;
        sub_filter_types *;
        sub_filter_once off;
        try_files $uri $uri/ /index.html;
    }

    location ^~ /static/ {
      gzip_static on;
      expires max;
      add_header Cache-Control public;
    }
    error_page 404 /index.html;
    client_max_body_size 20M;
}

改造后的逻辑:map 根据 $host 解析出当前域名对应的 <head> 替换文本(未匹配到时走 default),sub_filter 用变量完成替换。这里还顺手调整了两个参数:

  • sub_filter_types *:对任意 Content-Type 生效,默认只对 text/html 生效,显式声明避免歧义
  • sub_filter_once off:允许替换多次出现。我们的场景 <head> 只出现一次,这个参数影响不大,按需设置即可

5. 验证效果

部署后分别访问两个域名,查看页面源码:

域名 A 的 meta 注入结果

域名 B 的 meta 注入结果

不同域名返回了各自对应的 meta 信息,需求完成。

参考资料

前后篇
← NEWER浏览器的工作原理(一): 用状态机解析HTTP响应OLDER →Vue3+webpack实现组件远程加载
评论