
1. Vue SPA 抓不到内容的真实场景如果你用 Vue 写了个单页应用本地跑起来一切正常路由跳转丝滑但把链接丢给搜索引擎的抓取工具或者用curl直接请求页面会发现返回的 HTML 里只有一个空的div idapp/div和几行打包后的 JS 引用。搜索引擎的爬虫虽然现在能执行一部分 JavaScript但执行预算有限、排队时间长很多中小站点的内容根本等不到渲染完成就被判定为空页面收录自然上不去。这个问题的本质是Vue SPA 的内容是在浏览器里由 JS 运行时生成的服务端吐出来的 HTML 是空壳。要解决它思路有两条一条是上 SSR服务端渲染比如 Nuxt 或 Vue 3 Vite SSR另一条是预渲染——在服务端用无头浏览器把页面渲染成完整 HTML再返回给爬虫。前者改造量大后者对已有项目侵入小适合已经上线、不想大动干戈的站点。这篇就聚焦第二条路用 Puppeteer headless 在服务端渲染 Vue SPA 页面交给 nginx 判断请求来源是爬虫就走预渲染服务是普通用户就走静态资源。同时把 TaoToken 的统一 Key 配置串进来让预渲染服务在需要调用模型能力比如动态生成 meta 描述、内容摘要时有个稳定的入口。整套流程我会给出可复制的启动参数、config.toml 骨架和 nginx 验证动作你照着改域名就能跑。适合谁看手里有 Vue SPA、被收录问题困扰、又不想重构成 SSR 的前端或运维同学。需要你有一台能装 Node 和 Chromium 依赖的 Linux 服务器以及一个能改 nginx 的权限。2. TaoToken 统一 Key 前置准备预渲染服务本身不一定要调模型但很多 SEO 场景会顺手做点增强比如根据页面正文自动生成description、给列表页补一段摘要、或者把标题做语义改写。这些动作如果散落在各个脚本里Key 管理会很乱。我的做法是让预渲染服务统一从 TaoToken 拿 Key一个入口管所有模型调用。TaoToken 在这里的角色是统一 API 网关你拿到一个 Key 之后模型对话、编码类请求都走同一个地址不用为每个模型单独配一套凭证。对预渲染这种后台服务来说配置越少越不容易出错。先去控制台建一个 Key控制台入口https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentconsoleAPI Keys 管理https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentapi-keys建好之后你会得到一串以sk-开头的 Key。注意这个 Key 只放在服务端别写进前端打包产物里预渲染服务是 Node 进程读环境变量或配置文件都行。API 的基础地址是https://taotoken.net/api注意这个地址不带任何查询参数直接作为 base_url 用。如果你用的是 OpenAI 兼容的 SDK把 base_url 指过去、api_key 填上就能通。接入文档在这里遇到参数问题可以对照接入文档https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentdoc如果你后面要做长期编码或者 Agent 类的自动化任务可以看 Coding Plan它更适合持续性的调用场景Coding Planhttps://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentcoding-plan想先验证模型通不通用模型对话页面发一条测试消息最快模型对话https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentmodel-chat3. 可复制的 Puppeteer 启动与 config.toml 骨架3.1 安装依赖先装 Puppeteer 和 Chromium 在 Linux 上需要的一堆系统库。CentOS 系用 yumDebian/Ubuntu 换成 apt 对应包名即可。# 安装 puppeteer npm install puppeteer # Chromium 运行依赖CentOS 示例 yum install -y pango.x86_64 libXcomposite.x86_64 libXcursor.x86_64 \ libXdamage.x86_64 libXext.x86_64 libXi.x86_64 libXtst.x86_64 \ cups-libs.x86_64 libXScrnSaver.x86_64 libXrandr.x86_64 \ GConf2.x86_64 alsa-lib.x86_64 atk.x86_64 gtk3.x86_64 yum install -y xorg-x11-fonts-100dpi xorg-x11-fonts-75dpi \ xorg-x11-utils xorg-x11-fonts-cyrillic xorg-x11-fonts-Type1 \ xorg-x11-fonts-misc yum update -y nss装完可以跑一句node -e require(puppeteer)确认模块能加载。如果报缺库多半是上面某个包没装上按报错补。3.2 config.toml 骨架我把配置抽成 TOML方便和 nginx、systemd 一起管理。下面这份骨架包含预渲染服务和 TaoToken 两部分Key 从环境变量注入不硬编码。# /etc/prerender/config.toml [server] # 预渲染服务监听地址只对内网开放 host 127.0.0.1 port 3000 # 是否校验来源 IP生产建议 true check_ip false allow_ip_list [] [browser] # Puppeteer 启动参数容器/低权限环境必须加 no-sandbox args [ --no-sandbox, --disable-setuid-sandbox, --disable-dev-shm-usage, --disable-gpu, --single-process ] # 页面渲染超时单位毫秒 timeout 15000 # 等待的选择器页面出现该元素视为渲染完成 wait_selector #app [taotoken] # 统一 API 地址不带查询参数 base_url https://taotoken.net/api # Key 从环境变量 TAOTOKEN_API_KEY 读取不要写死 api_key_env TAOTOKEN_API_KEY # 默认模型按需替换 default_model gpt-4o-mini几个参数说明一下。--disable-dev-shm-usage在内存小的机器上很关键不加容易因为/dev/shm太小导致 Chromium 崩溃。--single-process能省资源但稳定性略差如果并发高可以去掉。wait_selector设成#app是等 Vue 挂载完成如果你的首屏内容在某个具体组件里换成那个组件的选择器更准。3.3 预渲染服务代码下面这份 server.js 在原文基础上做了几处调整加了超时控制、等待选择器、以及从 config.toml 读配置。浏览器只启动一次服务常驻避免每个请求都开关浏览器。const http require(http); const fs require(fs); const puppeteer require(puppeteer); // 简易 TOML 读取生产可用 iarna/toml const conf JSON.parse(fs.readFileSync(/etc/prerender/config.json, utf8)); function getIp(req) { return req.headers[x-real-ip] || req.headers[x-forwarded-for] || req.connection.remoteAddress || ; } function checkIP(ip) { return conf.server.check_ip ? conf.server.allow_ip_list.includes(ip) : true; } (async () { const browser await puppeteer.launch({ args: conf.browser.args }); const server http.createServer((request, response) { const url http:// request.headers.host request.url; const ip getIp(request); if (!checkIP(ip)) { response.statusCode 404; response.end(404 NOT Found); return; } browser.newPage().then(async (page) { try { await page.goto(url, { waitUntil: networkidle0, timeout: conf.browser.timeout }); await page.waitForSelector(conf.browser.wait_selector, { timeout: conf.browser.timeout }); const content await page.content(); await page.close(); response.setHeader(Content-Type, text/html; charsetutf-8); response.end(content); } catch (err) { await page.close(); response.statusCode 500; response.end(render error: err.message); } }); }); server.listen(conf.server.port, conf.server.host, () { console.log(prerender listening on conf.server.host : conf.server.port); }); })();注意我把配置读成了 JSON因为 Node 原生没有 TOML 解析。你可以用iarna/toml把上面的 config.toml 直接读进来或者用构建脚本把 TOML 转成 JSON。两种都行关键是 Key 走环境变量。启动时注入 Keyexport TAOTOKEN_API_KEYsk-你的Key node server.js4. nginx 反代与 curl 验证预渲染结果4.1 nginx 配置核心逻辑是判断 User-Agent命中爬虫关键词就转发到预渲染服务否则走静态文件。原文的配置基本可用我补了超时和缓冲相关设置避免预渲染慢的时候 nginx 直接 502。location / { proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; if ($http_user_agent ~* spider|bot|Googlebot|Baiduspider|bingbot) { proxy_pass http://127.0.0.1:3000; proxy_read_timeout 30s; proxy_connect_timeout 10s; proxy_buffering off; } try_files $uri $uri/ router; } location router { rewrite ^(.*)$ /index.html last; }proxy_buffering off让响应边渲染边回传对长页面友好。proxy_read_timeout给到 30 秒比 Puppeteer 的 15 秒超时留了余量。4.2 curl 验证是否含目标关键词改完 nginx 记得nginx -t再 reload。然后模拟爬虫请求看返回的 HTML 里有没有你的目标关键词。curl -A Googlebot/2.1 (http://www.google.com/bot.html) \ -s http://your-domain.com/your-page | grep -o 你的目标关键词如果输出了关键词说明预渲染生效。再对比一下普通用户请求curl -s http://your-domain.com/your-page | grep -o 你的目标关键词普通请求大概率匹配不到因为返回的是空壳 HTML这正是预期结果——用户浏览器会自己执行 JS 渲染爬虫拿到的是预渲染好的完整页面。还可以看响应头确认走了哪条路径在预渲染服务里加一个自定义头X-Prerender: 1然后curl -A Baiduspider -sI http://your-domain.com/your-page | grep -i prerender有输出就说明 nginx 分流正确。5. 本篇常见错排查5.1 Puppeteer 启动报错缺共享库现象是error while loading shared libraries: libXxxx.so。这是系统库没装全按第 3.1 节的 yum 列表逐个补。用ldd查 Chromium 二进制缺哪个库最直接ldd node_modules/puppeteer/.local-chromium/*/chrome-linux/chrome | grep not found5.2 渲染超时或返回空内容多半是waitUntil和wait_selector没配对。Vue 应用如果首屏有异步接口networkidle0会等所有请求结束接口慢就容易超时。可以改成domcontentloaded加显式等待选择器或者把超时调大。另外确认wait_selector选的是真实存在的元素选错了会一直等到超时。5.3 nginx 分流不生效检查$http_user_agent的大小写nginx 的~*是不区分大小写的但关键词要覆盖到实际爬虫的 UA。用curl -A手动指定 UA 测试最可靠。还有一种情况是 CDN 在前面真实 UA 被改写了需要在 CDN 层放行或透传。5.4 TaoToken 调用返回 401先确认环境变量TAOTOKEN_API_KEY在启动 Node 的 shell 里确实存在echo $TAOTOKEN_API_KEY看一眼。再确认 base_url 是https://taotoken.net/api没有多余斜杠或查询参数。如果还不行去模型对话页面用同一个 Key 发条消息能通说明 Key 没问题问题在代码里的请求构造。5.5 预渲染服务内存涨得快每个newPage都会占内存虽然关了页面但 Chromium 的内存回收有延迟。并发高的时候限制一下同时打开的页面数或者定期重启服务。--single-process能压一部分内存但别在高并发下用。6. 后续怎么接预渲染跑通之后如果你还想让页面内容更聪明一点比如根据正文自动生成 meta description可以在预渲染服务里拿到page.content()之后抽正文丢给模型处理再回填到 HTML 的 head 里。这时候统一 Key 的价值就体现出来了一个 Key 走完所有模型调用不用在多个平台之间切换凭证。需要长期跑编码或自动化任务的Coding Plan 那条线更适合持续调用Coding Planhttps://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentcoding-plan接入过程中遇到参数或鉴权问题对照接入文档最快接入文档https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentdocKey 管理和新建都在控制台和 API Keys 页面控制台https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentconsoleAPI Keyshttps://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentapi-keys最后提醒一句预渲染不是银弹。它适合内容相对稳定、更新频率不高的页面。如果你的站点是高频动态内容SSR 或者增量静态生成会更合适。Puppeteer 这条路胜在改造小、上线快先用它把收录问题压下去再慢慢规划更彻底的方案是个务实的顺序。