为什么你的 SPA 搜不到?——用 Next.js 搞懂 SEO 与 SSR

📅 2026/8/14 15:59:40
为什么你的 SPA 搜不到?——用 Next.js 搞懂 SEO 与 SSR
文章目录SEO 搜索引擎优化SPA 的好处SPA 的短板天生不友好 SEO创建全栈项目初始化项目SEO 友好是怎么实现的CSR 和 SSRCSR客户端渲染SSR服务端渲染CSR vs SSR 一张表看懂Next.js 语法约定大于一切App Router文件即路由布局与嵌套渲染规则先 layout再 page客户端组件为什么需要「客户端组件」一个关键误区客户端组件「只」在浏览器渲染水合Hydration把静态页面激活成可交互页面实战一个完整的客户端组件服务端组件 vs 客户端组件 一张表SEO 的基本做法第一层你是谁元信息第二层做内容第三层SSR 服务端渲染约定文件速查表全文总结核心知识点复盘常见问题 / 避坑指南Next 是 React 的全栈框架Nuxt 是 Vue 的全栈框架Nest 是后端的 Node.js 框架——三个名字很像但定位完全不同。一句话区分Next.js 既能写页面前端也能写 API后端是典型的全栈框架它背靠 VercelSSR 和 SEO 能力做得非常出色这也是为什么大量 AI 产品的官网都选择用 Next.js 来搭建。本文会从 SEO 这个切入口讲起一步步讲清楚 SPA 的短板、CSR 与 SSR 的本质区别然后实战创建一个 Next.js 项目最后落到 SEO 的具体做法上。读完你会明白为什么同样是 ReactNext.js 能解决 SPA 最痛的 SEO 问题。SEO 搜索引擎优化SPA 的好处SPASingle Page Application单页应用是现在前端最主流的开发方式它的体验确实很好组件在前端挂载页面数据通过useEffect等钩子异步请求不需要整页刷新。前端路由页面切换走前端路由速度快、体验流畅接近原生 App 的顺滑感。这也解释了 SPA 的由来——它就是照着原生 App 的体验做的。原生 App 要同时写 iOS 和 Android 两套代码而 SPA 只需要写一套 HTML就能在浏览器里跑出接近 App 的体验。现在很多 App 里 80% 的页面其实都是用 WebView 组件承载的 SPA 页面前端一套代码到处复用。SPA 的短板天生不友好 SEO但 SPA 有一个致命短板它压根不是为了搜索引擎设计的。在 PC 时代百度、谷歌是流量的入口SEO 就是网站的命脉。但 SPA 的页面在爬虫眼里几乎是空的。看一个典型的 SPA 入口文件!-- spa-demo/index.html --!doctypehtmlhtmllangenheadmetacharsetUTF-8/titlespa-demo/title/headbody!-- 爬虫爬到的就只有这个空空的挂载点 --dividroot/div!-- 真正的页面内容要靠这段 JS 在浏览器里跑出来 --scripttypemodulesrc/src/main.jsx/script/body/html再配合入口 JS// spa-demo/src/main.jsximport{StrictMode}fromreactimport{createRoot}fromreact-dom/clientimportAppfrom./App.jsx// React 把 App 组件渲染进 #root 这个空 div 里createRoot(document.getElementById(root)).render(StrictModeApp//StrictMode,)看懂了吗服务器返回的 HTML 里只有一个div idroot/div所有真正的页面内容都是等浏览器下载并执行main.jsx、App.jsx之后才由 JavaScript变出来的。爬虫搜索引擎的机器人去抓取这个 URL 时拿到的是执行 JS 之前的原始 HTML——也就是那个空空的#root。所以对爬虫来说这个页面什么都没有自然无法收录、无法给关键词排名。一句话总结SPA 的页面是浏览器里长出来的而爬虫看不到这个过程只看到了光秃秃的#root。而现在的现实是AI 产品、Agent 站点如雨后春笋掘金、CSDN 这类老牌内容站流量几乎都来自 SEO。想要让搜索引擎把你的内容收录并推荐给别人光靠 SPA 是不够的。于是全栈且 SEO 友好的 Next.js以及 Vue 侧的 Nuxt.js就成了主流选择。一句话概括这条演进路径#rootSPA空壳 → SEO 友好React JSX 在服务端编译成 HTMLNext.js创建全栈项目初始化项目创建 Next.js 项目非常简单一条命令npx create-next-applatest运行后会有一系列交互式提问是否用 TypeScript、Tailwind、ESLint、App Router 等一路回车选默认配置即可。创建完成后项目里就集成了完整的全栈技术栈依赖作用react/react-domReact 界面库负责写组件typescript类型系统让代码更健壮tailwindcss原子化 CSS 方案快速写样式eslint代码风格与规范检查nextNext.js 框架本体提供路由、渲染、API 能力顺带提一个更前沿的概念GEOGeneration Engine Optimization生成式引擎优化。当用户入口从搜索引擎变成豆包、ChatGPT 这类 AI 助手时我们要做的是让 AI 在生成答案时能带上我们的内容和购买链接。这是 SEO 在 AI 时代的新形态可以作为扩展了解。SEO 友好是怎么实现的Next.js 之所以 SEO 友好核心秘密在于它把在浏览器里渲染这件事挪到了服务器上做。要彻底讲清这一点得先分清两个概念CSR 和 SSR。CSR 和 SSRSEO 的根本问题其实是同一个问题组件到底在哪里渲染答案只有两个——在浏览器里客户端还是在服务器上服务端。CSR客户端渲染CSRClient Side Rendering客户端渲染就是 SPA 的渲染方式流程如下用户访问/todos服务器返回一个几乎空的index.html里面只有#root和script。浏览器下载并执行main.js、App.jsx、Todos.jsx这些 JS。React 在用户的浏览器里把Todos /组件挂载进#root页面才真正显示出来。代码层面前端路由长这样// SPA 里的前端路由 Route path/todos element{Todos /} /这个Todos /组件是懒加载的用户点击/todos链接时浏览器才去下载Todos.jsx然后在本地客户端渲染。整个过程不刷新页面体验很好——但内容都在 JS 里爬虫拿不到。SSR服务端渲染SSRServer Side Rendering服务端渲染的思路完全反过来在服务器上就把 React 组件 数据编译成 HTML 字符串直接返回给浏览器。对比一下传统 Java 全栈和 Next.js 全栈的区别就能秒懂Java 全栈前后端分离后端/todos路由 → controller 处理请求 → service 查 MySQL → 把todos数据以 JSON 数组的形式返回给前端 → 前端拿到 JSON 再渲染。后端只管数据页面是前端拼出来的。Next.js 全栈/todos路由返回的不是 JSON而是 React 组件编译之后的 HTML。服务端拿到todos数据直接和 JSX 模板拼在一起输出完整网页。用公式表达 SSR 的核心jsx组件模板 todos数据 服务端渲染出来的 HTML关键在于只要组件不做事件监听、不用useEffect也就是不需要在浏览器里交互它就能被当成一个纯函数在服务端用 Node.js 跑一遍把结果格式化成字符串注意服务器端没有 DOM本质是字符串的格式化然后发给浏览器。这样爬虫抓到的就是含完整内容的 HTMLSEO 自然就好了。CSR vs SSR 一张表看懂维度CSR客户端渲染SSR服务端渲染渲染位置用户浏览器服务器首屏 HTML空的#root完整内容爬虫能拿到内容吗❌ 拿不到✅ 能拿到SEO 友好度差好典型框架SPAReact/Vue 纯前端Next.js / Nuxt.js首屏速度慢要先下 JS 再跑快直接给 HTML交互体验强不刷新、流畅需要额外水合hydrateNext.js 语法约定大于一切Next.js 最大的设计哲学是约定大于配置——不用写路由配置文件文件放在哪里、叫什么名字路由就是什么。这是 Next.js 给 React 开发者准备的开箱即用利器。App Router文件即路由在app/目录下文件夹就是路由路径page.tsx就是页面。看下面这个目录结构和它对应的访问地址app/ ├── layout.tsx # 根布局所有页面共用 ├── page.tsx # 访问 / 首页 ├── about/ │ └── page.tsx # 访问 /about └── dashboard/ ├── layout.tsx # dashboard 专用布局 ├── page.tsx # 访问 /dashboard └── settings/ └── page.tsx # 访问 /dashboard/settings对应关系一目了然建文件夹 建嵌套路由建page.tsx 建页面。不需要任何react-router式的路由配置。布局与嵌套layout.tsx是布局文件用来放导航栏等多个页面共用的部分。它会把子页面children包裹起来实现布局复用。根布局app/layout.tsx// app/layout.tsx —— 根布局包裹所有页面importtype{Metadata}fromnext;importLinkfromnext/link;import./globals.css;// 导出 metadata就是给搜索引擎看的名片exportconstmetadata:Metadata{title:我的全栈网站,description:这是一个用 Next.js 搭建的全栈 SEO 友好站点,keywords:[Next.js,全栈,SSR,SEO],};exportdefaultfunctionRootLayout({children}:{children:React.ReactNode}){return(html langzh-CNbody{/* 导航栏三个页面共用的部分放在 layout 里只写一次 */}navulliLink href/首页/Link/liliLink href/about关于/Link/liliLink href/dashboard后台管理/Link/li/ul/nav{/* children 就是当前路由对应的 page.tsx */}{children}/body/html);}首页app/page.tsx注意这是一个服务端组件默认就在服务器上跑// app/page.tsx —— 访问 /// 在 App Router 中组件默认是「服务端组件」在服务器上用 Node 渲染// jsx 在这里被编译成 html 字符串直接返回给浏览器和爬虫exportdefaultfunctionHome(){returnh1Hello World/h1;}app/about/page.tsx// app/about/page.tsx —— 访问 /aboutexportdefaultfunctionAbout(){returnh1About Us/h1;}dashboard的专用布局app/dashboard/layout.tsx// app/dashboard/layout.tsx —— 只作用于 /dashboard 及其子页面importLinkfromnext/link;exportdefaultfunctionDashboardLayout({children}:{children:React.ReactNode}){return(divnavLink href/dashboard/settings设置/Link/nav{/* 这里渲染 /dashboard/page.tsx 或 /dashboard/settings/page.tsx */}{children}/div);}渲染规则先 layout再 page一个路由被访问时渲染顺序是固定的访问 /dashboard/settings ↓ 先渲染根布局 app/layout.tsx ↓ 再渲染 dashboard 布局 app/dashboard/layout.tsx嵌套的布局会层层包裹 ↓ 最后渲染页面 app/dashboard/settings/page.tsx也就是说layout.tsx是外壳page.tsx是内容。外壳层层嵌套内容最终被塞进最内层。这样的设计让导航栏、侧边栏这类公共部分只需写一次。客户端组件为什么需要「客户端组件」Next.js 默认把 React 组件当成服务端组件Server Component在服务器上把 JSX 编译成 HTML直接返回给浏览器和爬虫这就是 SSR 开发模式jsx - htmlSEO 因此友好。但现实里有不少页面是强交互的表单输入、点击按钮、实时更新列表……这些依赖浏览器的状态和事件服务器上跑不了。比如待办事项页用户要输入文字、点「添加」按钮、拿到接口数据实时渲染——这些能力需要useState、useEffect、onClick只能发生在浏览器里。于是 Next.js 提供了use client这个标记在文件顶部写一句use client就把这个组件声明为客户端组件。一个关键误区客户端组件「只」在浏览器渲染很多人以为加了use client组件就完全不在服务器渲染了。恰恰相反——客户端组件依然会先在服务器上渲染一遍再去浏览器渲染第二遍。可以把它理解成一个「先包好、后下锅」的过程服务器先渲染一遍把组件里能静态确定的部分比如h1待办事项/h1这个标题先编译成 HTML 字符串随页面一起发给浏览器。这一步就像包好水饺、冻上、给你送过来——先给你一版现成的 HTML。浏览器再渲染一遍拿到静态 HTML 之后浏览器下载并执行客户端 JS挂载 React、绑定点击事件、执行useEffect、把页面激活成能交互的状态。这一步就是水煮——冻水饺光送到手还不能吃得下锅煮熟。这下锅煮熟的过程官方术语叫水合Hydration。水合Hydration把静态页面激活成可交互页面水合Hydration浏览器拿到服务器返回的静态 HTML 之后挂载客户端 JS、绑定点击事件、激活交互能力的过程。一句话服务器负责把 HTML 这个壳给你浏览器负责把它激活成真正能点的页面。因此客户端组件会执行两次第一次在服务器生成 HTML 字符串。此时useState是初始值、useEffect不会跑、事件还没绑定。第二次在客户端真正挂载、执行useEffect、绑定事件相当于在服务器给的那版 HTML 上打补丁。实战一个完整的客户端组件看待办事项页app/todos/page.tsx就是一个典型的客户端组件// app/todos/page.tsx —— 访问 /todosuse client;// ① 声明这是客户端组件因为要交互import{useState,useEffect}fromreact;import{typeTodo}from./types;exportdefaultfunctionTodosPage(){// ② useState组件的状态只能存在浏览器里const[todos,setTodos]useStateTodo[]([]);// 待办列表const[text,setText]useStatestring();// 输入框内容// ③ 拉取待办数据constfetchTodosasync(){constresawaitfetch(/api/todos);constdata:Todo[]awaitres.json();setTodos(data);};// ④ 新增一条待办consthandleAddasync(){if(!text.trim())return;// 空内容直接返回awaitfetch(/api/todos,{method:POST,headers:{Content-Type:application/json},body:JSON.stringify({content:text,completed:false}),});setText();// 清空输入框fetchTodos();// 重新拉取列表让新数据显示出来};// ⑤ useEffect组件挂载后执行一次只发生在浏览器里useEffect((){fetchTodos();},[]);return(div style{{marginTop:12px}}h1待办事项/h1input value{text}onChange{(e)setText(e.target.value)}// ⑥ 事件监听placeholder请输入新的待办任务/button onClick{handleAdd}style{{marginLeft:8px}}添加/buttonul style{{paddingLeft:0,listStyle:none}}{todos.map((item)(li key{item.id}{item.content}/li))}/ul/div);}对照代码把执行两次讲透服务器第一次渲染todos还是初始值[]useEffect不执行onClick还没绑定。服务器只把h1待办事项/h1和一个空的ul渲染成 HTML 发出去。浏览器第二次渲染水合React 挂载、绑定onChange/onClick、执行useEffect→ 调fetchTodos()→ 拿到/api/todos的数据 →setTodos→ 列表才长出来。这也就解释了之前那个现象为什么「查看源代码」里能看到h1待办事项/h1却看不到列表数据——因为列表是水合之后、在浏览器里异步请求才出现的。服务端组件 vs 客户端组件 一张表维度服务端组件默认客户端组件use client是否需要声明不需要默认就是文件顶部加use client主要用途读数据、渲染静态内容交互、表单、实时更新能用useState/useEffect吗❌ 不能✅ 能能绑定事件吗❌ 不能✅ 能在服务器渲染吗✅ 只在服务器✅ 先在服务器再在客户端对 SEO好内容全在 HTML 里部分内容靠客户端补齐典型例子page.tsxHello Worldtodos/page.tsx一句话记默认组件是纯展示、跑在服务器要加交互状态、事件、副作用才加use client。加了之后也不是只在浏览器跑而是服务器先渲染一遍浏览器再水合一遍。SEO 的基本做法搞懂了原理落地到具体做法SEO 可以分成三层由浅入深。第一层你是谁元信息搜索引擎和用户第一眼看到的是页面的title、meta标签这是网站的名片。核心是三个title页面标题/titlemetanamedescriptioncontent页面描述/metanamekeywordscontent关键词/title你是谁、这个页面讲什么每个页面都该有独立标题。description你的内容能提供什么价值诱导用户点进来。keywords关键词虽然现在权重下降但仍是基础。在 Next.js 里直接通过metadata导出即可框架会自动生成对应的head标签见上一节的layout.tsx示例。这一步是让搜索引擎认识你。第二层做内容SEO 的根基永远是内容——用户为什么来你网站因为你有他需要的信息。title/description 只是把用户引进来真正留住用户和搜索引擎的是高质量的、持续更新的内容。没有内容SEO 技巧都是空中楼阁。第三层SSR 服务端渲染有了内容还不够还得让爬虫抓得到。这就是前面讲的 SSR 的价值让服务端直接吐出含完整内容的 HTML。典型场景是内容型站点比如/post/:id这种文章详情页——可能有千万篇文章。如果这些页面都是 SSR 渲染、能被搜索引擎逐篇收录那整站被收录的内容会给 SEO 带来巨大的加权。这也是新闻站、博客站、文档站大量采用 Next.js 的原因。约定文件速查表最后把 App Router 的约定文件集中成一张表方便查阅也是日常开发最高频接触的文件名作用page.tsx生成一个路由路由的页面layout.tsx布局包裹子路由可嵌套loading.tsx加载态页面数据未就绪时显示error.tsx错误边界捕获渲染错误not-found.tsx自定义 404 页route.tsAPI 路由处理 GET/POST 等请求纯后端能力全文总结这篇文章用一条主线串起了 Next.js 的核心价值——SEOSPA 体验好但 SEO 差页面在浏览器里由 JS 渲染爬虫只拿到空空的#root无法收录内容。Next.js 是全栈框架既能写页面也能写 API背靠 VercelSSR/SEO 能力突出是 AI 产品官网的主流选择。CSR 与 SSR 的本质区别只在于组件在哪里渲染——浏览器里CSR还是服务器上SSR。SSR 把jsx 数据在服务端拼成 HTML所以爬虫能抓到完整内容。Next.js 的语法核心是约定大于配置文件即路由page.tsx是页面、layout.tsx是布局层层嵌套。SEO 三件套元信息title/description/keywords→ 好内容 → SSR 服务端渲染由浅入深。核心知识点复盘SPA / CSR单页应用客户端渲染#root空壳 JS 挂载体验好但 SEO 差。SSR服务端渲染jsx 数据 → HTML 字符串SEO 好是 Next.js 的默认渲染方式。全栈Next.js 既能写页面page.tsx也能写 APIroute.ts。约定文件page/layout/loading/error/not-found/route。渲染顺序根layout→ 嵌套layout→page。元信息title你是谁、description有什么价值、keywords关键词。GEOAI 时代的 SEO 新形态让 AI 生成答案时带上你的内容。常见问题 / 避坑指南拼错约定文件名page.tsx写成apge.tsx、pgae.tsx路由不会生效访问直接 404。这是最容易犯的低级错误遇到 404 先检查文件名。layout.tsx必须存在根布局app/layout.tsx是强制的而且必须包含html和body标签否则会报错。服务端组件里别用浏览器 API默认是服务端组件里面不能用useState、useEffect、window、document这类浏览器专属的东西否则会报错。需要交互时在文件顶部加use client。移动项目后依赖失效如果你用 pnpm 管理依赖移动项目文件夹后npm run dev报Cannot find module是因为 pnpm 的node_modules里是绝对路径的软链接。删掉node_modules和.next重新pnpm install即可。别用错包管理器项目里有pnpm-lock.yaml就用pnpm install有package-lock.json就用npm install混用会产生多份锁文件、依赖不一致。元信息要一页一改每个页面的 title/description 最好独立而不是所有页面共用一个默认标题否则搜索引擎会认为内容重复影响收录质量。