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 = falsereturn 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 至少包含 headlinedatePublisheddateModifiedimageauthor。缺少 dateModifiedimage,搜索结果中不会显示富媒体摘要。

10. 没有 404 / 500 页面

Next.js App Router 默认的 404 是白底黑字,没有导航、没有推荐内容。用户点到死链直接流失。创建 app/not-found.tsxapp/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默认不发送 runtimeServer Component 不 hydrate 但有 React runtime

Astro 的优势是默认值就是最佳实践。Next.js 的优势是灵活性——但灵活性的代价是你必须知道每个默认值背后的坑。

如果你的站点同时有博客、交互式 Lab、用户系统、API——Next.js 的全栈能力是 Astro 替代不了的。关键是:把该配的配好,把该删的删掉


修完之后

14 项全部修完后:

不需要换框架。Next.js 能做到 Astro 做的一切,前提是你知道哪些地方需要手动补。