什么是 "JavaScript 渲染"?
一个页面是 JavaScript 渲染的,意味着服务器发送的 HTML 实质上是空的 — 实际内容(标题、正文、产品信息)只有在浏览器下载并运行了一个 JavaScript 包,把 React、Vue、Svelte 或 Angular 这样的框架挂载到一个空的根元素之后才会出现。
典型的 JS 渲染主页在网络上看起来是这样:
<body>
<div id="root"></div>
<script type="module" src="/assets/index-D9LVtTP6.js"></script>
</body>
可见的 <body> 内容是零字节。你在浏览器中看到的一切都是 JS 运行之后在客户端构造出来的。
为什么对智能体很重要
大多数 AI 爬虫不执行 JavaScript。当 ChatGPT、Claude、Perplexity、Gemini 的抓取或典型的智能体框架访问你的 URL 时,他们拿到的是原始 HTML — 而你的主页是空白的。你的产品描述、标题、链接和正文对他们来说都是不可见的。
这对检索和摘要是真正的伤害。当一个 LLM 在引用你的 URL 时被问到 "这家公司是做什么的?",它会产出糟糕的回答或产生幻觉,因为页面上没有任何东西可读。
这 不会 破坏什么
输出到 HTML 静态 <head> 中的标签不受客户端渲染的影响。即使是完全 JS 渲染的 SPA,只要构建工具正确填充了 head,这些检查仍然可以通过:
- JSON-LD 结构化数据 (
<script type="application/ld+json">) - OpenGraph (
og:image、og:title等) - Twitter Card 元数据
- 规范 URL
- favicon 和
<link rel="alternate">标签
如果你的站点是 JS 渲染的,而且 这些检查也都失败了,那是独立的缺口 — 通过给构建的 HTML 模板加上正确的 <head> 标签来修复,不论是否修复渲染本身。
共同的根本原因: 静态托管的 SPA 包
JS 渲染的站点通常以静态包(一个 index.html 和一个 JS/CSS 文件夹)交付,部署到对任何 GET 请求都返回 index.html 的主机上: Vercel 静态、Netlify、Cloudflare Pages、GitHub Pages、S3 + CloudFront、Fastly Frontend、Firebase Hosting。
这种部署模式根本没有任何按请求的服务器端逻辑。产生 JS 渲染 body 的同一份配置,也 会产生:
- 对每个
Accept头都返回相同的 HTML(没有内容协商) - 对未知路径返回 HTML 200,而不是结构化的 JSON 404
- 没有为智能体调优的
Vary、Cache-Control或速率限制头 - 没有按 User-Agent 改变响应的方式
不触及部署模型而单独修复一个症状(例如内容协商)通常是不可能的 — 你需要服务器端逻辑,或者在静态包前面放一个边缘函数。
修复阶梯
挑选能解决你情况的最低成本选项。
1. 构建时预渲染
如果主页内容主要是静态的,在构建期间预渲染。浏览器仍然会用 React/Vue 等进行水合,但服务器发送的初始 HTML 已经包含渲染后的 DOM。爬虫看到真实内容。
- Vite + react-snap 或 vite-plugin-prerender
- Astro(
output: 'static')— JS 渲染的组件在构建时被服务端渲染为 HTML - Next.js 配
output: 'export'或getStaticProps - Nuxt 配
nuxt generate
对营销页面、博客和文档来说,这是最低成本的修复。
2. 完整的服务器端渲染(SSR)
如果主页需要动态数据(登录态、A/B 测试、新鲜内容),使用每次请求都渲染的框架:
- Next.js(默认的 App Router + React Server Components)
- Remix
- SvelteKit
- Nuxt SSR 模式
SSR 需要一个运行时(Node、Bun、Deno 或边缘运行时),意味着离开纯静态托管。
3. 用于内容协商的边缘函数
如果迁移到 SSR 太破坏现状,在静态包前面放一个边缘函数,拦截请求并返回备用表示:
// Cloudflare Worker / Vercel Edge / Netlify Edge
export default async function (request) {
const accept = request.headers.get('accept') || '';
const ua = (request.headers.get('user-agent') || '').toLowerCase();
const isAgent = /claudebot|gptbot|chatgpt-user|perplexitybot|google-extended/.test(ua);
if (isAgent || accept.includes('text/markdown')) {
return new Response(await fetchMarkdownSummary(), {
headers: { 'Content-Type': 'text/markdown', 'Vary': 'Accept, User-Agent' },
});
}
return fetch(request); // 透传到静态包
}
当底层应用必须保持客户端渲染(例如重交互应用)时,这是合适的修复 — 但你仍然希望智能体读到有意义的内容。
4. <noscript> 兜底(最小可行)
至少在静态 HTML 的 <noscript> 块里放上你的关键内容:
<noscript>
<h1>你的公司</h1>
<p>你做什么,两句话说清。链到文档、价格、API。</p>
<a href="/about">关于</a> · <a href="/docs">文档</a> · <a href="/pricing">价格</a>
</noscript>
这不能代替预渲染,但能避免爬虫看到完全空白的页面。大多数 JS 渲染的站点并没有这一项。
如何验证
不执行 JavaScript 地抓取你的站点,看看 body 里是否有真实内容:
curl -s -A "ClaudeBot/1.0" https://yourdomain.com/ | \
python3 -c "import sys, re; t = sys.stdin.read(); body = re.search(r'<body[^>]*>(.*?)</body>', t, re.S); print(len(re.sub(r'<[^>]+>', '', body.group(1) if body else '').strip()))"
如果打印出来的数字小于几百个字符,智能体看不到你的内容。