问题发现:搜索引擎眼里的我的文章页是什么
博客文章页面是 Vue 渲染的,一直怀疑收录不好但没实际验证过。这次我直接模拟 Googlebot 抓自己的文章页:
curl -A "Googlebot" https://stewie.fun/posts/daily-quiz-2026-09-01
拿到的 HTML 里面是这样的:
<body>
<h1 style="position:absolute;...">Stewie 的前端实验室...</h1>
<div id="app"></div>
<script type="module" src="/assets/index-CYJQMOWH.js"></script>
</body>
整个页面就 6.4KB,正文一个字都没有——全部内容要等浏览器下载 JS、执行 Vue、调 API、渲染 DOM 之后才出现。Google 理论上能跑 JS,但渲染队列排期长、渲染预算有限;Bing 对 JS 的支持一直是半吊子;百度更弱,基本只看初始 HTML。结果就是:文章页在百度和 Bing 眼里约等于不存在。
一个容易忽略的坑:隐藏 H1 救不了内容
我之前在源码里塞了个视觉隐藏的 H1,想着至少让 Bing 站长工具能读到 H1。事实证明这只能骗过"检测 H1 是否存在"这类浅层检查,救不了内容收录——正文、内链、结构化数据全都不在源码里,搜索引擎无从索引。
三条路的取舍:Nuxt、预渲染、后端渲染
方案一:重构成 Nuxt 3(SSR/SSG)。最正统,但对我是下下签:整个 Vue3 + TS + Vite 项目迁移,构建链路、路由、部署全换,还要长期维护一个 Node 渲染服务。我的博客本身就是 Vue3 技术栈的展示品,换框架太麻烦了。
方案二:构建时预渲染。只适合页面数量固定的官网。我的文章天天在加,每发一篇就得重新 build,构建产物里永远缺最新的——直接排除。
方案三:后端动态渲染(Dynamic Rendering)。让后端在「爬虫请求」时返回完整 HTML,「用户请求」照旧走 SPA。这是 Google 官方认可的手段,红线只有一条:给爬虫和给用户看的内容必须同源等价——同一篇文章数据、同一份正文,只是省了 JS 渲染步骤。只要不往爬虫版塞正文里没有的东西,就不是 cloaking(隐蔽伪装),不会被惩罚。
我的后端是 Spring Boot,文章正文本来就以 HTML 字符串存在数据库里(前端拿去 v-html 渲染的同一份数据),后端直接吐 HTML 几乎是白捡的——最终选它。
实现架构:Nginx 按 UA 分流
整体链路:
爬虫请求 /post/{slug}
→ Nginx map $http_user_agent 命中爬虫名单
→ 内部改写到 Spring Boot(Thymeleaf 渲染)
→ 返回完整 HTML(meta + JSON-LD + 正文 + 评论区)
人类请求同一 URL
→ UA 不匹配
→ try_files /index.html
→ SPA 接管,体验与之前完全一致
Nginx 侧几个关键设计
- 爬虫名单用 map 管理:Googlebot、bingbot、Baiduspider、Sogou、360、Bytespider 之外,把 AI 爬虫也放进来了——GPTBot、ClaudeBot、anthropic-ai、PerplexityBot、CCBot、Applebot、Amazonbot、meta-externalagent。这些是 AEO/GEO 的入口,AI 引擎抓不到内容就不会引用你。
- 内部改写用 error_page 418 手法而不是 if + rewrite:命中爬虫后先返回 418 再 error_page 转内部 location,配合
internal指令保证外部无法直接访问渲染端点——不然有人伪造 Googlebot UA 就能绕过前端刷接口。 - proxy_cache 按 URI+UA 做 key,10 分钟,另外配置
proxy_cache_valid 404 30s——否则文章刚发布的瞬间爬虫来了一次 404,这个 404 会在缓存里躺满整个周期。
Spring Boot 侧
一个 Controller + 一个 Thymeleaf 模板就完事:
@GetMapping("/post/{slug}")
public ResponseEntity<String> seoPost(@PathVariable String slug) {
Post post = postService.getBySlug(slug);
if (post == null || post.getStatus() != 1) {
response.setStatus(404); // 手动 404,别抛异常
return render404();
}
return ResponseEntity.ok()
.contentType(MediaType.TEXT_HTML).charset(UTF_8)
.body(renderTemplate(post));
}
两个细节:404 要手动 setStatus,抛异常会被全局异常处理器兜成 200 + JSON 的"软 404",搜索引擎最烦这个;模板里 JSON-LD 用注入的 ObjectMapper 序列化,防止正文里的引号打碎结构化数据。
模板输出完整文档:title/description/canonical/og/twitter 卡片 + BlogPosting JSON-LD + 封面标题元信息 + 正文(th:utext 直注数据库里的 HTML)+ 服务端渲染的评论区 + 热门文章内链。内链对爬虫发现旧文章很有价值——它顺着 5 个链接就把站内相关文章全爬了。
上线验证:数字说话
部署后用同一套 UA 复测:
Googlebot → 200, 16419B ✅ 完整正文 + JSON-LD
bingbot → 200, 16419B ✅
Baiduspider → 200, 16419B ✅
GPTBot → 200, 16419B ✅
ClaudeBot → 200, 16419B ✅
PerplexityBot → 200, 16419B ✅
Chrome(真人)→ 200, 6879B SPA 空壳,照旧走前端
爬虫页面的 JSON-LD 校验:BlogPosting 类型、headline、datePublished、author 全部就位;正文 9 个 h2 全在源码里;canonical 指向标准 URL;不存在的 slug 返回真实 404。人类侧页面体积几乎没变,路由、点赞、评论、TOC 交互零改动。
对照上线前:同一个 URL,爬虫拿到的是 6427B 的空壳,现在拿到 16419B 的完整内容——这就是从"搜索引擎眼里不存在"到"正常网页"的差距。
不只有 SEO:AEO 和 GEO 顺路解决
2026 年个人网站的可见性是三件事:SEO 让人搜到你,AEO(Answer Engine Optimization)让 AI 搜索在答案里引用你,GEO(Generative Engine Optimization)让 ChatGPT/Perplexity 这类生成式引擎聊到相关话题时提到你。
这套架构对三者是同时生效的——因为 GPTBot、ClaudeBot、PerplexityBot 都在爬虫名单里,它们之前同样拿不到 JS 渲染的内容。要让 AI 愿意引用,内容形态比技术更重要:开头直接给结论(BLUF 原则)、多放对比表格、"一句话记住"这类可摘录的压缩表达,这种引用对于我们这种的个人网站来说是很可观的。
上线清单与后续
- Thymeleaf 模板必须起后端实测——编译通过只说明 Java 没问题,模板表达式错误只有渲染时才炸。用
java -jar --server.port=8081起一个,curl 爬虫 UA 确认拿到完整 HTML 再挂 Nginx。 - sitemap 同步成新路径后再提交 GSC 和 Bing 站长工具(URL 检查 → 请求编入索引),百度站长平台同步提交——百度 JS 渲染最弱,是这套方案收益最大的搜索引擎。
SPA 不是 SEO 的死刑判决,但空壳 HTML 是——让爬虫和人类看到同一份内容,只是用各自能读懂的方式。
整个过程最大的收获是想明白了一个判断标准:方案好不好,不看它多正统,看它和现有架构的摩擦力有多大。Nuxt 是标准答案,但对我这个"Spring Boot + Vue3 SPA + 每天发一篇文章"的站点,后端动态渲染是摩擦力最小的那条路——前端一行没动,爬虫拿到的东西翻了一倍不止。
常见问题
- SPA 博客搜索引擎完全不收录吗?Google 能执行 JS,理论上有机会,但渲染队列排期长、预算有限,收录慢且不稳定;Bing 支持不完整;百度基本只读初始 HTML。靠等不如主动给 HTML。
- 按 UA 返回不同内容会被 Google 判作弊吗?不会,这是 Google 官方文档认可的 Dynamic Rendering。红线是内容等价:爬虫和用户拿到同一份数据渲染的结果,不能给爬虫塞用户看不到的内容。
- 为什么不用 Nuxt 重写?整个前端要迁移、还要长期维护 Node 渲染服务。我的场景里 Spring Boot 吐 HTML 的成本几乎为零(正文本来就是 HTML 字符串),摩擦力最小。
- 预渲染(prerender)为什么不行?构建时生成静态页,只适合页面固定的站。动态发文的内容站每发一篇就要重新构建,不现实。
- AI 爬虫(GPTBot 等)需要单独处理吗?把它们加进 Nginx 的爬虫 UA 名单即可,和搜索引擎共用同一套渲染逻辑。想让 AI 引用内容,robots.txt 显式 Allow + 内容本身结构化(结论前置、表格、FAQ)比技术手段更重要。