记录一次 Vue 架构的轻量级 SEO 优化实践

背景

初衷是提升个人网站:ynsstudy.cn对浏览器的检索程度,但是当具体操作时发现传统客户端渲染 (CSR) 的劣势开始显现:搜索引擎爬虫通常只能抓取到初始的空 <div id="app"></div> 容器,无法有效收录通过 JavaScript 异步加载的文章内容。

方案选型

在方案调研阶段,主要评估了以下两种路径:

1.彻底重构为 SSR (Nuxt.js):

SEO 效果最佳,但需要重构现有的 Vue 路由、状态管理(将 Pinia 中的 Token 存储从 localStorage 迁移至 Cookie),并且需要单独处理强依赖浏览器环境的组件(如富文本编辑器)。

2.自建无头浏览器服务:

在 Java 后端引入 Playwright 服务端渲染。但考虑到当前服务器(4G内存)由于运行微服务,可用内存已不足 300MB,引入完整的浏览器内核会导致全面崩盘的风险(主要原因还是服务器太辣鸡)。

综合考虑迁移成本、业务需求(仅首页和个人主页强依赖 SEO)以及服务器资源瓶颈,当然还有一部分原因是本人对Nuxt.js了解的真是少之又少,最终选择了侵入性最小的动态渲染 (Dynamic Rendering) 方案:结合 Nginx 的 User-Agent 识别与第三方预渲染服务 (Prerender.io)。

实施过程

1. Nginx 层流量分发配置

核心思路是在网关层进行流量鉴别。当普通用户访问时,Nginx 返回 Vue 编译后的静态文件,保持原有的无刷新体验;当识别到爬虫访问时,将其代理至预渲染服务,由预渲染服务在云端执行完 JS 并组装 HTML 后返回给爬虫。

需要注意的是,在 Nginx 配置语法中,proxy_set_header 指令不能直接放置在 if 块内部,需将其提取到外部层级。核心配置节选如下:

location @prerender {
    set $prerender 0;
    
    # 识别主流搜索引擎爬虫
    if ($http_user_agent ~* "baiduspider|googlebot|bingbot|yandex|sogou|spider") {
        set $prerender 1;
    }
    # 静态资源排除预渲染
    if ($uri ~* "\.(js|css|xml|less|png|jpg|jpeg|gif|pdf|doc|txt|ico|rss|zip|mp3|rar|exe|wmv|doc|avi|ppt|mpg|mpeg|tif|wav|mov|psd|ai|xls|mp4|m4a|swf|dat|dmg|iso|flv|m4v|torrent|ttf|woff|svg|eot)") {
        set $prerender 0;
    }

    resolver 8.8.8.8 114.114.114.114;
    proxy_set_header X-Prerender-Token 'YOUR_TOKEN_HERE';

    if ($prerender = 1) {
        rewrite .* /$scheme://$host$request_uri? break;
        proxy_pass http://service.prerender.io;
    }
    if ($prerender = 0) {
        root /var/www/vue_dist;
        rewrite .* /index.html break;
    }
}

2. 动态修改title标签

放弃了使用原生 JavaScript 直接操作 document.titledocument.createElement 的方式。引入 @vueuse/head 进行响应式头部管理。在接口数据返回后,直接更新响应式变量,组件卸载时会自动清理残留的 Meta 标签。

3. 预渲染时序控制(防空壳抓取)

实施初期遇到了爬虫抓取过快,页面异步数据还未挂载就被截图存成空壳的问题(导致功能实现了,但是核心诉求并没有得到解决)。
解决方案是引入预渲染服务的全局状态锁。在 index.html 头部声明 window.prerenderReady = false;,等待 Vue 组件数据请求完成且 DOM 更新后(nextTick),再将其置为 true,通知云端浏览器可以开始提取快照。

4. DOM 结构改造与内链建设

网站中常规的页面跳转多通过 @click="router.push(...)" 实现,但这会导致爬虫由于不识别 JS 事件而无法进行深度抓取。
为了构建健康的内部链接网,对文章列表页进行了渐进式增强:使用 <a> 标签包裹可点击区域,配置相对路径的 href 供爬虫识别,同时配合 Vue 的 @click.prevent 修饰符拦截默认的页面跳转,触发原有的弹窗阅读逻辑。此举在不破坏原有交互的前提下,提供了完整的爬虫抓取路径。

<a :href="'/oneBlog/' + blog.GUID" @click.prevent class="seo-link">
  <h4 class="blog-title">{{ blog.BLOG_TITLE }}</h4>
</a>

5. Sitemap 接口性能优化

在生成供搜索引擎主动抓取的站点地图时,原有的 Java 代码存在将全量数据加载到内存并通过 StringBuilder 拼接的问题,这在数据量增大时极易引发内存溢出。
对代码进行了如下重构:

  • 废弃 StringBuilder,改为 response.getWriter().print() 流式输出,大幅降低 JVM 内存占用。
  • 增加了 XML 保留字符(如 &、<)的转义处理,防止 XML 解析异常。
  • 拉开权重梯度:将核心文章详情的 <priority> 设为 0.8,个人主页权重下调至 0.5,引导爬虫资源向核心内容倾斜。

总结

对于部分页面依赖 SEO,且服务器资源有限的项目,基于网关拦截的动态渲染是一个具有较高性价比的技术决策。它避免了全盘推翻现有前端架构,同时也能满足搜索引擎收录的核心诉求。