基于Next.js 15与Sanity CMS构建高性能个人网站的技术实践
1. 项目概述:一个现代开发者的个人网站是如何炼成的
如果你是一名开发者,想搭建一个既能展示个人作品、又能写写技术博客,同时还得兼顾设计感和性能的个人网站,那么你大概率会和我一样,在技术选型上纠结很久。是直接用WordPress模板,还是自己从零手写?是用静态站点生成器,还是上全栈框架?我最终的选择是Next.js 15 + Tailwind CSS + Sanity CMS这套组合拳,并且在整个开发过程中,深度使用了包括Claude、Cursor 和 v0在内的AI工具来辅助。今天,我就来拆解一下我的个人网站 colemcconnell.xyz 的完整构建思路、技术细节以及那些只有亲手做过才会知道的“坑”与“爽点”。
这个项目不仅仅是一个在线名片,它更是一个实验场,用来验证现代前端开发流程与AI辅助编程结合的最佳实践。它需要满足几个核心需求:首先是极致的性能,个人网站往往是访客了解你的第一扇门,加载速度慢一秒都可能让人失去耐心;其次是内容管理的灵活性,我希望写博客像在Notion里一样简单,而不是每次更新都要去改代码;最后是可维护性与开发体验,代码结构要清晰,便于日后迭代,同时开发过程要高效、不折磨人。接下来,我会从设计思路、技术栈深度解析、具体实现步骤以及避坑指南四个方面,带你完整复现这个项目。
2. 技术栈选型与深度解析
为什么是这三驾马车——Next.js, Tailwind CSS, Sanity?这绝不是随大流的选择,而是经过深思熟虑,权衡了开发效率、性能产出和维护成本后的决定。
2.1 为什么选择 Next.js 15 作为核心框架?
Next.js 已经远远超出了一个“React框架”的范畴,它提供了一整套面向生产的解决方案。我选择最新的Next.js 15,主要是看中了它在App Router架构下带来的范式转变和性能优势。
App Router vs. Pages Router:这是最关键的决定。虽然Pages Router更成熟、生态丰富,但App Router代表了未来。它基于React Server Components,允许你更精细地控制组件的渲染位置(服务端或客户端)。对于个人网站这种内容驱动、交互相对简单的站点,这意味着绝大部分页面(如博客列表、文章详情、关于页面)都可以作为服务端组件渲染,直接将HTML发送给浏览器,省去了客户端Hydration的步骤,从而获得惊人的首次加载速度和核心网页指标。
具体优势分析:
- 服务端渲染与静态生成的无缝融合:通过
generateStaticParams和fetchAPI,我可以轻松地为所有博客文章在构建时生成静态页面(SSG),享受CDN加速的快感。同时,对于需要动态数据的部分(比如基于访客位置的问候语),又可以保留服务端渲染的能力。 - 内置的图像优化:
next/image组件自动处理图片的懒加载、响应式尺寸和现代格式转换。我的网站上有不少项目截图和个人头像,这个组件帮我节省了大量手动优化图片的时间,并且显著提升了性能。 - 简化的数据获取:在App Router中,直接在服务端组件里使用
async/await调用fetch或数据库查询,逻辑清晰直观。这对于从Sanity CMS获取文章数据来说,代码变得异常简洁。 - 逐步采用:Next.js 15确保了良好的向后兼容性,让我可以平稳地从旧模式过渡,并在需要时混合使用两种路由。
注意:App Router的学习曲线确实比Pages Router陡峭,尤其是“服务端组件”和“客户端组件”的边界需要花时间理解。我的经验是,默认将所有组件视为服务端组件,只有当你明确需要使用
useState,useEffect,onClick等客户端特性时,再在文件顶部添加‘use client’指令将其转换为客户端组件。
2.2 Tailwind CSS:从抵触到真香的效用优先CSS
曾经我也是“语义化CSS”的拥护者,认为Tailwind这种把样式写在HTML里的方式是开倒车。但在这个项目里,我给了它一次机会,结果彻底改变了我的看法。
核心价值在于开发速度与一致性:
- 无需上下文切换:你不再需要为想一个类名而绞尽脑汁,也不再需要在HTML和CSS文件之间来回跳转。所有的样式都在同一处,编写效率极高。
- 设计系统内嵌:通过
tailwind.config.js文件,你可以定义自己的颜色、间距、字体大小等设计令牌。这强制了整个网站的设计保持一致性。比如,我定义了主色primary-500,那么任何地方使用text-primary-500或bg-primary-500都是完全相同的蓝色。 - 极致的生产包体积:Tailwind使用PurgeCSS(在v3后是内置的优化引擎)在构建时自动移除所有未使用的CSS类。这意味着最终生成的CSS文件小得惊人。我的整个网站生产环境的CSS文件大小不到10KB。
实操配置要点: 我的tailwind.config.js不仅仅配置了颜色,还集成了自定义字体和容器居中。
/** @type {import('tailwindcss').Config} */ module.exports = { content: [ './src/pages/**/*.{js,ts,jsx,tsx,mdx}', './src/components/**/*.{js,ts,jsx,tsx,mdx}', './src/app/**/*.{js,ts,jsx,tsx,mdx}', ], theme: { extend: { colors: { primary: { 50: '#eff6ff', 100: '#dbeafe', // ... 自定义颜色梯度 500: '#3b82f6', // 主蓝色 600: '#2563eb', }, gray: { 750: '#2d3748', // 添加一个深灰色用于暗色模式 } }, fontFamily: { 'sans': ['Inter', 'system-ui', 'sans-serif'], // 使用Inter字体 'mono': ['Fira Code', 'monospace'], }, animation: { 'fade-in-up': 'fadeInUp 0.5s ease-out', } }, }, plugins: [], }同时,在app/globals.css中引入字体和自定义动画:
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap'); @import url('https://fonts.googleapis.com/css2?family=Fira+Code&display=swap'); @tailwind base; @tailwind components; @tailwind utilities; @layer utilities { .animation-delay-200 { animation-delay: 0.2s; } .animation-delay-400 { animation-delay: 0.4s; } } @keyframes fadeInUp { from { opacity: 0; transform: translateY(20px); } to { opacity: 1; transform: translateY(0); } }2.3 Sanity CMS:将内容控制权夺回手中
选择Sanity作为内容管理系统,是我做过最正确的决定之一。与WordPress等传统CMS不同,Sanity是一个Headless CMS,它只通过API提供结构化的内容(JSON),不关心前端如何展示。这给了我极大的自由。
Sanity Studio:你的个性化内容后台Sanity Studio是一个可以完全自定义的React应用。你可以像搭积木一样,通过编写简单的模式定义来构建内容模型。
// schemas/post.js export default { name: 'post', title: 'Blog Post', type: 'document', fields: [ { name: 'title', title: 'Title', type: 'string', validation: Rule => Rule.required() }, { name: 'slug', title: 'Slug', type: 'slug', options: { source: 'title', maxLength: 96, }, validation: Rule => Rule.required() }, { name: 'publishedAt', title: 'Published At', type: 'datetime', initialValue: () => new Date().toISOString(), }, { name: 'body', title: 'Body', type: 'array', of: [ {type: 'block'}, // 富文本块 {type: 'image', options: {hotspot: true}}, // 支持热点裁剪的图片 {type: 'code'}, // 代码块 ] }, // ... 更多字段 ] }通过这样的模式定义,我在Sanity Studio中获得了一个极其优雅、专注的写作环境,支持Markdown式的快捷操作、代码高亮、图片上传与优化,体验堪比专业写作软件。
GROQ查询语言:从Sanity获取数据需要使用GROQ。它类似于GraphQL,但更简洁。例如,获取所有已发布的博客文章,按发布日期倒序排列:
const query = `*[_type == "post" && defined(slug.current)] | order(publishedAt desc) { _id, title, slug, publishedAt, excerpt, "coverImageUrl": coverImage.asset->url }`;在Next.js的服务端组件中,我可以直接使用Sanity的官方客户端执行这个查询,获取纯净的JSON数据。
为什么不是其他Headless CMS?我也评估过Strapi、Contentful等。Sanity胜在:1) 开发者的体验极佳,配置即代码;2) 实时协作和内容预览功能强大;3) 免费层额度非常慷慨,足够个人博客使用。
3. AI辅助开发:从构思到实现的效率革命
这个项目的另一个核心主题是AI辅助开发。我并非用AI生成整个项目,而是将其作为“超级副驾驶”,在各个关键环节提升效率。
3.1 v0.dev:UI原型与组件生成的“闪电侠”
在项目初期,我对网站的整体布局和组件风格只有模糊的想法。手动从零开始设计并编写每个按钮、卡片、导航栏的代码是极其耗时的。这时,我使用了v0 by Vercel。
工作流:
- 用自然语言描述需求:我在v0的输入框中写下:“一个简洁的博客文章卡片组件,包含标题、摘要、发布日期和阅读时长,使用Tailwind CSS,风格现代简约,有微妙的悬停效果。”
- 即时生成与迭代:v0在几秒钟内就生成了多个React组件变体。我可以选择最接近的一个,然后继续用自然语言指令微调:“把阴影改得更柔和一些”,“将标题字体加粗”,“在移动端改为垂直堆叠布局”。
- 一键复制代码:满意后,直接将生成的JSX和Tailwind类代码复制到我的项目中。这些代码质量很高,结构清晰,几乎不需要修改就能直接使用。
价值:v0极大地加速了UI原型的构建过程。它帮我快速探索了多种设计可能性,并将我从重复性的样式编写工作中解放出来,让我能更专注于业务逻辑和数据流。
3.2 Claude:全栈开发顾问与代码优化师
如果说v0擅长前端UI,那么Claude就是我整个项目的“技术顾问”。我主要用它来处理以下几类任务:
1. 解释复杂概念与提供学习路径: 当我第一次接触Next.js 15的Server Actions时,概念有些模糊。我向Claude提问:“用简单的类比解释Next.js Server Actions,并给出一个在博客项目中用于提交评论表单的具体例子。” Claude不仅给出了清晰的解释(“就像给表单按钮直接绑定了一个服务器端函数”),还提供了包含错误处理、重验证等最佳实践的完整代码示例,让我快速上手。
2. 代码重构与优化: 我会将我自己写的感觉“有点臃肿”的组件代码丢给Claude,让它提出优化建议。例如,一个从Sanity获取数据的工具函数,我最初写得比较冗长。Claude建议我将其抽象成一个可复用的、支持TypeScript泛型的sanityFetch函数,并增加了缓存和错误重试的逻辑,使代码更健壮、更优雅。
3. 编写技术文档与注释: 良好的文档对个人项目同样重要。我会让Claude根据我的代码,生成清晰的JSDoc注释或组件的使用说明。这在我几个月后回头维护代码时,提供了巨大的帮助。
4. 调试与排查: 当遇到一个晦涩的错误时,我会将错误信息和相关代码片段提供给Claude。它经常能指出我忽略的细节,比如Sanity查询中字段名拼写错误,或者Next.js中客户端组件错误地导入了服务端模块。
3.3 Cursor:深度集成AI的代码编辑器
Cursor是我本次开发的主力编辑器。它基于VS Code,但深度集成了AI能力,其“Chat”和“Composer”模式改变了我的编码方式。
“Composer”模式(指令编程): 这是最强大的功能。我不需要自己一行行敲代码,而是用自然语言描述我想要的功能。例如,在项目根目录下,我打开Composer并输入:
“在
/src/components目录下创建一个名为ThemeToggle的客户端组件。它应该是一个按钮,用于在浅色和深色模式之间切换。使用next-themes库来管理主题状态。图标使用react-icons/fa6中的FaSun和FaMoon。按钮需要有适当的ARIA标签和无障碍支持。”
Cursor在几秒钟内就生成了近乎完美的组件代码,包括正确的导入、状态逻辑和Tailwind样式。我只需要进行微小的调整即可。
“Chat”模式(上下文感知对话): 在编辑器中选中一段代码,右键唤出Cursor Chat,就可以针对这段代码进行提问或要求修改。比如,我选中一个数据获取函数,问:“如何为这个函数添加SWR进行客户端数据缓存和定期重验证?” Cursor能基于当前文件的上下文,给出非常精准的修改建议和代码片段。
与Claude的互补:Cursor更专注于“在编辑器内”基于当前文件的即时操作,而Claude更适合处理需要长篇解释、跨文件规划或深度思考的复杂问题。两者结合,覆盖了从宏观设计到微观实现的全流程。
4. 核心功能实现与架构拆解
有了强大的技术栈和AI工具,接下来就是将它们组合起来,构建网站的核心功能。我的网站主要包含以下几个部分:首页(关于我+最新博客)、博客列表页、博客文章详情页、项目展示页。
4.1 项目结构与数据流设计
清晰的架构是项目可维护的基础。我的src/app目录结构如下:
src/app/ ├── (marketing)/ # 营销页面组(首页、关于、项目) │ ├── page.tsx # 首页 │ ├── projects/ │ │ └── page.tsx # 项目列表页 │ └── layout.tsx # 营销页面的布局(可能包含特殊的页眉页脚) ├── blog/ │ ├── page.tsx # 博客列表页 │ ├── [slug]/ │ │ └── page.tsx # 博客文章详情页(动态路由) │ └── layout.tsx # 博客部分的布局 ├── api/ # API路由(例如提交评论) │ └── comment/ │ └── route.ts ├── layout.tsx # 根布局(包含全局导航、页脚) └── globals.css # 全局样式数据流:
- 构建时(SSG):对于博客列表和文章详情页,我在
generateStaticParams中查询Sanity,获取所有文章的slug,Next.js会在构建时为每个slug生成静态页面。 - 运行时(SSR/CSR):首页需要展示最新的3篇文章和精选项目,这部分数据在请求时从Sanity获取(使用
fetch并设置revalidate选项进行增量静态再生)。主题切换、移动端菜单等交互功能则在客户端完成。 - 内容更新:当我在Sanity Studio中发布一篇新博客时,我会手动或通过Webhook触发Vercel的重新部署,重新生成静态页面。对于不常变的内容,这是一种性能和实时性兼顾的方案。
4.2 博客系统核心:从Sanity到渲染
这是整个网站最复杂的部分,涉及数据获取、序列化渲染和样式处理。
步骤一:定义并部署Sanity Schema首先,在Sanity项目中定义博客文章的模式,如前文post.js所示。重点是body字段,它是由block组成的数组。block是Sanity的便携文本格式,包含了标题、段落、列表等富文本内容,以及内联的图片、代码引用。
步骤二:在Next.js中查询数据我创建了一个lib/sanity.client.ts文件来配置Sanity客户端,以及一个lib/sanity.queries.ts文件来存放所有的GROQ查询。
// lib/sanity.queries.ts import { groq } from 'next-sanity'; export const POSTS_QUERY = groq`*[_type == "post" && defined(slug.current)] | order(publishedAt desc) { _id, title, slug, publishedAt, excerpt, "coverImage": coverImage.asset->{ url, metadata { dimensions } }, body }`; export const POST_QUERY = groq`*[_type == "post" && slug.current == $slug][0] { ..., "coverImage": coverImage.asset->{ url, metadata { dimensions } }, body, author->{name, image} }`;步骤三:在页面组件中获取并渲染在博客列表页app/blog/page.tsx中,作为一个服务端组件,我可以直接查询数据:
import { client } from '@/lib/sanity.client'; import { POSTS_QUERY } from '@/lib/sanity.queries'; export default async function BlogPage() { const posts = await client.fetch(POSTS_QUERY); return ( <div> <h1>All Blog Posts</h1> <div className="grid gap-6 md:grid-cols-2"> {posts.map((post) => ( <BlogCard key={post._id} post={post} /> ))} </div> </div> ); }在文章详情页app/blog/[slug]/page.tsx中,需要结合动态路由和静态生成:
import { client } from '@/lib/sanity.client'; import { POST_QUERY, POSTS_SLUGS_QUERY } from '@/lib/sanity.queries'; // 生成静态路径 export async function generateStaticParams() { const slugs = await client.fetch(POSTS_SLUGS_QUERY); // 查询所有slug return slugs.map((slug) => ({ slug })); } export default async function BlogPostPage({ params }: { params: { slug: string } }) { const post = await client.fetch(POST_QUERY, { slug: params.slug }); if (!post) { notFound(); // 使用Next.js的notFound函数 } return ( <article> <h1>{post.title}</h1> <SanityContent content={post.body} /> </article> ); }步骤四:渲染便携文本(Body)Sanity返回的body是块(block)的数组,不能直接渲染。我们需要使用@portabletext/react库将其转换为React组件。更重要的是,我们可以自定义每个类型的渲染器。
// components/SanityContent.tsx import { PortableText } from '@portabletext/react'; import { getImageDimensions } from '@sanity/asset-utils'; import Image from 'next/image'; import SyntaxHighlighter from 'react-syntax-highlighter'; const myPortableTextComponents = { types: { image: ({ value }) => { const { width, height } = getImageDimensions(value); return ( <div className="my-8"> <Image src={value.asset.url} alt={value.alt || 'Blog image'} width={width} height={height} className="rounded-lg shadow-lg" sizes="(max-width: 768px) 100vw, 768px" /> {value.caption && ( <p className="mt-2 text-center text-sm text-gray-600">{value.caption}</p> )} </div> ); }, code: ({ value }) => { return ( <div className="my-6 overflow-hidden rounded-lg"> <SyntaxHighlighter language={value.language || 'text'} style={yourChosenStyle} // 例如:atomOneDark showLineNumbers customStyle={{ margin: 0, fontSize: '0.9em' }} > {value.code} </SyntaxHighlighter> </div> ); }, }, marks: { link: ({ children, value }) => { const rel = !value.href.startsWith('/') ? 'noreferrer noopener' : undefined; return ( <a href={value.href} rel={rel} className="text-primary-600 hover:underline"> {children} </a> ); }, }, block: { h2: ({ children }) => <h2 className="mt-10 mb-4 text-2xl font-bold">{children}</h2>, normal: ({ children }) => <p className="my-4 leading-relaxed">{children}</p>, // ... 自定义其他块级元素样式 }, }; export default function SanityContent({ content }) { return <PortableText value={content} components={myPortableTextComponents} />; }通过这种方式,我完全控制了博客内容的最终呈现样式,使其与网站的整体设计语言完美融合。
4.3 性能优化实战
一个快的网站至关重要。我采取了以下几项关键优化措施:
1. 图片优化策略:
- 全部使用
next/image:自动提供WebP/AVIF格式,设置尺寸,实现懒加载。 - 在Sanity中定义图片热点:在Sanity Studio上传图片时,可以设置热点和裁剪区域。在查询时,可以通过URL参数动态生成不同尺寸和裁剪的图片,实现响应式图片。
- 预加载关键图片:在首页,我会用
<link rel="preload">预加载首屏英雄区域的图片。
2. 字体优化:
- 使用
next/font本地加载Google Fonts(Inter和Fira Code),避免第三方请求和布局偏移。
// app/layout.tsx import { Inter, Fira_Code } from 'next/font/google'; const inter = Inter({ subsets: ['latin'], display: 'swap', variable: '--font-inter', }); const firaCode = Fira_Code({ subsets: ['latin'], display: 'swap', variable: '--font-mono', }); export default function RootLayout({ children }) { return ( <html lang="en" className={`${inter.variable} ${firaCode.variable}`}> <body>{children}</body> </html> ); }3. 静态生成与增量静态再生:
- 所有博客文章页面在构建时静态生成。
- 首页和博客列表页使用
fetch的revalidate选项设置为3600秒(1小时),这意味着它们最多每1小时重新验证一次数据,在保证性能的同时也具备了一定的新鲜度。
4. 代码分割与懒加载:
- Next.js的App Router默认支持基于路由的代码分割。
- 对于较重的第三方组件(如某些图表库、特定的动画库),我使用
next/dynamic进行动态导入,确保它们只在需要时加载。
const HeavyComponent = dynamic(() => import('@/components/HeavyComponent'), { ssr: false, // 如果组件依赖浏览器API,则禁用服务端渲染 loading: () => <SkeletonLoader />, // 加载时的占位符 });5. 部署、监控与持续迭代
项目开发完成,只是第一步。将其稳定、高效地部署到线上,并建立反馈循环,才是项目长期健康运行的关键。
5.1 选择Vercel作为部署平台
对于Next.js项目,Vercel是不二之选。它由Next.js的创建者开发,提供了开箱即用的最优支持。
部署流程简化到极致:
- 将代码推送到GitHub仓库。
- 在Vercel控制台导入该仓库。
- Vercel会自动检测到是Next.js项目,并应用最优的构建配置。几乎不需要任何额外设置。
核心优势:
- 全球边缘网络:生成的静态页面和资源会被分发到全球的CDN节点,确保全球用户都能快速访问。
- Serverless Functions:我的API路由(如评论提交)会自动部署为无服务器函数,无需管理服务器。
- 环境变量管理:在Vercel控制台可以方便地设置生产环境和预览环境的变量,如Sanity的Project ID和Token。
- 预览部署:每个Pull Request都会自动生成一个独立的、可分享的预览URL,方便进行代码审查和测试。
- Analytics:Vercel提供了内置的、隐私友好的分析工具,可以查看页面性能、访问量等核心数据。
5.2 配置自定义域名与HTTPS
我拥有colemcconnell.xyz这个域名。在Vercel中配置自定义域名非常简单:
- 在项目设置的“Domains”页面,添加你的域名。
- Vercel会给出需要添加的DNS记录(通常是CNAME记录指向
cname.vercel-dns.com)。 - 前往你的域名注册商(如Cloudflare, Namecheap)的DNS管理页面,添加该记录。
- 等待DNS生效(通常几分钟到几小时)。Vercel会自动为你申请并配置SSL证书,启用HTTPS。
5.3 性能监控与错误追踪
上线后不能做“甩手掌柜”。我集成了以下工具来监控网站健康状态:
1. Vercel Speed Insights:这是Vercel内置的工具,基于真实的用户访问数据,提供核心网页指标(LCP, FID, CLS)的详细报告。我可以清楚地看到哪个页面性能不佳,并定位问题原因。
2. Sentry:用于前端错误追踪。我通过@sentry/nextjs包轻松集成。任何未捕获的JavaScript错误都会自动上报到Sentry面板,包含完整的堆栈跟踪、用户浏览器信息等,极大加速了线上问题的排查。
配置Sentry示例:
// sentry.client.config.js 和 sentry.server.config.js import * as Sentry from '@sentry/nextjs'; Sentry.init({ dsn: process.env.NEXT_PUBLIC_SENTRY_DSN, tracesSampleRate: 0.1, // 性能追踪采样率 debug: false, });5.4 内容更新与工作流
我的内容发布工作流已经变得非常顺畅:
- 写作:在本地或线上的Sanity Studio中撰写新博客文章,插入图片、代码块。
- 预览:Sanity Studio可以与Vercel预览部署连接,我可以在发布前看到文章在真实网站上的渲染效果。
- 发布:在Sanity中点击发布。对于重要的新文章,我通常会手动触发Vercel的重新部署,以立即生成新的静态页面。对于不紧急的更新,可以依赖ISR(增量静态再生)或设置一个Webhook让Vercel自动构建。
- 分享:文章上线后,通过社交媒体等渠道分享链接。
6. 常见问题、踩坑记录与解决方案
在实际开发中,我遇到了不少挑战。这里记录下最典型的几个问题及其解决方案,希望能帮你绕过这些坑。
6.1 Sanity数据查询与类型安全
问题:从Sanity查询到的数据是any类型,在TypeScript项目中失去了类型安全,导致开发时没有智能提示,容易出错。
解决方案:使用sanity-codegen或手动定义TypeScript类型。 我选择手动定义,因为更可控。我为每个Sanity Schema都创建了对应的TypeScript接口。
// types/sanity.ts export interface SanityImage { _type: 'image'; asset: { _ref: string; _type: 'reference'; url?: string; // 通过投影(projection)获取 metadata?: { dimensions: { width: number; height: number }; }; }; hotspot?: { x: number; y: number; height: number; width: number }; crop?: { top: number; bottom: number; left: number; right: number }; } export interface Post { _id: string; _type: 'post'; title: string; slug: { _type: 'slug'; current: string }; publishedAt: string; body: any[]; // Portable Text 块数组 coverImage?: SanityImage; excerpt?: string; }然后在查询函数中指定返回类型:
export async function getPosts(): Promise<Post[]> { const posts = await client.fetch(POSTS_QUERY); return posts; }6.2 Next.js App Router中缓存与重新验证的困惑
问题:在App Router中,fetch请求默认是缓存的,这有时会导致数据不更新。不理解revalidate和generateStaticParams的配合机制。
解决方案:明确数据获取策略。
- 完全静态(SSG):在
generateStaticParams中获取所有路径,在页面组件中使用fetch或直接查询,不设置revalidate。适用于几乎不变的页面。 - 增量静态再生(ISR):在页面组件中使用
fetch并设置revalidate: 3600。页面在构建时生成,之后最多每隔revalidate秒在后台重新生成一次。适用于博客列表、需要一定新鲜度的页面。 - 服务端渲染(SSR):在页面组件中使用
fetch并设置cache: 'no-store'。每次请求都会获取最新数据。适用于高度动态、个性化的页面。 - 客户端获取(CSR):在客户端组件中使用
useEffect或SWR、TanStack Query。适用于用户交互后的数据加载。
关键心得:对于个人博客,我采用混合策略。文章详情页用SSG,因为发布后很少修改。博客列表页用ISR(1小时),平衡性能和内容更新。首页也用ISR(可能时间更短,比如10分钟),以展示最新的文章。
6.3 暗色模式与Tailwind CSS的集成
问题:实现暗色模式时,Tailwind的dark:变体需要依赖class策略,而next-themes库在防止水合作用不匹配时需要小心处理。
解决方案:严格按照next-themes的指南操作,并在Tailwind配置中启用class策略。
- 安装
next-themes。 - 在
app/providers.tsx中创建主题提供者:
'use client'; import { ThemeProvider } from 'next-themes'; export function Providers({ children }: { children: React.ReactNode }) { return ( <ThemeProvider attribute="class" defaultTheme="system" enableSystem> {children} </ThemeProvider> ); }- 在根布局
app/layout.tsx中使用这个Providers包裹{children}。 - 在
tailwind.config.js中设置darkMode: 'class'。 - 在组件中使用时,确保切换按钮是客户端组件,并使用
useTheme钩子。
'use client'; import { useTheme } from 'next-themes'; import { useEffect, useState } from 'react'; import { FaSun, FaMoon } from 'react-icons/fa6'; export default function ThemeToggle() { const { theme, setTheme } = useTheme(); const [mounted, setMounted] = useState(false); // 防止水合作用不匹配,只在客户端渲染后显示 useEffect(() => setMounted(true), []); if (!mounted) return null; // 或者返回一个占位符 return ( <button onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')} aria-label="Toggle theme" className="p-2 rounded-lg bg-gray-100 dark:bg-gray-800" > {theme === 'dark' ? <FaSun /> : <FaMoon />} </button> ); }6.4 与AI工具协作时的代码质量把控
问题:AI生成的代码有时存在冗余、不符合项目规范或存在潜在性能问题。
解决方案:建立“AI生成 -> 人工审查 -> 优化集成”的流程。
- 明确指令:给AI的指令要尽可能具体。不要只说“创建一个按钮”,而要说“创建一个使用Tailwind CSS的、带有图标和加载状态的React按钮组件,支持primary和secondary两种变体”。
- 代码审查:将AI生成的代码视为一位初级同事提交的PR。仔细审查每一行,检查是否有不必要的依赖、硬编码的值、可访问性问题或潜在bug。
- 集成与重构:不要直接复制粘贴大段代码。将AI生成的代码作为起点,将其拆解、重构,融入你现有的项目结构和设计模式中。例如,AI可能生成了一个独立的工具函数,而你的项目里已经有一个类似的工具文件,应该将其合并。
- 理解原理:对于AI提供的复杂解决方案(比如一个自定义Hook),花时间理解其工作原理,而不是盲目使用。这能帮助你在出现问题时进行调试。
7. 项目总结与未来展望
回顾整个项目的构建过程,这是一次将现代前端技术栈与AI辅助工具深度结合的愉快实践。Next.js 15的App Router带来了性能与开发体验的双重提升,Tailwind CSS让样式开发变得高效且一致,Sanity CMS则赋予了内容管理前所未有的灵活性。而Claude、Cursor和v0这些AI工具,并非取代开发者,而是成为了强大的“效率倍增器”,它们帮我快速跨越了从想法到原型、从复杂逻辑到清晰代码的障碍。
这个网站目前已经稳定运行,但我视其为一个持续演进的项目。未来的迭代方向可能包括:引入更精细的文章标签与分类系统,利用Sanity的强大查询能力;增加交互式元素,比如用Next.js服务端动作实现无需JavaScript的评论表单;或者尝试一些前沿的视觉效果,如基于视差的滚动动画。技术栈本身也在快速迭代,我会持续关注Next.js、React和AI工具生态的新动态,适时地将有益的更新融入项目中。
如果你也想搭建一个类似的个人网站,我的建议是:不要追求一步到位。先从核心功能(如首页和博客)开始,用这套技术栈快速搭建一个可用的版本。在过程中,大胆尝试AI工具,但始终保持批判性思维,理解每一行代码背后的含义。最重要的是,享受创造的过程,让你的网站成为你学习和成长的真实记录。
