从 CSR 到 Server Component:吃透 Next.js 16 App Router 路由、布局与 SEO

从 CSR 到 Server Component:用 Next.js 16 App Router 吃透路由、布局与 SEO

前端开发者第一次接触 Next.js,最容易产生两个疑问:

  1. 已经有 React 了,为什么还需要 Next.js?
  2. Next.js 所说的服务端渲染,到底解决了什么问题?

要回答它们,不能只记住 page.tsxlayout.tsx 这些文件名,而要先理解页面究竟在哪里生成,以及浏览器第一次请求 URL 时究竟拿到了什么。

本文会从 CSR 与服务端渲染的差异出发,逐步实现首页、关于页、待办页、后台页和设置页,并通过根布局与嵌套布局理解 App Router 的核心工作方式。最后再回到 SEO,看看一个对搜索引擎友好的页面需要具备哪些条件。

本文使用 Next.js 16.3.3、React 19.2.8、TypeScript 5 和 Tailwind CSS 4。

一、先分清 Next.js、Nuxt 和 Nest

这几个名字很接近,但它们解决的问题并不相同:

  • Next.js 是 React 生态中的全栈框架;
  • Nuxt 是 Vue 生态中的全栈框架;
  • Nest 是偏后端的 Node.js 框架。

Next.js 不只是"帮 React 配好路由"的工具。它既能组织前端页面,也具备编写服务端逻辑的能力,还提供了服务端渲染、静态预渲染、字体优化和元数据管理等开箱即用的能力。

这类能力对内容站、产品官网和需要自然搜索流量的 AI 产品尤其重要。它们不仅要有接近单页应用的交互体验,还希望搜索引擎能够直接理解每个 URL 的标题、摘要和正文。

除了传统 SEO,还可以看到 GEO(Generative Engine Optimization,生成式引擎优化) 这个概念。用户的信息入口正在从百度、Google 等搜索引擎扩展到豆包、DeepSeek 等生成式产品;GEO 关注的是内容能否在生成式回答中被理解、呈现,并把用户带回具体内容或产品链接。本文不展开额外的 GEO 技巧,但无论入口如何变化,清晰的页面主题和有价值的正文始终是基础。

二、SPA 的体验很好,为什么 SEO 仍然是问题

传统 React SPA 通常采用 CSR,也就是 Client Side Rendering,客户端渲染

假设应用中有一个待办页面,前端路由可能会写成类似下面这样:

tsx 复制代码
<Route path="/todos" element={<Todos />} />

访问 /todos 时,服务器往往仍然返回同一个 index.html。其中最关键的内容可能只有一个用于挂载应用的节点:

html 复制代码
<div id="root"></div>
<script src="main.js"></script>

随后浏览器下载并执行 JavaScript,React 才把 Todos 组件渲染进 #root。如果组件还要在 useEffect 中请求数据,页面正文会出现得更晚。

整个过程可以概括为:

text 复制代码
服务器返回 HTML 空壳和 JavaScript
              ↓
浏览器下载并执行 JavaScript
              ↓
React 在浏览器中生成页面
              ↓
用户看到完整内容

CSR 的优点非常明显:页面切换流畅、组件可以在前端按需挂载,切换路由时也不必每次整页刷新。很多移动端应用中的 WebView 页面,本质上也在利用 Web 技术快速复用界面和业务逻辑。

但从 SEO 的角度看,CSR 增加了理解页面的成本。搜索引擎第一次获得的 HTML 可能没有正文,必须继续执行 JavaScript、等待异步请求,才能知道页面讲了什么。现代搜索引擎并非完全不能处理 JavaScript,但与正文已经存在于 HTML 中的页面相比,纯 CSR 页面的抓取和索引链路更长,也更不稳定。

问题的根本不是"React 不支持 SEO",而是:有价值的内容是否已经出现在服务器返回的 HTML 中。

三、把 React 组件放到服务端运行

React 组件本质上是根据数据描述 UI 的函数。如果一个组件只做"获取数据并生成界面",不依赖浏览器 DOM、点击事件或 useEffect,它就很适合在服务器执行。

在传统前后端分离项目中,访问待办接口可能得到一个 JSON 数组,浏览器再根据 JSON 生成 UI:

text 复制代码
/api/todos → JSON 数据 → 浏览器执行 React → 页面

在具备服务端渲染能力的全栈项目中,服务器可以把组件和数据结合起来,先生成可直接显示的页面内容:

text 复制代码
组件 + todos 数据 → 服务端生成 UI → HTML → 浏览器

对应的请求流程是:

text 复制代码
浏览器请求 URL
      ↓
服务器找到对应的页面组件
      ↓
组件获取数据并生成 UI
      ↓
服务器把结果作为 HTML 返回
      ↓
浏览器直接显示首屏内容

这就是理解服务端渲染的起点:以前主要让浏览器负责生成页面,现在服务器也可以提前完成这部分工作。

不过还要注意一个容易混淆的边界:

  • Server Component 描述组件主要在哪里执行;
  • SSR 强调页面是否在每次请求到来时动态生成;
  • 如果页面没有动态数据,Next.js 也可能在构建阶段提前生成静态 HTML。

因此,"默认是 Server Component"并不等于"每次请求都执行一次 SSR"。本文的几个页面都没有动态取数,构建时可以预渲染成静态页面;但它们的首屏 HTML 同样能够直接包含正文,对 SEO 依然友好。

四、创建一个 Next.js 全栈项目

使用官方脚手架创建项目:

bash 复制代码
npx create-next-app@latest

选择默认配置后,项目包含 React、TypeScript、ESLint、Tailwind CSS 和 App Router。常用脚本如下:

json 复制代码
{
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "lint": "eslint"
  }
}

它们分别用于启动开发服务器、生成生产构建、运行生产服务和检查代码规范。

本文只关注真正参与页面组织的部分,目录可以简化为:

text 复制代码
app/
├── layout.tsx
├── page.tsx
├── globals.css
├── about/
│   └── page.tsx
├── todos/
│   └── page.tsx
└── dashboard/
    ├── layout.tsx
    ├── page.tsx
    └── settings/
        └── page.tsx

这棵目录树已经把所有页面的 URL 结构表达出来了。

五、App Router:文件系统就是路由表

App Router 的核心思想是"约定优于配置":不需要手写一张路由表,文件夹表示 URL 片段,page.tsx 表示这个路径可以被访问。

上面的目录会形成如下映射:

文件位置 URL 页面作用
app/page.tsx / 首页
app/about/page.tsx /about 关于页
app/todos/page.tsx /todos 待办页
app/dashboard/page.tsx /dashboard 后台首页
app/dashboard/settings/page.tsx /dashboard/settings 后台设置页

1. 首页

首页只是一个普通 React 函数组件:

tsx 复制代码
// Server Component:React 在服务端环境中运行并生成页面内容
export default function Home() {
  return <h1>Hello nextjs</h1>;
}

文件顶部没有 'use client',所以它默认是 Server Component。它不需要 useEffect,也没有点击事件,只负责生成标题,非常适合在服务端执行。

2. 关于页

about 路径段中导出页面组件:

tsx 复制代码
function About() {
  return <h1>About Us</h1>;
}

export default About;

启动开发服务器后,直接访问 http://localhost:3000/about 就能到达这个页面,不需要额外配置 /about 路由。

3. 待办页

待办页当前只用于验证路由:

tsx 复制代码
export default function TodosPage() {
  return <>Todos</>;
}

它对应 /todos。这里使用 Fragment 返回文本,避免为了一个根节点额外添加无意义的标签。

4. 后台首页与设置页

后台首页对应 /dashboard

tsx 复制代码
export default function Page() {
  return <h1>Hello, Dashboard! 后台管理系统</h1>;
}

设置页位于更深一层的目录,因此对应 /dashboard/settings

tsx 复制代码
export default function Page() {
  return <h1>Hello, Settings!</h1>;
}

这里两个组件都可以命名为 Page,因为它们属于不同模块,最终 URL 由目录位置决定,而不是由函数名决定。

六、Root Layout:只写一次的全站公共骨架

一个网站通常有多个页面共享导航、字体和整体结构。如果每个页面都重复这些代码,不仅啰嗦,也很难统一维护。

App Router 使用 layout.tsx 解决这个问题。应用根目录中的布局会包裹所有页面:

tsx 复制代码
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import Link from "next/link";
import "./globals.css";

const geistSans = Geist({
  variable: "--font-geist-sans",
  subsets: ["latin"],
});

const geistMono = Geist_Mono({
  variable: "--font-geist-mono",
  subsets: ["latin"],
});

export const metadata: Metadata = {
  title: "Create Next App",
  description: "Generated by create next app",
};

export default function RootLayout({ children }: LayoutProps<"/">) {
  return (
    <html
      lang="en"
      className={`${geistSans.variable} ${geistMono.variable} h-full antialiased`}
    >
      <body className="min-h-full flex flex-col">
        <nav>
          <ul>
            <li><Link href="/">首页</Link></li>
            <li><Link href="/about">关于</Link></li>
            <li><Link href="/dashboard">后台管理</Link></li>
          </ul>
        </nav>
        {children}
      </body>
    </html>
  );
}

这段代码有五个值得注意的部分。

1. children 是当前路由的页面内容

布局并不知道当前访问的是首页、关于页还是后台页,它只预留 {children}。Next.js 匹配 URL 后,会把对应页面放到这里。

访问 /about 时,可以把最终结构理解为:

tsx 复制代码
<RootLayout>
  <About />
</RootLayout>

因此,根导航只写一次,却会出现在所有页面上。

2. 根布局负责 <html><body>

根布局给整个应用提供 HTML 骨架。h-fullmin-h-fullflex flex-col 用来建立至少占满视口高度的纵向布局;字体变量也被挂到 <html> 上,供全局样式使用。

如果页面主要使用中文,实际项目中可以把 lang="en" 调整为 lang="zh-CN",让文档语言与正文一致。

3. next/font 统一管理字体

GeistGeist_Mono 分别生成 CSS 变量:

tsx 复制代码
variable: "--font-geist-sans"
variable: "--font-geist-mono"

全局样式再把它们映射到普通字体和等宽字体。布局只需把生成的变量类名放到根元素,不必在每个页面重复配置。

这里还要注意"声明字体变量"和"真正使用字体"的区别。当前 body 明确设置了 Arial, Helvetica, sans-serif,页面实际会优先使用这组字体;Geist 变量虽然已经注册并映射到 Tailwind 主题,但还需要给元素添加 font-sansfont-mono,或者在 CSS 的 font-family 中直接引用对应变量,字体才会真正应用。

导航栏没有使用普通字符串拼接路由,而是使用 Next.js 提供的 Link

tsx 复制代码
<Link href="/dashboard">后台管理</Link>

href 与目录产生的 URL 一一对应。这样既保留真实链接的语义,也能使用框架提供的客户端路由体验。

5. LayoutPropschildren 提供类型

LayoutProps<"/"> 是 Next.js 生成的全局类型辅助工具。对于根布局,它会把 children 标注为 React.ReactNode,同时与具体布局路由关联,避免在严格 TypeScript 配置下出现隐式 any

七、嵌套 Layout:只包裹某一个路由分支

根布局适合全站导航,但后台通常还需要自己的二级导航。此时可以在 dashboard 路径段中继续创建 layout.tsx

tsx 复制代码
import Link from "next/link";

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

这个布局不会影响 /about/todos,只会包裹 /dashboard 及其子路由。

访问 /dashboard/settings 时,实际嵌套关系可以理解为:

tsx 复制代码
<RootLayout>
  <DashboardLayout>
    <SettingsPage />
  </DashboardLayout>
</RootLayout>

把几个地址放在一起比较,就更容易看清布局的作用范围:

text 复制代码
/                    RootLayout → Home
/about               RootLayout → About
/todos               RootLayout → TodosPage
/dashboard           RootLayout → DashboardLayout → DashboardPage
/dashboard/settings  RootLayout → DashboardLayout → SettingsPage

这就是嵌套路由和嵌套布局之间的对应关系:目录负责 URL 层级,布局负责 UI 层级。

示例中的 children 必须有明确类型。由于 TypeScript 开启了严格模式,如果直接写成下面这样:

tsx 复制代码
export default function DashboardLayout({ children }) {
  // ...
}

类型检查会提示 children 隐式具有 any 类型。使用 LayoutProps<"/dashboard"> 后,代码既保持简洁,也通过了布局路由约束。

八、全局样式与 Tailwind CSS 如何接入布局

全局样式通过根布局中的这一行进入整个应用:

tsx 复制代码
import "./globals.css";

样式文件首先引入 Tailwind CSS:

css 复制代码
@import "tailwindcss";

随后定义亮色、暗色背景以及字体变量:

css 复制代码
:root {
  --background: #ffffff;
  --foreground: #171717;
}

@theme inline {
  --color-background: var(--background);
  --color-foreground: var(--foreground);
  --font-sans: var(--font-geist-sans);
  --font-mono: var(--font-geist-mono);
}

@media (prefers-color-scheme: dark) {
  :root {
    --background: #0a0a0a;
    --foreground: #ededed;
  }
}

body {
  background: var(--background);
  color: var(--foreground);
  font-family: Arial, Helvetica, sans-serif;
}

这里的职责分工很清楚:

  • :root 保存全局颜色变量;
  • @theme inline 把 CSS 变量映射给 Tailwind 主题;
  • prefers-color-scheme: dark 根据系统偏好切换深色配色;
  • body 应用页面的背景、前景色,并把当前字体栈设为 Arial、Helvetica 和通用无衬线字体。

页面组件因此可以专注于结构,而无需在每一个页面重复基础样式。

九、SEO 不是只加一个 title,而是三层共同作用

一个页面要对搜索引擎友好,至少要考虑三个层次。

第一层:用元数据说明"你是谁"

传统 HTML 会直接编写:

html 复制代码
<title>页面标题</title>
<meta name="description" content="页面摘要" />
<meta name="keywords" content="关键词" />

在 App Router 中,可以通过带类型的 metadata 对象描述这些信息:

tsx 复制代码
import type { Metadata } from "next";

export const metadata: Metadata = {
  title: "Next.js App Router 入门",
  description: "理解 Next.js 的文件路由、嵌套布局、服务端组件与 SEO。",
  keywords: ["Next.js", "App Router", "Server Component", "SEO"],
};

脚手架默认的 Create Next AppGenerated by create next app 只适合占位。真正发布网站时,标题应该回答"这是谁",描述应该回答"它做什么、能提供什么价值"。

第二层:提供真正有价值的正文

元数据只能概括页面,用户最终访问页面是为了内容。没有正文,再精心设计的关键词也无法替代内容本身。

对于文章站来说,每一篇文章都应该有独立 URL、独立标题和完整正文。例如 /post/id 可以表示某一篇具体文章,而不是让整个站点始终只有一个入口页面。可被索引的高质量页面越完整,整个内容站越容易积累搜索价值。

第三层:让首屏 HTML 直接包含内容

如果标题和正文只能等浏览器执行 JavaScript 后才出现,搜索引擎就要付出额外的渲染成本。服务端组件、SSR 或静态预渲染可以让服务器返回的 HTML 直接带上主要内容,这才是渲染层面对 SEO 的帮助。

可以把三层关系概括为:

text 复制代码
元数据:告诉搜索引擎页面是什么
正文内容:告诉用户页面有什么价值
服务端可读 HTML:让内容更直接地被抓取和理解

三者缺一不可。只做服务端渲染却没有内容,不会自动获得良好 SEO;只写元数据但正文仍是空壳,也没有解决渲染链路的问题。

十、从 URL 到页面,完整走一遍

最后以 /dashboard/settings 为例,把整条链路串起来:

  1. 浏览器请求 /dashboard/settings
  2. App Router 根据目录找到设置页;
  3. 设置页先被 dashboard 布局包裹,得到后台导航;
  4. 整个后台区域再被根布局包裹,得到全站导航、字体和 HTML 骨架;
  5. Next.js 在服务端生成可直接显示的页面结果;
  6. 浏览器拿到包含主要内容的 HTML 并显示页面;
  7. 后续点击 Link 时,继续使用 Next.js 的应用内导航能力。

这个过程同时体现了 App Router 的三个重要约定:

  • page.tsx 决定某个 URL 展示什么;
  • layout.tsx 决定一组页面共享什么;
  • 目录嵌套同时决定 URL 层级与布局嵌套关系。

总结

Next.js 的价值不只是让 React 项目多了几个特殊文件,而是把路由、布局、服务端执行和 SEO 串成了一套统一模型。

从本文的几个页面可以得到这些关键结论:

  1. CSR 在浏览器中生成页面,交互体验好,但首个 HTML 可能缺少正文;
  2. Next.js App Router 中的组件默认是 Server Component,适合在服务端生成内容;
  3. Server Component 与请求时 SSR 不是同一个概念,静态预渲染同样可以返回完整 HTML;
  4. page.tsx 配合目录自动形成路由,不需要手写路由表;
  5. layout.tsx 提供全站骨架,嵌套 layout.tsx 提供局部公共界面;
  6. Link 把各个文件路由连接起来,children 则把页面填入对应布局;
  7. SEO 需要元数据、优质正文和服务端可读 HTML 三层配合。

掌握这些内容后,再去学习动态路由、服务端数据请求和后端接口,就会有一条非常清楚的主线:无论项目继续变得多复杂,本质仍然是在回答两个问题------某个 URL 应该找到哪个页面,以及这个页面应该由哪些布局和数据共同生成。

相关推荐
七牛开发者1 小时前
拆解 DeepSeek Harness:Profile 与 Bundle 如何装配运行时
前端·javascript·后端
葡萄城技术团队1 小时前
表格智能体系列 · 1:一句话怎么变成一次表格操作
前端
梦曦i1 小时前
Vue-Router 2.4.0 新增可控重定向功能
前端·uni-app
deli0070071 小时前
Markdown 实时预览器:左边写右边看,排版效率翻倍
前端·ai编程
郑州光合科技余经理2 小时前
海外版外卖系统架构:订单怎么流转、权限怎么分
java·开发语言·前端·系统架构·uni-app·php·ai编程
pengyu2 小时前
【Kotlin 协程修仙录 · 炼虚境 · 初阶】 | 虚空造物:Channel 基础与协程间通信的管道艺术
android·前端·kotlin
提线木偶2 小时前
CORS 到底谁说了算?一份跨域配置的避坑指南
前端·后端
网安蟹佬霸3 小时前
区块链与智能合约安全实战:从Solidity审计到DeFi漏洞深度剖析
运维·前端·网络·安全·自动化·区块链·智能合约
念何架构之路3 小时前
Gin响应渲染
前端·javascript·gin