KEEL · 龙骨 · A CURRICULUM FOR THE AI ERA
07 · 全站切换:消灭所有 http 残留 — keel 龙骨
本章目标:站点里不再有任何一处硬编码的 http 链接。 验收标准:抓下来的 HTML/JSON 里 http:// 出现次数为 0,浏览器控制台无混合内容告警。
本章目标:站点里不再有任何一处硬编码的 http 链接。
验收标准:抓下来的 HTML/JSON 里http://出现次数为 0,浏览器控制台无混合内容告警。
为什么单独一章:HTTPS 配好之后页面"能打开",但如果页面内部还写着 http://,会产生三类问题——混合内容被浏览器拦截(资源加载失败)、canonical/og:url 指向 http 版本(SEO 权重分裂)、重定向链多一跳(性能与抓取预算浪费)。这些问题不会让站点挂掉,只会让它一直不对劲。
一、残留藏在哪:六类常见位置
一次真实的迁移里,我们改了 11 处。它们分布在:
| 类型 | 例子 | 后果 |
|---|---|---|
| 构建脚本默认值 | PUBLIC_SITE_URL=http://www.xxxxxx.cn |
生成的 canonical/sitemap 全是 http |
| 前端常量 | api/paths.ts 里的 base URL |
请求发到 http,被 301 后被浏览器拦(POST 更糟) |
index.html 模板 |
og:image / canonical 写死 |
分享卡片与收录指向 http |
| SSR 预渲染脚本 | 生成 route shell 时拼的绝对 URL | 爬虫抓到的正文里全是 http 链接 |
| 内容生成脚本 | 文章内链、sitemap.xml | 站点地图提交后被判为无效 |
| 后端配置/邮件模板 | 重置密码链接 | 功能直接不可用 |
规律:凡是"生成绝对 URL"的地方,都有一个来源常量。找出这个常量统一改,比逐个改字符串靠谱得多。
二、怎么把它们全部找出来
搜索(本地源码)
# 排除 lock 文件、构建产物与依赖目录
grep -rn "http://www\.xxxxxx\.cn" \
--include="*.ts" --include="*.tsx" --include="*.mjs" --include="*.js" \
--include="*.py" --include="*.html" --include="*.json" --include="*.sh" \
--exclude-dir=node_modules --exclude-dir=dist --exclude-dir=.git .
记得也搜一下裸域名与 IP:
grep -rn "http://\(www\.\)\?xxxxxx\.cn\|http://203\.0\.113\.10" --exclude-dir=node_modules .
验证(线上产物)
搜源码只能保证"源头干净",真正要验的是发布出去的东西:
# 首页 HTML 里还有没有 http
curl -s https://www.xxxxxx.cn/learning/ | grep -o 'http://[^"'"'"' ]*' | sort -u
# sitemap 必须全是 https
curl -s https://www.xxxxxx.cn/learning/sitemap.xml | grep -c 'http://'
# robots.txt 里的 Sitemap 行
curl -s https://www.xxxxxx.cn/robots.txt
# 预渲染的 SSR 页面(爬虫看到的那一份)
curl -s https://www.xxxxxx.cn/learning/courses/tools/lessons/1/ | grep -o 'http://[^"'"'"' ]*' | sort -u
期望:全部为空。 只要还有一处,就说明某个生成环节没被覆盖。
三、混合内容(Mixed Content)怎么查
浏览器加载 HTTPS 页面时,页面内的 http 子资源会被拦截:
- 被动内容(图片/音频/视频):被拦截,控制台告警,图片不显示
- 主动内容(脚本/样式/iframe/XHR):直接阻断,功能失效
排查方法
- 打开页面 → F12 → Console,看 "Mixed Content" 警告,它会给出具体 URL。
- 批量检查:
curl -s https://www.xxxxxx.cn/learning/ \ | grep -oE '(src|href)="http://[^"]*"' | sort -u
三类解法
<!-- 1. 最推荐:改成协议相对或 https 绝对地址 -->
<img src="https://www.xxxxxx.cn/learning/assets/logo.png">
<!-- 2. 第三方资源不支持 https → 换源,或代理到自己域名下 -->
<!-- 3. 临时兜底(不推荐,只是把问题藏起来):
upgrade-insecure-requests 会让浏览器自动把 http 请求升级为 https -->
<meta http-equiv="Content-Security-Policy" content="upgrade-insecure-requests">
注意 upgrade-insecure-requests 的副作用:如果某个第三方资源确实只有 http,升级后会直接 404/失败,比不升级更糟。它适合作为上线过渡期的保险,而不是最终方案。
四、后端与代理:别让应用自己以为在 http 下
nginx 把请求转发给后端时,后端看到的是 http://127.0.0.1:5177。如果应用根据这个生成绝对 URL,产出的必然是 http。两个层面的修法:
① nginx 传递真实协议(第 06 章配置里已有):
proxy_set_header X-Forwarded-Proto $scheme;
② 应用侧信任这个头。以 FastAPI/uvicorn 为例:
# uvicorn 默认 proxy_headers=True 会改写 request.client,
# 如果你有自己的可信代理判定(TRUSTED_PROXY_IPS),必须关掉它:
uvicorn.run(app, host="127.0.0.1", port=5177, proxy_headers=False)
否则 uvicorn 自带的中间件会覆盖 request.client,让你基于 IP 的可信代理判断失效——表现为"同一份代码,本地对、线上不对"。
③ 站点 URL 用配置而非推导。最稳的做法是显式配置:
PUBLIC_SITE_URL=https://www.xxxxxx.cn
让所有生成绝对 URL 的地方都读这一个变量,而不是从请求头推导。推导逻辑在多层代理下极易出错,且难以测试。
五、切换后的回归检查清单
按这个顺序跑一遍,全绿才算完成:
DOMAIN=www.xxxxxx.cn
# 1. 首页与一个内页都 200
curl -s -o /dev/null -w "%{http_code}\n" https://$DOMAIN/learning/
curl -s -o /dev/null -w "%{http_code}\n" https://$DOMAIN/learning/courses/tools/lessons/1/
# 2. canonical / og:url 是 https
curl -s https://$DOMAIN/learning/ | grep -oE '<link rel="canonical"[^>]*>'
curl -s https://$DOMAIN/learning/ | grep -oE 'property="og:url"[^>]*'
# 3. sitemap 与 robots 可访问且为 https
curl -s -o /dev/null -w "%{http_code}\n" https://$DOMAIN/learning/sitemap.xml
curl -s https://$DOMAIN/robots.txt
# 4. 页面内无 http 残留
curl -s https://$DOMAIN/learning/ | grep -c 'http://' || echo "0 (干净)"
# 5. 接口请求也走 https(前端 network 面板确认,或直接打 API)
curl -s -o /dev/null -w "%{http_code}\n" https://$DOMAIN/learning/api/health
- 首页/内页/API 全部 200
- canonical 与 og:url 均为 https 且指向 www 版本
- sitemap.xml 可被访问,内容全为 https
- robots.txt 的
Sitemap:行是 https - HTML 中
http://出现 0 次 - 浏览器 Console 无 Mixed Content 警告
- 移动端(真机)打开正常,无资源加载失败
六、一个真实教训:改了源码不等于改了线上
HTTPS 全量切换时遇到过这样一个问题:本地把所有 http:// 改成 https:// 并提交,但线上抓下来仍是 http。
原因:发布脚本只发布"内容"(Markdown → JSON),不发布前端源码。前端的构建发生在服务器上,用的是服务器目录里那份旧源码。所以:
- 改内容(文章)→ 发布脚本会带上
- 改前端代码 / 构建配置 → 必须单独同步源码到服务器并重新构建
判断方法:抓一个带版本特征的东西。例如新版本的 HTML 里会出现新组件的类名、新的 JS chunk 文件名:
curl -s https://www.xxxxxx.cn/learning/ | grep -oE 'assets/[A-Za-z0-9._-]+\.js'
对比本地 dist/assets/ 里的文件名。对不上,说明线上跑的是旧构建产物——此时再怎么改本地代码也不会生效。
下一章处理最后一个环节:证书的生命周期管理与到期前该做的事。