return null:Next.js App Router 博客的 14 个 SEO 死穴
Googlebot 爬你的博客,看到的是一片空白。不是服务器挂了,不是页面 404,是你亲手写的
return null把整个<body>清空了。
0. 症状
部署了一个 Next.js 16 + App Router 的技术博客,文章全是 Server Component,metadata 配得整整齐齐,sitemap 也有,robots.txt 也放了。但 Google Search Console 里,收录数是 0。
curl 一看 HTML 源码:
<body>
<!-- 空的 -->
</body>6 篇精心写的深度技术文章,Googlebot 一个字都没看到。
1. 元凶:ClientOnly 的 return null
根 layout 里有一个 ClientOnly 组件包裹了整个 {children}:
// components/AuthProvider.tsx
'use client'
export function ClientOnly({ children }) {
const [mounted, setMounted] = useState(false)
useEffect(() => { setMounted(true) }, [])
if (!mounted) return null // ← SSR 阶段永远走这里
return <AuthProvider>{children}</AuthProvider>
}SSR 阶段 mounted = false → return null → HTML body 为空。
这个组件的原意是等客户端 hydration 完成后再渲染,避免 auth 状态闪烁。但副作用是:所有页面的 SSR 输出为零。Googlebot 虽然能执行 JS,但需要等 hydration 完成才能看到内容,爬取效率和索引优先级大幅下降。
修复:删掉 if (!mounted) return null,SSR 阶段也正常输出 children。Auth 状态在 SSR 阶段是空的,没关系——博客文章不需要登录态。
2. cookies() 暗杀 ISR
修完 SSR 后,给博客列表页配了 ISR:
export const revalidate = 3600 // 每小时重新生成但发现每次请求仍然走服务端渲染,ISR 缓存完全没生效。
原因:页面里调用了 cookies()。
import { cookies } from 'next/headers'
export default async function BlogPage() {
const cookieStore = await cookies() // ← 这行杀死了 ISR
const token = cookieStore.get('token')?.value
}在 Next.js App Router 中,cookies() 是动态函数(Dynamic Function)。一旦调用,无论你怎么设 revalidate,页面都会强制进入动态渲染模式。ISR 形同虚设。
修复:把 cookie 逻辑移到客户端组件里。博客列表页不需要在服务端读 cookie。
3. 缺 metadataBase,canonical 全废
每篇文章都配了 openGraph.url,但没在根 layout 设 metadataBase:
// ❌ 之前
export const metadata: Metadata = {
title: "DiffServ — V8 Performance Lab",
}
// ✅ 之后
export const metadata: Metadata = {
metadataBase: new URL("https://diffserv.xyz"),
title: "DiffServ — V8 Performance Lab",
}没有 metadataBase,所有相对路径的 canonical URL、OG 图片地址都无法被 Next.js 解析为绝对 URL。搜索引擎拿到的是残缺的 meta 信息。
4. www 和裸域同时响应
Nginx 里 server_name diffserv.xyz www.diffserv.xyz 同时响应两个域名,Google 视为两个独立站点,PageRank 被一分为二。
修复:www 单独做 301:
server {
listen 443 ssl http2;
server_name www.diffserv.xyz;
return 301 https://diffserv.xyz$request_uri;
}5. 没有 HSTS
有 HTTP→HTTPS 301,但没有 Strict-Transport-Security 头。用户每次输入裸域都要经历一次 80→443 的重定向,白白多 100-300ms。
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;6. 静态资源没有长缓存头
Next.js 的 /_next/static/ 文件名自带 content hash,天然可以永久缓存。但 Nginx 没配:
location /_next/static/ {
proxy_pass http://web;
add_header Cache-Control "public, max-age=31536000, immutable" always;
}没有这行,浏览器每次都要发条件请求验证缓存,白白浪费 RTT。
7. 没有 RSS
技术博客没有 /feed.xml = 放弃了 Feedly、Inoreader 等 RSS 阅读器的整个流量入口。在 Next.js App Router 里用 Route Handler 几十行就能生成。
8. 没有 OG 图片
所有文章声明了 twitter.card: summary_large_image 但没给图片 URL。社交平台分享是纯文本链接,点击率比带图低 40%+。
Next.js App Router 支持 app/opengraph-image.tsx 动态生成。
9. JSON-LD 缺字段
Google Rich Results 要求 BlogPosting 至少包含 headline、datePublished、dateModified、image、author。缺少 dateModified 和 image,搜索结果中不会显示富媒体摘要。
10. 没有 404 / 500 页面
Next.js App Router 默认的 404 是白底黑字,没有导航、没有推荐内容。用户点到死链直接流失。创建 app/not-found.tsx 和 app/error.tsx,至少给一个回首页的链接。
11. next.config.ts 为空
const nextConfig: NextConfig = {
poweredByHeader: false, // 隐藏 X-Powered-By: Next.js
images: { formats: ['image/avif', 'image/webp'] },
};poweredByHeader 暴露技术栈给攻击者;不启用 AVIF 意味着放弃了 30-50% 的图片压缩率。
12. viewport 禁止缩放
// ❌ WCAG 违规
export const viewport: Viewport = {
maximumScale: 1,
userScalable: false,
}WCAG 2.1 明确要求用户能放大到至少 200%。删掉这两行。
13. sitemap lastModified 每次构建都变
lastModified: new Date() 导致每次 ISR 重生成时所有 URL 的修改时间都会更新,Google 会重新爬取全站,浪费 crawl budget。硬编码真实的修改日期。
14. 内部链接用了 <a> 而不是 <Link>
Next.js 的 <Link> 会自动 prefetch 目标页面,原生 <a> 触发全页刷新。所有内部跳转都应该用 <Link>。
对标 Astro:Next.js 的额外成本
| 维度 | Astro 默认 | Next.js 需要手动做 |
|---|---|---|
| SSR 输出 | 纯 HTML,零 JS | 确保不被 ClientOnly 阻断 |
| ISR | 默认 SSG | 手动配 revalidate,不能碰 cookies() |
| RSS | 一行插件 | 手写 Route Handler |
| OG 图片 | 社区包成熟 | opengraph-image.tsx 或手动 |
| 零 JS | 默认不发送 runtime | Server Component 不 hydrate 但有 React runtime |
Astro 的优势是默认值就是最佳实践。Next.js 的优势是灵活性——但灵活性的代价是你必须知道每个默认值背后的坑。
如果你的站点同时有博客、交互式 Lab、用户系统、API——Next.js 的全栈能力是 Astro 替代不了的。关键是:把该配的配好,把该删的删掉。
修完之后
14 项全部修完后:
- HTML 源码可见全部文章内容,Googlebot 无需执行 JS
- ISR 缓存生效,TTFB 从 ~500ms 降到 ~50ms
- 社交分享带品牌 OG 图片
- RSS 接入全球阅读器生态
- HSTS preload + www 301 + immutable 缓存
- Lighthouse 全绿
不需要换框架。Next.js 能做到 Astro 做的一切,前提是你知道哪些地方需要手动补。