)
1. 从零搭一个 Next.js SSR 博客为什么值得折腾Next.js SSR 博客说白了就是让服务器先把页面渲染成完整 HTML 再发给浏览器爬虫拿到的是带内容的页面而不是一个空壳。这件事对做技术博客的人特别重要你写了几十篇文章结果搜索引擎只收录了首页那种感觉比代码跑不通还难受。Claude Code 在这里的角色是帮你把「数据层 → SEO → 主题 → 页面」这条链路一次性铺好而不是让你在十几个配置文件之间来回翻文档。我这次要交付的东西很具体一个基于 Next.js App Router 的 SSR 博客骨架包含可复制的next.config、generateMetadata配置、暗黑模式 CSS 变量以及 Lighthouse SEO 评分和主题切换的验证步骤。适合谁适合已经会一点 React、想认真做内容站、但不想被 SEO 细节拖死的人。整条链路里模型调用统一走 TaoToken 的 Key省得你在多个平台之间切来切去。先说清楚一个前提SSR 不等于「一定 SEO 满分」。SEO 满分靠的是元数据完整、结构化数据正确、canonical 不重复、sitemap 能被抓。SSR 只是保证内容在首屏 HTML 里。这两件事要分开看下面会一步步落地。2. TaoToken 前置统一 Key 与 Claude Code 接入在动手写代码之前先把模型调用这条线理顺。TaoToken 提供的是统一的 API 入口你拿一个 Key 就能在 Claude Code 这类工具里调用模型不用为每个模型单独配一套凭证。官网入口是 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content API 基址是 https://taotoken.net/api 。接入 Claude Code 的典型做法是在环境变量里配置基址和 Key。你可以这样操作export ANTHROPIC_BASE_URLhttps://taotoken.net/api export ANTHROPIC_API_KEY你的_TaoToken_Key配好之后Claude Code 发出的请求就会走 TaoToken 的统一入口。这里有个细节不同版本的 Claude Code 读取的环境变量名可能略有差异如果发现没生效先确认你用的工具文档里写的是ANTHROPIC_BASE_URL还是别的名字。Key 的创建在控制台的 API Keys 页面地址是 https://taotoken.net/console/api-keys 建议给这个 Key 起个能认出用途的名字比如nextjs-blog-dev方便后面轮换。注意Key 不要写进前端代码或提交到 Git 仓库。放在.env.local里并确保.gitignore包含它。如果你更想先在网页里验证模型能不能正常对话可以用模型对话入口 https://taotoken.net/models 快速试一句如果是长期做编码和 Agent 任务Coding Plan 页面 https://taotoken.net/coding-plan 会更合适额度模型不一样。接入文档在 https://taotoken.net/doc 遇到参数问题先查这里。3. 可复制配置next.config、metadata 与暗黑模式骨架3.1 next.config 与项目初始化先建项目。用 App Router因为generateMetadata和sitemap.ts这些能力都在这一代里npx create-next-applatest my-blog --typescript --tailwind --app --src-dir cd my-blog npm install next-themes gray-matter remark remark-html reading-timenext.config.mjs里SSR 博客最该关注的是图片和重定向。一个够用的骨架/** type {import(next).NextConfig} */ const nextConfig { reactStrictMode: true, images: { formats: [image/avif, image/webp], remotePatterns: [ { protocol: https, hostname: ** }, ], }, async redirects() { return [ { source: /feed, destination: /feed.xml, permanent: true }, ]; }, }; export default nextConfig;images.formats让 Next 优先输出 AVIF/WebP这对 Lighthouse 的 Performance 分有直接帮助。redirects是给 RSS 做个别名老读者用/feed也能跳过去。3.2 metadata 配置SEO 的核心App Router 里页面级 SEO 靠generateMetadata。下面这段可以直接放进src/app/posts/[slug]/page.tsximport type { Metadata } from next; import { getPost } from /lib/posts; const SITE_URL https://your-domain.com; export async function generateMetadata( { params }: { params: { slug: string } } ): PromiseMetadata { const post await getPost(params.slug); if (!post) return { title: 文章未找到 }; const url ${SITE_URL}/posts/${post.slug}; return { title: post.title, description: post.excerpt, alternates: { canonical: /posts/${post.slug} }, openGraph: { type: article, url, title: post.title, description: post.excerpt, publishedTime: post.date, modifiedTime: post.updated || post.date, tags: post.tags, }, twitter: { card: summary_large_image, title: post.title, description: post.excerpt, }, robots: { index: true, follow: true }, }; }这里几个点值得单独说。alternates.canonical是防重复收录的关键尤其是你同时有分页和标签页的时候。openGraph.type设成article而不是默认的website社交平台抓取时才会按文章处理。robots显式写index: true比不写更稳某些爬虫对缺省值的处理不一致。结构化数据用 JSON-LD 注入放在页面组件里const jsonLd { context: https://schema.org, type: Article, headline: post.title, datePublished: post.date, dateModified: post.updated || post.date, author: { type: Person, name: post.author || 博主 }, mainEntityOfPage: { type: WebPage, id: url }, };然后在 JSX 里用script typeapplication/ldjson dangerouslySetInnerHTML{{ __html: JSON.stringify(jsonLd) }} /输出。Lighthouse 的 SEO 审计会检查这个。3.3 暗黑模式 CSS 变量骨架暗黑模式最容易踩的坑是「闪白」页面先渲染亮色JS 加载后再切暗色用户眼前一白。解法是用next-themes配合 CSS 变量让主题在 HTML 上就定好。src/app/globals.css里定义变量:root { --color-bg: #ffffff; --color-text: #1a1a2e; --color-border: #e5e7eb; --color-accent: #3b82f6; } .dark { --color-bg: #0f172a; --color-text: #e2e8f0; --color-border: #334155; --color-accent: #60a5fa; } body { background-color: var(--color-bg); color: var(--color-text); transition: background-color 0.3s ease, color 0.3s ease; }主题提供者包在根布局里use client; import { ThemeProvider } from next-themes; export default function Providers({ children }: { children: React.ReactNode }) { return ( ThemeProvider attributeclass defaultThemesystem enableSystem disableTransitionOnChange {children} /ThemeProvider ); }attributeclass让 next-themes 往html上加dark类正好对上上面的.dark选择器。disableTransitionOnChange是防止切换瞬间所有元素一起过渡导致的卡顿感。4. 验证请求Lighthouse SEO 评分与主题切换4.1 跑起来看首屏 HTML先构建再启动别用 dev 模式测 SEOdev 模式的输出和产物不一样npm run build npm run start打开一个文章页右键「查看网页源代码」。你要能在源码里直接看到文章标题、正文段落、link relcanonical和 JSON-LD 脚本。如果正文是空的说明渲染没走 SSR检查页面组件里有没有误加use client——加了就变客户端渲染了。4.2 Lighthouse SEO 审计用 Chrome DevTools 的 Lighthouse 面板或者命令行npx lighthouse http://localhost:3000/posts/your-slug --only-categoriesseo --viewSEO 满分通常卡在这几项document没有title、缺 meta description、链接不可抓取、robots.txt无效。前面 metadata 配好了这几项基本能过。如果 SEO 分没到 100先看审计报告里具体哪条标红别盲目改。4.3 主题切换验证验证暗黑模式有没有闪白最直接的办法是把系统主题切成暗色然后硬刷新页面CtrlShiftR。如果页面在加载瞬间是白的再变暗说明主题注入时机不对。正常情况下html标签在首屏就带上了dark类。再验证切换按钮点一下应该在三态之间循环亮 → 暗 → 跟随系统。切换时观察有没有布局跳动如果按钮位置变了多半是图标尺寸不一致导致的给按钮固定宽高即可。5. 本篇常见错排查报错一generateMetadata不生效标题还是默认的。检查这个函数是不是写在page.tsx里而不是layout.tsx。layout 的 metadata 会被页面级覆盖但页面级必须自己导出。另外确认函数是async的返回的是Metadata对象。报错二暗黑模式切换后刷新又变回亮色。这是defaultTheme设成了light而不是system或者enableSystem没开。next-themes 把用户选择存在 localStorage如果没开 system 检测刷新时读不到偏好就会回默认值。报错三Lighthouse 提示「图片没有明确的宽高」。Next 的Image组件需要你给width和height或者用fill配合父容器定位。封面图建议用固定比例容器包一层避免布局偏移CLS拉低 Performance 分。报错四sitemap 里出现了草稿文章。在getAllPostMetas里过滤draft字段别只在列表页过滤。sitemap 生成函数如果直接读全部文件草稿也会被写进去搜索引擎抓到 404 或空页面对站点权重有影响。报错五canonical 指向了 localhost。把SITE_URL抽成环境变量构建时注入。写死在代码里本地测试没问题一部署就全错。6. 把这条链路用顺后面就轻松了这套骨架跑通之后你新增一篇文章只需要往content/posts丢一个 Markdown 文件metadata、sitemap、RSS 都会自动带上。真正花时间的不是写代码而是把 SEO 的每个细节确认到位——canonical、结构化数据、robots、sitemap一个都不能少。如果你在接入模型调用时遇到 Key 或基址的问题先去 API Keys 页面 https://taotoken.net/console/api-keys 确认 Key 状态再对照接入文档 https://taotoken.net/doc 检查环境变量名。想先验证模型响应是否正常用模型对话 https://taotoken.net/models 发一句话最快。长期做编码和 Agent 任务的话Coding Plan https://taotoken.net/coding-plan 的额度模型更适合持续调用。最后留一个我踩过的坑别在layout.tsx里写死metadata的 title页面级generateMetadata会覆盖它但如果你忘了在页面里导出用户看到的就是 layout 的默认标题。这个错误在本地开发时不容易发现部署后看搜索结果才反应过来。