return null:Next.js App Router 博客的 14 个 SEO 死穴

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 源码:

xml 复制代码
<body>
  <!-- 空的 -->
</body>

6 篇精心写的深度技术文章,Googlebot 一个字都没看到。


1. 元凶:ClientOnly 的 return null

根 layout 里有一个 ClientOnly 组件包裹了整个 {children}

javascript 复制代码
// 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>
}
xml 复制代码
// app/layout.tsx
<body>
  <ClientOnly>{children}</ClientOnly>
</body>

SSR 阶段 mounted = falsereturn null → HTML body 为空。

这个组件的原意是等客户端 hydration 完成后再渲染,避免 auth 状态闪烁。但副作用是:所有页面的 SSR 输出为零。Googlebot 虽然能执行 JS,但需要等 hydration 完成才能看到内容,爬取效率和索引优先级大幅下降。

修复 :删掉 if (!mounted) return null,让 SSR 阶段也正常输出 children。

javascript 复制代码
export function ClientOnly({ children }) {
  const [mounted, setMounted] = useState(false)
  useEffect(() => { setMounted(true) }, [])
  // 不阻塞 SSR:mounted=false 时也输出 children
  return <AuthProvider>{children}</AuthProvider>
}

Auth 状态在 SSR 阶段是空的,没关系------博客文章不需要登录态。


2. cookies() 暗杀 ISR

修完 SSR 后,给博客列表页配了 ISR:

arduino 复制代码
export const revalidate = 3600 // 每小时重新生成

但发现每次请求仍然走服务端渲染,ISR 缓存完全没生效。

原因:页面里调用了 cookies()

javascript 复制代码
// blog/page.tsx
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

arduino 复制代码
// ❌ 之前
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 配置:

ini 复制代码
server_name diffserv.xyz www.diffserv.xyz;

两个域名同时响应相同内容,Google 视为两个独立站点,PageRank 被一分为二。

修复:www 单独做 301:

perl 复制代码
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 头。用户每次输入 diffserv.xyz 都要经历一次 80→443 的重定向,白白多 100-300ms。

ini 复制代码
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;

6. 静态资源没有长缓存头

Next.js 的 /_next/static/ 文件名自带 content hash,天然可以永久缓存。但 Nginx 没配:

arduino 复制代码
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 生成:

xml 复制代码
// app/feed.xml/route.ts
export async function GET() {
  const items = blogPosts.map(post => `
    <item>
      <title>${post.title}</title>
      <link>https://diffserv.xyz/blog/${post.slug}</link>
      <pubDate>${new Date(post.date).toUTCString()}</pubDate>
      <description>${post.description}</description>
    </item>
  `).join('')
​
  const xml = `<?xml version="1.0" encoding="UTF-8"?>
    <rss version="2.0">
      <channel>
        <title>DiffServ Lab</title>
        <link>https://diffserv.xyz</link>
        ${items}
      </channel>
    </rss>`
​
  return new Response(xml, {
    headers: { 'Content-Type': 'application/rss+xml' },
  })
}

8. 没有 OG 图片

所有文章声明了 twitter.card: summary_large_image 但没给图片 URL。社交平台分享是纯文本链接,点击率比带图低 40%+。

Next.js App Router 支持 app/opengraph-image.tsx 动态生成 OG 图片,或者在 public/ 放一张默认图然后在全局 metadata 里引用。


9. JSON-LD 缺 dateModifiedimage

Google Rich Results 要求 BlogPosting 类型至少包含 headlinedatePublisheddateModifiedimageauthor。缺少 dateModifiedimage,搜索结果中不会显示富媒体摘要(发布日期、缩略图)。


10. 没有 404 / 500 页面

Next.js App Router 默认的 404 是一个白底黑字的 "404 | This page could not be found",没有导航、没有推荐内容。用户点到死链直接流失。

创建 app/not-found.tsxapp/error.tsx,至少给一个回首页的链接和几篇推荐文章。


11. next.config.ts 为空

ini 复制代码
const nextConfig: NextConfig = {};

至少加两行:

yaml 复制代码
const nextConfig: NextConfig = {
  poweredByHeader: false,           // 隐藏 X-Powered-By: Next.js
  images: { formats: ['image/avif', 'image/webp'] },
};

poweredByHeader 暴露技术栈给攻击者;不启用 AVIF 意味着放弃了 30-50% 的图片压缩率。


12. viewport 禁止缩放

yaml 复制代码
export const viewport: Viewport = {
  maximumScale: 1,
  userScalable: false,
}

WCAG 2.1 明确要求用户能放大到至少 200%。这两行让 Lighthouse Accessibility 直接扣分。删掉。


13. sitemap lastModified 每次构建都变

javascript 复制代码
lastModified: new Date(),  // ← 每次 ISR 重生成都是新时间

Google 看到所有 URL 的 lastModified 同时变化,会重新爬取全站,浪费 crawl budget。硬编码真实的修改日期。


14. 内部链接用了 <a> 而不是 <Link>

部分博客文章里的内部跳转(/lab/blog/xxx)用了原生 <a> 标签。Next.js 的 <Link> 组件会自动 prefetch 目标页面,用 <a> 则触发全页刷新,白白丢掉了客户端路由的性能优势。


对标 Astro:Next.js 的额外成本

维度 Astro 默认 Next.js 需要手动做
SSR 输出 纯 HTML,零 JS 需确保不被 ClientOnly 阻断
ISR 默认 SSG 需手动配 revalidate,且不能碰 cookies()
RSS @astrojs/rss 一行配 手写 Route Handler
OG 图片 社区包成熟 opengraph-image.tsx 或手动
零 JS 默认不发送 runtime Server Component 不 hydrate,但仍有 React runtime 开销
sitemap @astrojs/sitemap 自动 手动实现,需注意 lastModified

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

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


修完之后

14 项全部修完后的状态:

  • HTML 源码可见全部文章内容,Googlebot 无需执行 JS
  • ISR 缓存生效,TTFB 从 ~500ms 降到 ~50ms
  • 社交分享带品牌 OG 图片
  • RSS 接入全球阅读器生态
  • HSTS preload + www 301 + immutable 缓存
  • Lighthouse Performance / SEO / Accessibility / Best Practices 全绿

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


GitHub: hlng2002/stw-sentinel 在线实验: diffserv.xyz/lab

相关推荐
萑澈1 天前
我用 Cloudflare 搭了一个 FlashInbox 临时邮箱
前端·next.js
大萝卜呼呼7 天前
Next.js第八课 - 缓存机制
前端·next.js
竹林8188 天前
在Next.js NFT市场中,我如何解决动态路由、链上数据获取与状态同步的连环坑
前端·javascript·next.js
大萝卜呼呼12 天前
Next.js第三课 - 布局与页面 - 优栈
前端·next.js
大萝卜呼呼13 天前
Next.js第二课 - 项目结构详解 - 优栈
前端·next.js
前端缘梦14 天前
Next.js全栈项目部署全流程|从0到1解决数据库、WebSocket、图片上传所有坑
前端·全栈·next.js
Bigger14 天前
🚀 开源发布!从 0 到 1,使用 Next.js + Nest.js 构建全栈自动化数据分析 AI Agent
agent·nestjs·next.js
倾颜14 天前
我是怎么把 Multi-Tool Runtime 升级成第一层 Skill Runtime 的
前端·llm·next.js
倾颜19 天前
不只是接个计算器:我是怎么把 Tool Calling 做成可扩展骨架的
langchain·llm·next.js