使用Nginx完成动态SEO配置
使用 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 判断,分别执行替换(域名和内容已脱敏,替换成自己的即可):
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 直接启动失败:
"sub_filter" directive is not allowed here查阅文档后确认了原因:

sub_filter 的合法上下文是 http、server、location,不能写在 if 块内。
4. 改造:用 map 做域名到配置的映射
既然不能按域名走 if 分支,就换个思路——用 map 指令把 Host 映射成"要注入的内容",location 里只做一次无条件替换:
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. 验证效果
部署后分别访问两个域名,查看页面源码:


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