从零理解 Next.js:SPA 的 SEO 短板与 SSR 全栈方案

从零理解 Next.js:SPA 的 SEO 短板与 SSR 全栈方

很多前端同学第一次接触 Next.js,都会被两个问题绕晕:"SPA 不是体验很好吗?为什么还要搞个 Next.js?" 以及 "'use client' 到底是什么意思,组件到底在哪里渲染?" 答案分别藏在两个词里------SEO 和 水合(Hydration)。这篇文章用大白话 + 一个能跑通的前后端 Demo,把 SPA 的 SEO 短板、SSR 的原理、'use client' 的真相,以及一个完整的全栈闭环(route.ts 接口 + page.tsx 页面)一次讲清。

前言

最近在学 Next.js,老师在开讲之前先花了一大段讲 SEO。我一开始很不理解:"我又不做运营,学个框架跟 SEO 有啥关系?" 后来才明白,理解不了 SEO,就理解不了 Next.js 为什么存在。踩了不少坑,写下来希望能帮到和我一样刚入门的同学。

你将会收获:

  • 🎯 SEO 到底是什么,为什么 PC 时代它是"命"
  • 🔑 SPA 为什么天生没有 SEO(#root 空壳的原理)
  • ⚡ CSR 和 SSR 的本质区别------组件到底在哪里渲染
  • 🧩 Next.js 的"文件即路由"和"嵌套布局"
  • 🔍 'use client' 的真相------Client Component 到底是不是"只在浏览器渲染"
  • 🥟 水合(Hydration)是什么,用"饺子"比喻一次讲透
  • 🛠️ 一个完整可跑的全栈 Demo:route.ts 接口 + page.tsx 页面 + 共享类型打通
  • 📌 SEO 的三层做法

技术栈: Next.js(App Router)+ TypeScript + Tailwind CSS + React

目录

  • [一、SEO 到底是什么?用大白话讲清楚](#一、SEO 到底是什么?用大白话讲清楚 "#%E4%B8%80seo-%E5%88%B0%E5%BA%95%E6%98%AF%E4%BB%80%E4%B9%88%E7%94%A8%E5%A4%A7%E7%99%BD%E8%AF%9D%E8%AE%B2%E6%B8%85%E6%A5%9A")
  • [二、SPA 为什么没有 SEO](#二、SPA 为什么没有 SEO "#%E4%BA%8Cspa-%E4%B8%BA%E4%BB%80%E4%B9%88%E6%B2%A1%E6%9C%89-seo")
  • [三、Next.js 怎么补上:SSR 的原理](#三、Next.js 怎么补上:SSR 的原理 "#%E4%B8%89nextjs-%E6%80%8E%E4%B9%88%E8%A1%A5%E4%B8%8Assr-%E7%9A%84%E5%8E%9F%E7%90%86")
  • [四、Next.js 语法:约定大于一切](#四、Next.js 语法:约定大于一切 "#%E5%9B%9Bnextjs-%E8%AF%AD%E6%B3%95%E7%BA%A6%E5%AE%9A%E5%A4%A7%E4%BA%8E%E4%B8%80%E5%88%87")
  • [五、'use client' 到底是什么意思?](#五、'use client' 到底是什么意思? "#%E4%BA%94use-client-%E5%88%B0%E5%BA%95%E6%98%AF%E4%BB%80%E4%B9%88%E6%84%8F%E6%80%9D")
  • [六、一个完整的全栈 Demo:待办事项](#六、一个完整的全栈 Demo:待办事项 "#%E5%85%AD%E4%B8%80%E4%B8%AA%E5%AE%8C%E6%95%B4%E7%9A%84%E5%85%A8%E6%A0%88-demo%E5%BE%85%E5%8A%9E%E4%BA%8B%E9%A1%B9")
  • [七、SEO 的三层做法](#七、SEO 的三层做法 "#%E4%B8%83seo-%E7%9A%84%E4%B8%89%E5%B1%82%E5%81%9A%E6%B3%95")
  • [八、常见坑:{children} 丢了](#八、常见坑:{children} 丢了 "#%E5%85%AB%E5%B8%B8%E8%A7%81%E5%9D%91children-%E4%B8%A2%E4%BA%86")
  • [九、面试高频 4 问](#九、面试高频 4 问 "#%E4%B9%9D%E9%9D%A2%E8%AF%95%E9%AB%98%E9%A2%91-4-%E9%97%AE")
  • 总结

一、SEO 到底是什么?用大白话讲清楚

先讲痛点:没有 SEO 会怎样

想象你开了一家饭店,但开在一条没有招牌、地图上搜不到的小巷子里。菜做得再好,也没人找得到你,只能靠老顾客口口相传。

网站没有 SEO 就是这个处境:你的内容再好,搜索引擎(百度、谷歌)搜不到、排不上,就等于没流量。

SEO 是什么

SEO(Search Engine Optimization,搜索引擎优化):让搜索引擎"看懂"你的页面、"信任"你的网站,从而在用户搜索时把你排在前面。

arduino 复制代码
用户搜"怎么做红烧肉"
        ↓
搜索引擎抓取全网网页 → 打分排序
        ↓
排前面的 → 点进来的人多 → 免费流量大

💡 一句话记住:SEO = 让搜索引擎"看懂"你 + "信任"你。

为什么 PC 时代 SEO 是"命"

PC 时代,用户上网的入口是搜索引擎。打开电脑 → 打开百度/谷歌 → 搜 → 点进去。所以:

流量 = 搜索排名 = 网站的命。

像掘金、CSDN 这些老牌内容网站,主要流量都来自 SEO------你搜技术问题,它们排在前面,你就点进去了。

到了移动端时代,入口变成了超级 App(微信、抖音),才催生了大量"App 化"的 SPA 应用。但做内容、做官网、做 AI 产品站,SEO 依然是命脉。

🔥 现在 AI 时代的 To C 产品多如牛毛 ,各种 AI Agent 产品站点,都靠官网 + SEO 引流。甚至出现了 GEO(生成式引擎优化)------让豆包、Kimi 这类 AI 在回答时"带上你的内容和购买链接"。入口变了,但"被搜到"的本质没变。


二、SPA 为什么没有 SEO

SPA(Single Page Application,单页应用)体验确实好:不刷新页面、切换快、像原生 App。但它天生就跟 SEO 八字不合

SPA 的工作方式

SPA 的 HTML 文件,长这样(这是 Vite 项目的 index.html):

html 复制代码
<!DOCTYPE html>
<html lang="en">
  <head>
    <title>spa-demo</title>   <!-- 只有一个默认标题 -->
  </head>
  <body>
    <div id="root"></div>       <!-- ⭐ 空的! -->
    <script type="module" src="/src/main.jsx"></script>
  </body>
</html>

看那个 <div id="root"></div>------它是空的 。真正的文字内容(文章、商品、链接)都在 main.jsx 引用的 React 组件里,要等浏览器执行 JS 之后才会被"塞"进这个 div。

关键:爬虫不执行 JS

less 复制代码
浏览器(会执行 JS):
  拿到 HTML → 执行 main.jsx → 内容渲染出来 ✅

爬虫(不执行 JS):
  拿到 HTML → 不执行 JS → 只看到 <div id="root"></div> ❌

🏠 一句话类比 :SPA 的 HTML 是一个空房子 + 一张"装修图纸在 JS 里"的纸条。浏览器会照着图纸把家具摆好;爬虫只看了看空房子,就走了。

用一张图看清 CSR

arduino 复制代码
CSR(Client Side Rendering,客户端渲染):
┌──────────┐   ①请求   ┌──────────┐
│ 浏览器    │ ───────> │  服务器    │
│ (客户端)  │ <─────── │          │
└──────────┘   ②返回空壳 └──────────┘
     │        (index.html #root)
     │ ③执行 JS
     │ ④才渲染出内容
     ▼
  用户看到页面(但爬虫早走了)

所以 SPA 的渲染方式叫 CSR(客户端渲染) ------组件在浏览器里渲染

谁来渲染 爬虫看到 SEO
SPA / CSR 浏览器(客户端) 空壳 #root ❌ 差
Next.js / SSR 服务器 完整 HTML ✅ 好

⚠️ 面试考点 :CSR 和 SSR 的根本区别,不是"速度快慢",而是组件到底在哪里渲染------在浏览器就是 CSR,在服务器就是 SSR。SEO 好不好,全看这一条。


三、Next.js 怎么补上:SSR 的原理

先回到 Java 全栈的"老办法"

其实 SSR 很早就有了。回想 Java 全栈的流程:

bash 复制代码
浏览器 → server:3000 → /todos 路由 → controller → service → MySQL
                                        ↓
                                  拿到 todos 数据
                                        ↓
                             塞进模板(JSP/Thymeleaf)渲染
                                        ↓
                               返回完整 HTML ✅

因为 HTML 是服务器拼好了再发的,所以爬虫一来就能抓到完整内容 → SEO 好。

React 本来做不了这件事

问题在于:React 天生设计成只能在浏览器里跑。它没有"在服务器上先渲染成 HTML"这个能力,所以 React 开发者一直拿不到 SSR 的好处。

Next.js 干的事,就是把 SSR 的能力补给了 React。

关键认知:React 组件其实"也是模板"

一个 React 组件本质上是一个纯函数

tsx 复制代码
// 输入:数据         输出:模板(JSX)
function TodosPage({ todos }) {
  return (
    <ul>
      {todos.map(t => <li key={t.id}>{t.title}</li>)}
    </ul>
  );
}

进去的是 todos 数据,出来的是模板。这跟 Java 里的模板引擎(JSP/Thymeleaf)是同一个东西 。所以------既然它是纯函数,为什么不能在服务器(Node.js)上跑?

Next.js 就是这么干的:

css 复制代码
浏览器访问 /todos
      │
      ▼
Next.js 服务器 (Node.js)          ← 全在服务器上,浏览器没参与
      │
      ├─ ① 路由匹配:app/todos/page.tsx
      ├─ ② 执行组件函数,查 todos 数据
      ├─ ③ JSX + 数据 → 编译成 HTML
      └─ ④ 套进 layout,拼成完整 HTML
      │
      ▼
返回完整 HTML 给浏览器 ✅
      │
      ▼
浏览器直接显示(不用跑 JS 就有内容)→ 爬虫也能抓到 → SEO 好

🍜 一句话类比:CSR 是"给你一包方便面,自己泡";SSR 是"餐厅现炒现卖,端上来就能吃"。

但有个前提:服务器上没有"浏览器"

服务器上跑的是 Node.js,没有用户点击、没有 DOM、没有窗口。所以:

✅ 能用在服务器组件里 ❌ 不能用在服务器组件里(浏览器专属)
纯函数、查数据、写 JSX onClick 事件监听
fetch 请求数据 useEffect
await 查数据库 useState 交互状态

所以 Next.js 把组件分成两类:

arduino 复制代码
Server Component(服务器组件)      Client Component(客户端组件)
- 默认就是                        - 文件顶部写 'use client'
- 能查数据、直接出 HTML            - 能用 onClick / useEffect / useState
- 负责 SEO                       - 负责交互

四、Next.js 语法:约定大于一切

Next.js 最反直觉的一点是:它靠"约定"而不是"配置" 。你不用写路由表,文件夹结构本身就是路由表

文件即路由

规则就两条:

arduino 复制代码
规则1:app/ 文件夹是"路由的根"
规则2:文件夹名 → 网址路径,page.tsx → 页面内容
tsx 复制代码
// app/page.tsx ------ 服务器端组件
// 在服务器端运行,把 JSX 编译成 HTML
export default function Home() {
  return (
    <>
      <h1>Hello World</h1>
    </>
  );
}
tsx 复制代码
// app/about/page.tsx
export default function About() {
  return (
    <>
      <h1>About Us</h1>
    </>
  );
}

项目结构对网址的映射关系:

bash 复制代码
app/
├─ layout.tsx            → 所有页面的"外壳"
├─ page.tsx              → 网址  /         → "Hello World"
├─ about/
│  └─ page.tsx           → 网址  /about    → "About Us"
└─ dashboard/
   ├─ layout.tsx         → dashboard 的"外壳"
   ├─ page.tsx           → 网址  /dashboard
   └─ settings/
      └─ page.tsx        → 网址  /dashboard/settings
你在浏览器访问 Next.js 去找 渲染出
/ app/page.tsx Hello World
/about app/about/page.tsx About Us
/dashboard app/dashboard/page.tsx 后台管理系统

🔑 为什么"加了 /about 就会变" :网址 /about文件夹名 about 决定;这个文件夹里必须有个叫 page.tsx 的文件,Next.js 才认它是个页面并渲染出来。两个约定缺一不可。

对比 SPA 的手写路由

SPA 要手动写路由表:

jsx 复制代码
// ❌ SPA:每个网址和组件的对应关系,要自己一条条写
<Routes>
  <Route path="/" element={<Home />} />
  <Route path="/about" element={<About />} />
</Routes>

Next.js 不需要这张表 。它启动时自动扫描 app/ 文件夹,看到 about/page.tsx,就自动知道"哦,/about 对应这个页面"。

嵌套布局:一组页面共用的"外壳"

layout.tsx 是"外壳",它自己会渲染,{children} 是留的洞,子页面内容填进来

tsx 复制代码
// app/layout.tsx ------ 根布局,套所有页面
import Link from "next/link";

export const metadata = {
  title: "我的 Next.js Demo",
  description: "一个 SEO 友好的全栈示例",
};

export default function RootLayout({ children }) {
  return (
    <html lang="zh">
      <body>
        <nav>
          <ul>
            <li><Link href="/">首页</Link></li>
            <li><Link href="/about">关于</Link></li>
            <li><Link href="/dashboard">后台</Link></li>
          </ul>
        </nav>
        {children}
      </body>
    </html>
  );
}

布局是可以"套娃"的。比如给 dashboard 这一组页面加个专属导航栏:

tsx 复制代码
// app/dashboard/layout.tsx ------ 第二层外壳,只套 dashboard 这组页面
import Link from "next/link";

export default function DashboardLayout({ children }) {
  return (
    <div>
      <nav>
        Nav
        <Link href="/dashboard/settings">Settings</Link>
      </nav>
      {children}
    </div>
  );
}

渲染后的效果:

访问网址 渲染结果
/dashboard Nav + 后台管理系统
/dashboard/settings Nav + settings 后台管理系统
/about 只有 About Us(没有 Nav)

看到了吗------nav 只出现在 dashboard 这组页面里,不用在每个页面重复写。这就是嵌套布局的意义:一组页面共用的东西,写一次就够了。

⚠️ layout 里的东西也会"打印"出来<html><body><nav> 这些不是配置,是真的会被输出成 HTML 的。只有 {children} 不是它自己,而是子页面内容填进来。

一个容易懵的点:"服务器"在哪?

很多同学会懵:"我明明在本地写代码,哪来的服务器?"

真相是:你运行 npm run dev 的那一刻,Next.js 会在你自己的电脑上启动一个 Node.js 进程,这个进程就是"服务器"。

arduino 复制代码
你的电脑
├─ ① 浏览器(客户端)       ← 打开 localhost:3000 看网页
└─ ② Node.js 进程(服务器端) ← npm run dev 启动的,负责渲染 HTML

浏览器访问 localhost:3000/about,请求在你自己电脑内部转了一圈:浏览器 → Node 进程(渲染 HTML)→ 返回浏览器。所谓"服务器端",指的就是"代码在 ② 这个 Node 进程里运行,而不是在 ① 浏览器里运行"。


五、'use client' 到底是什么意思?

一个最常见的误解

第三节讲过,Next.js 把组件分成两类:Server Component(默认,负责出 HTML / SEO)Client Component(顶部写 'use client',负责交互) 。但很多同学一看到 'use client',第一反应就错了:

"这个组件只在浏览器里渲染,服务器根本不碰它。"

这个理解错得很关键。老师上课反复强调的那句话,才是对的:

'use client' 不是"只在浏览器渲染",而是"这个组件有交互、需要在浏览器运行"------它先在服务器把能渲染的渲染完,再交给客户端接着渲染。

它的完整生命周期

arduino 复制代码
'use client' 组件的完整生命周期:

① 服务器端  先把 JSX 渲染成 HTML(能提前渲染的先渲染完)
        ↓
② 浏览器    拿到静态 HTML,立刻有内容可看
        ↓
③ 浏览器    加载 JS,把事件、状态"接上去"
        ↓
④ 页面"活"了,可以点击、可以交互

水合(Hydration):页面"活"起来的那一步

第 ③ 步有个专业名词叫 Hydration(水合),老师的注释写得很到位:

水合(hydration):浏览器拿到静态 HTML 之后,挂载客户端 JS,绑定点击事件、激活交互。

用老师的"饺子"比喻最好懂:

步骤 饺子比喻 对应技术
包好饺子、冻上 服务器把 JSX 渲染成 HTML SSR 服务端渲染
给你送过来 把静态 HTML 发给浏览器 返回 HTML
你拿去水煮 浏览器绑定事件、激活交互 Hydration 水合

饺子既不是"生"的,也不是"熟"的------服务器把饺子包好冻上送过来,浏览器负责最后煮熟。 这就是 Next.js 组件的运行真相。

组件会"执行两次"

所以一个 'use client' 组件,代码会跑两次

CSR 组件会执行两次:第一次在服务器(生成 HTML),第二次在客户端(挂载事件、激活交互),像打补丁的感觉。

执行位置 干什么 产出
第 1 次:服务器 执行组件函数,JSX → HTML 静态 HTML(SEO 友好)
第 2 次:客户端 跑 JS,绑 onClick、跑 useEffect 可交互页面

⚠️ 面试考点 :问"Client Component 是不是只在浏览器渲染?"------不是。它先服务器渲染(SSR)再客户端水合(Hydration),所以 Next.js 的 Client Component 首屏 HTML 也是完整的,照样吃得到 SEO 的好处。

什么时候必须写 'use client'

一句话判断(第三节那张表的延伸):

只要用了浏览器专属的能力------useStateuseEffectonClick 事件、fetch 调接口------就必须在文件顶部写 'use client' 不写,Next.js 会直接报错,因为这些 Hook 只能在浏览器里跑。


六、一个完整的全栈 Demo:待办事项

光讲概念太虚。下面用老师课上这个"待办事项"例子,把 Server Component(接口)+ Client Component(页面)+ 共享类型 串成一个完整的前后端应用。

三个文件的分工

scss 复制代码
┌─────────────────────────────────────────────┐
│  app/todos/type.ts   共享类型(前后端通用)      │
│  id / content / completed                    │
└─────────────────────────────────────────────┘
        ↑ 被下面两个文件 import

┌──────────────────┐   fetch("/api/todos")   ┌──────────────────┐
│  app/todos/       │ ─────────────────────▶ │  app/api/todos/   │
│  page.tsx         │                          │  route.ts         │
│  'use client'     │ ◀─────────────────────  │  GET() / POST()   │
│  前端 UI(交互)   │   res.json() → setTodos  │  后端接口(数据)  │
└──────────────────┘                          └──────────────────┘
   Client Component(有交互)                     Server Component(默认,无 'use client')

第一步:共享类型 type.ts

前后端都要用到 Todo 这个结构,抽出来放一起,两边 import,避免各写一份:

ts 复制代码
// app/todos/type.ts ------ 前后端共享的数据结构
export type Todo = {
  id: number;        // 唯一标识,React 列表渲染要当 key 用
  content: string;   // 待办内容
  completed: boolean; // 是否完成
};

第二步:后端接口 route.ts

ts 复制代码
// app/api/todos/route.ts ------ 纯后端,没有 'use client'
// 注意:文件放在 app/api/ 下,就成了数据接口,而不是页面
import { type Todo } from '../../todos/type';

// 内存里的"假数据库",真实项目会换成查 MySQL
let todos: Todo[] = [
  { id: 1, content: '学习AppRouter', completed: true },
  { id: 2, content: 'next.js 个人官网开发', completed: false },
];

// 约定:导出一个叫 GET 的函数 = 处理 HTTP 的 GET 请求(RESTful)
export async function GET() {
  // Response.json 是 Next.js 封装好的:把数组转成 JSON 并带上响应头
  return Response.json(todos);
}

// 约定:导出一个叫 POST 的函数 = 处理 HTTP 的 POST 请求
export async function POST(req: Request) {
  const body = await req.json();       // 拿到前端 POST 过来的 JSON 体
  const newTodo: Todo = {
    id: +Date.now(),                  // 用时间戳当 id(简陋但够用)
    content: body.content,            // 前端传来的内容
    completed: false,                 // 新加的默认未完成
  };
  todos.push(newTodo);                // 塞进"假数据库"
  return Response.json(newTodo);      // 把新建的这条返回给前端
}

关键点:导出的函数名就是 HTTP 方法名GET 处理 GET 请求,POST 处理 POST 请求,这是 Next.js 的 RESTful 约定,非常直白。

第三步:前端页面 page.tsx

tsx 复制代码
// app/todos/page.tsx ------ 有交互,所以顶部写 'use client'
'use client';

import { useEffect, useState } from 'react';
import { type Todo } from './type';

export default function TodosPage() {
  const [todos, setTodos] = useState<Todo[]>([]);  // 列表数据
  const [text, setText] = useState<string>("");    // 输入框内容

  // 拉取后端数据
  const fetchTodos = async () => {
    const res = await fetch("/api/todos");          // 请求我们刚写的 GET 接口
    const data: Todo[] = await res.json();
    setTodos(data);                                 // 存进 state → 触发重新渲染
  };

  // 新增一条待办
  const handleAdd = async () => {
    if (!text.trim()) return;                       // 空内容直接忽略
    await fetch("/api/todos", {                     // 请求 POST 接口
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ content: text }),      // 把输入框内容发给后端
    });
    setText("");                                    // 清空输入框
    await fetchTodos();                             // 重新拉一遍,让新条目显示出来
  };

  useEffect(() => {
    fetchTodos();                                   // 页面挂载时拉一次数据
  }, []);

  return (
    <div style={{ marginTop: "12px" }}>
      <h1>待办事项</h1>
      <input
        value={text}
        onChange={(e) => setText(e.target.value)}
        placeholder="请输入新的待办事项"
      />
      <button onClick={handleAdd} style={{ marginLeft: "8px" }}>添加</button>

      <ul style={{ paddingLeft: "0", listStyle: "none" }}>
        {todos.map((item) => (
          <li key={item.id} style={{ margin: "8px 0", display: "flex", gap: "10px" }}>
            <span style={{
              textDecoration: item.completed ? "line-through" : "none",
              cursor: "pointer",
            }}>
              {item.content}
            </span>
            <button>删除</button>
          </li>
        ))}
      </ul>
    </div>
  );
}

逐行拆解:一个请求是怎么"跑一圈"的

加一条待办的完整流程(这正是"客户端 ↔ 服务端"协作的缩影):

scss 复制代码
用户在输入框打字
      ↓ onChange → setText        (存进 state)
点击"添加"
      ↓ handleAdd
fetch POST /api/todos              (发请求给后端)
      ↓
route.ts 的 POST 函数              (后端收数据)
      ↓ todos.push(newTodo)        (存进"假数据库")
      ↓ 返回 newTodo
前端拿到响应
      ↓ setText("") 清空输入框
      ↓ fetchTodos() 重新拉列表
      ↓ setTodos(data)             (更新 state)
      ↓ React 重新渲染
页面出现新的一条待办 ✅

几个关键的"为什么":

代码 为什么这样写
'use client' 用了 useState/useEffect/onClick,这些只能在浏览器跑
useEffect(..., []) 第二个参数是空数组 = 只在页面挂载时执行一次
text.trim() 防止提交全是空格的内容
key={item.id} React 列表渲染要求每项唯一 key,否则会警告
completed ? "line-through" : "none" 三元表达式:完成了加删除线,没完成不加
POST 后再 fetchTodos() 让新加的那条立刻显示出来(否则要手动刷新)

🍜 一句话记住route.ts 是后端出数据,page.tsx 是前端画界面,中间靠 fetch + 共享的 type 打通------这就是 Next.js 全栈开发的完整闭环。


七、SEO 的三层做法

知道原理后,实操上 SEO 分三层:

第一层:你是谁?(元信息)

html 复制代码
<title>我的 Next.js 教程</title>
<meta name="description" content="这是一个介绍 Next.js 的网站">
<meta name="keywords" content="Next.js, SEO, SSR">

告诉搜索引擎:你是谁(title)、做什么的(description)、有什么价值(keywords)

在 Next.js 里,写在 layout.tsxpage.tsxmetadata 里:

tsx 复制代码
export const metadata = {
  title: "我的 Next.js Demo",
  description: "一个 SEO 友好的全栈示例",
};

第二层:做内容

用户来的原因。再好的 title,没有真正有用的内容,也留不住人、排不上前。内容为王,这是 SEO 最根本的一层。

第三层:SSR 服务端渲染

这是技术层面最狠的一招。设想一个博客站 /post/:id,一篇文章一个页面、千万篇文章:

  • 如果靠 CSR,爬虫抓到的全是空壳,千万篇等于零
  • 如果靠 SSR,每篇文章都返回完整 HTML,整站内容都被 SEO 收录,给你加权

📌 一句话记住三层:第一层说"我是谁",第二层给"用户来的理由",第三层靠 SSR 让"内容被爬虫抓到"。


八、常见坑:{children} 丢了

写嵌套布局时,最容易犯的错就是忘了把 {children} 放进 JSX

tsx 复制代码
// ❌ 错误:children 拿到了,但没放进 JSX,子页面内容会消失
export default function DashboardLayout({ children }) {
  return (
    <div>
      <nav>Nav</nav>
      {/* 这里漏了 {children}! */}
    </div>
  );
}

结果是:访问 /dashboard只显示 "Nav",页面内容"后台管理系统"不见了

tsx 复制代码
// ✅ 正确:必须把 {children} 放进去,子页面内容才会显示
export default function DashboardLayout({ children }) {
  return (
    <div>
      <nav>Nav</nav>
      {children}
    </div>
  );
}

🐛 原因children 是 Next.js 传给你的"子页面内容",你不在 JSX 里渲染它,它自然就丢了。layout 自己的内容(nav)会正常显示,丢的只是洞里的子页面


九、面试高频 4 问

1. CSR 和 SSR 有什么区别?

根本区别是组件在哪里渲染

  • CSR(客户端渲染):服务器返回空壳 HTML + JS,浏览器执行 JS 才渲染内容。SEO 差。
  • SSR(服务端渲染):服务器把组件编译成 HTML 再返回。SEO 好。

一句话:CSR 在浏览器渲染,SSR 在服务器渲染。
2. 为什么 SPA 对 SEO 不友好?

因为 SPA 的 HTML 只有一个空的 <div id="root"></div>,真正内容要靠 JS 执行才出现。而搜索引擎爬虫不执行 JS(或执行不稳定),所以抓到的是空壳,没内容可收录。
3. Next.js 里 Server Component 和 Client Component 怎么区分?

  • Server Component(默认) :在服务器渲染,能查数据、直接出 HTML,负责 SEO。不能用 useState/useEffect/onClick
  • Client Component :文件顶部写 'use client',能用交互相关的 Hook,负责交互。

判断标准:凡是用了浏览器专属 API(事件、生命周期、DOM)的,就必须是 Client Component。
4. Client Component 是不是"只在浏览器渲染"?

不是。'use client' 组件会执行两次:第一次在服务器(SSR,生成静态 HTML),第二次在客户端(Hydration,挂载事件、激活交互) 。所以它的首屏 HTML 依然是完整的,SEO 友好。判断标准不是"在哪渲染",而是"用了哪些能力"------凡是用 useState/useEffect/onClick 的,文件顶部必须写 'use client'


总结

核心概念速查表

概念 一句话
SEO 让搜索引擎"看懂"你 + "信任"你
SPA 一个页面 + JS 切换,体验好但没 SEO
CSR 组件在浏览器渲染,爬虫抓不到
SSR 组件在服务器渲染,返回完整 HTML
文件即路由 文件夹名 = 网址,page.tsx = 页面
layout 外壳,{children} 是留的洞
Server Component 能查数据出 HTML,负责 SEO
'use client' 标记交互组件:先 SSR 再客户端水合,不是"只在浏览器渲染"
Hydration 水合 浏览器拿到 HTML 后挂载 JS、绑定事件、激活交互
route.ts 接口 导出的 GET/POST 函数 = 对应 HTTP 方法的数据接口
全栈闭环 页面 fetch /apiroute.ts 出数据 → setState 重新渲染

一句口诀

SPA 体验好但没 SEO,Next.js 靠"服务器渲染组件"把 SEO 补上------文件即路由、布局套娃;要交互就标 'use client',先 SSR 再水合;前端 fetch + route.ts 接口,就是一个完整的全栈闭环。

核心代码骨架

tsx 复制代码
// app/layout.tsx
export default function RootLayout({ children }) {
  return (
    <html lang="zh">
      <body>
        <nav>导航栏</nav>
        {children}   {/* 别忘了这一行 */}
      </body>
    </html>
  );
}

// app/about/page.tsx ------ 服务器组件,直接出 HTML
export default function About() {
  return <h1>About Us</h1>;  // 服务器端渲染成 HTML,爬虫直接抓到
}
ts 复制代码
// app/api/todos/route.ts ------ 后端接口
export async function GET() {
  return Response.json(todos);
}
export async function POST(req: Request) {
  const body = await req.json();
  todos.push({ id: +Date.now(), content: body.content, completed: false });
  return Response.json(todos.at(-1));
}
tsx 复制代码
// app/todos/page.tsx ------ 前端页面,要交互就标 'use client'
'use client';
// ... useState / useEffect / fetch("/api/todos") / todos.map(...)

希望这篇文章对你有帮助!尤其是刚开始接触 Next.js、被 SEO 和 SSR 绕晕的同学,看完能有个清晰的整体图景。有问题欢迎在评论区交流~ 觉得有用的话,求个点赞收藏 🔥

相关推荐
进击的蛋蛋3 小时前
JS对象的遍历
javascript·面试
淡海水3 小时前
C#与常用数据结构源码剖析-全篇导览
开发语言·数据结构·unity·面试·职场和发展·c#
杨航 AI7 小时前
** AI 面试算法 50 题清单**,不是单纯的 LeetCode 题号列表,而是按照“**考察概率 × 面试价值 × 你当前准备方向**”排序
算法·leetcode·面试
无我Code13 小时前
开发者-2026年中总结
前端·面试·程序员
kyriewen17 小时前
今年裁了16万技术人——但字节前端岗反而涨了23%
前端·javascript·面试
众人皆醒我独醉19 小时前
训练加速实战:Flash Attention、Gradient Checkpointing 与数据流水线
后端·面试·gpu
Loveyourself19 小时前
Claude Code Memory 总体系核心代码逐行解析
面试·agent
王林不想说话19 小时前
ES6 到 ES2026 全面进阶指南:新特性、原理、示例与工程落地
前端·javascript·面试
黄敬峰19 小时前
TypeScript 必考题:type 和 interface 的区别一次搞懂
面试