从零到上线,我用 Claude Code 半天搭完了一个完整网站:Next.js + Cloudflare Pages 全流程复盘
从零到上线,我用 Claude Code 半天搭完了一个完整网站:Next.js + Cloudflare Pages 全流程复盘
一、我要做什么
2026 年辞了职,经过一段时间思考,开始全职独立开发。第一件事——给自己搭个网站。
需求很明确:写博客、展示产品、推荐工具、展示接单服务。SEO 要好,更新要方便,成本要低。
技术上定了几个方向:不用 WordPress(太重),不用第三方 CMS(不灵活),能 Markdown 写文章,能 git push 自动上线。
最终方案:Next.js 14 + Tailwind CSS + Cloudflare Pages。开发工具:Claude Code。
这篇文章从零开始,完整记录从项目初始化 → 开发 → 踩坑 → 部署上线 → 日常更新的全过程。项目源码已开源,文末有 GitHub 地址。
二、第一天:用 AI 把网站主体搭起来
2.1 项目初始化
我在 Claude Code 的终端里敲了一段需求描述:
“帮我建一个 Next.js 14 个人网站,App Router + TypeScript + Tailwind CSS。全站黑白配色,点缀色用橙色。包含首页、关于、博客列表和详情、工具箱、服务页、联系页。博客用 Markdown 管理,部署到 Cloudflare Pages。”
Claude Code 大概是这么干的:
第一步:跑脚手架
npx create-next-app@14 xiaoniubuniu-web --typescript --tailwind --app
第二步:装依赖
npm install gray-matter react-markdown remark-gfm react-syntax-highlighter react-icons
npm install -D @cloudflare/next-on-pages@1.13.15
这里有个细节:@cloudflare/next-on-pages 的 1.13.16 版本要求 next >= 14.3.0,但 Next.js 14 最新只到 14.2.35——14.3.0 这个版本号压根不存在。Claude Code 自己遍历了适配器所有历史版本,锁定了不声明 next 依赖的 1.13.15。
第三步:搭建目录结构
xiaoniubuniu-web/
├── content/
│ ├── blog/ ← 博客 Markdown
│ ├── products/ ← 产品 Markdown
│ └── tools/ ← 工具箱 Markdown
├── src/
│ ├── app/ ← 页面路由
│ ├── components/ ← 全局组件
│ └── lib/ ← 数据读取 + 常量
├── public/images/ ← 图片资源
├── next.config.mjs
└── tailwind.config.ts
2.2 核心实现:Markdown 驱动的内容系统
博客、产品、工具箱三个模块全部用 Markdown 管理。以博客为例:
① 文章格式
---
title: "农村老兵的出海独立站第一周复盘"
date: "2026-06-28"
category: "build-in-public"
tags: ["出海", "独立开发", "复盘"]
description: "辞职回村第一周,我用 Next.js 搭了个个人网站,成本不到100块。"
---
# 正文内容...
② 数据读取层
// src/lib/blog.ts
import fs from "fs";
import path from "path";
import matter from "gray-matter";
export interface BlogPost {
slug: string;
title: string;
date: string;
category: string;
tags: string[];
description: string;
content: string;
}
const blogDir = path.join(process.cwd(), "content/blog");
export function getAllPosts(): BlogPost[] {
if (!fs.existsSync(blogDir)) return [];
return fs.readdirSync(blogDir)
.filter(f => f.endsWith(".md"))
.map(f => {
const slug = f.replace(".md", "");
const raw = fs.readFileSync(path.join(blogDir, f), "utf8");
const { data, content } = matter(raw);
return { slug, title: data.title, date: data.date,
category: data.category, tags: data.tags || [],
description: data.description || "", content };
})
.sort((a, b) => (a.date < b.date ? 1 : -1));
}
③ 博客列表页(静态 + 客户端筛选)
Cloudflare Pages 要求所有页面纯静态,所以列表页在服务端生成全量 HTML,搜索、分类筛选、分页全部在客户端完成:
// src/app/blog/page.tsx — 服务端组件
import { Suspense } from "react";
import { getAllPosts } from "@/lib/blog";
import BlogFilter from "@/components/BlogFilter";
export default function BlogListPage() {
const posts = getAllPosts(); // 构建时执行,生成全量数据
return (
<Suspense fallback={<div>加载中...</div>}>
<BlogFilter posts={posts} />
</Suspense>
);
}
// src/components/BlogFilter.tsx — 客户端组件
"use client";
import { useSearchParams, useRouter } from "next/navigation";
export default function BlogFilter({ posts }: { posts: BlogPost[] }) {
const searchParams = useSearchParams();
const router = useRouter();
const category = searchParams.get("category") || "all";
const search = searchParams.get("search") || "";
let filtered = posts;
if (category !== "all") filtered = filtered.filter(p => p.category === category);
if (search) {
const q = search.toLowerCase();
filtered = filtered.filter(p =>
p.title.toLowerCase().includes(q) || p.description.toLowerCase().includes(q));
}
// 分页
const page = parseInt(searchParams.get("page") || "1");
const perPage = 9;
const totalPages = Math.ceil(filtered.length / perPage);
const paged = filtered.slice((page - 1) * perPage, page * perPage);
const updateParams = (key: string, value: string) => {
const params = new URLSearchParams(searchParams.toString());
value ? params.set(key, value) : params.delete(key);
if (key !== "page") params.delete("page"); // 重置页码
router.push(`/blog?${params.toString()}`, { scroll: false });
};
return (
<div>
{/* 搜索框 + 分类按钮 + 文章网格 + 分页器 */}
</div>
);
}
④ 博客详情页(SSG + JSON-LD)
// src/app/blog/[slug]/page.tsx
// 构建时生成所有文章的静态页面
export function generateStaticParams() {
return getAllPostSlugs().map(slug => ({ slug }));
}
// 每篇文章独立的 SEO 元数据
export function generateMetadata({ params }): Metadata {
const post = getPostBySlug(params.slug);
return {
title: post.title,
description: post.description,
openGraph: { type: "article", publishedTime: post.date, tags: post.tags },
};
}
// Article 结构化数据
const jsonLd = {
"@context": "https://schema.org",
"@type": "Article",
headline: post.title,
datePublished: post.date,
author: { "@type": "Person", name: "小牛不牛" },
};
// 正文渲染
<ReactMarkdown
remarkPlugins={[remarkGfm]}
components={{
code({ className, children }) {
const lang = /language-(\w+)/.exec(className || "")?.[1];
return lang ? (
<SyntaxHighlighter style={oneDark} language={lang}>
{String(children).replace(/\n$/, "")}
</SyntaxHighlighter>
) : <code className={className}>{children}</code>;
}
}}
>
{post.content}
</ReactMarkdown>
2.3 整个过程花了多久
从把需求告诉 Claude Code 到浏览器里看到完整的页面,中间也不短的微调网站,大约半天。
效率提升最明显的不是"写代码"本身——而是配置类、调试类的工作。Tailwind 主题配置、TypeScript 严格模式兼容、Cloudflare 适配器版本选择、Server Component 和 Client Component 边界处理——这些事以前每一个都够折腾半小时,现在 AI 一次性全搞定。
三、踩坑和解决方案
坑 1:fs 模块不能出现在客户端
Next.js App Router 里,"use client" 组件不能直接或间接引用 fs 模块。博客、产品、工具的数据读取函数都在 lib/ 下,而分类标签等常量也在同一个文件里。如果客户端组件 import { categoryLabels } from "@/lib/products",webpack 会尝试把整个 products.ts 打进客户端 bundle,包括 import fs from "fs",直接报错。
解决方案:拆文件。
src/lib/products.ts ← 服务端专用:fs 读取 Markdown
src/lib/product-constants.ts ← 客户端安全:纯常量定义
客户端组件只引 product-constants,服务端组件引 products。
坑 2:Cloudflare Pages 不认动态路由
博客列表页最初用服务端的 searchParams 做筛选和搜索,结果构建时报错:
The following routes were not configured to run with the Edge Runtime: /blog
原因:用了 searchParams 的页面在 Next.js 中是动态路由,Cloudflare Pages 不允许非 Edge Runtime 的动态路由。
解决方案:服务端只做 SSG 输出全量文章列表,搜索、分类切换、分页全部在客户端用 useSearchParams 完成,外层包 <Suspense>。
坑 3:setupDevPlatform() 导致本地开发 404
按照 Cloudflare 文档在 next.config.mjs 中加了:
if (process.env.NODE_ENV === "development") {
await setupDevPlatform();
}
结果 next dev 时所有 JS/CSS chunk 全部 404。
原因:setupDevPlatform() 模拟了 Cloudflare Workers 的请求处理管道,拦截了 Next.js dev server 的静态文件路由。本项目是纯静态站点,根本不需要 Workers 运行时。
解决方案:删掉这段。next.config.mjs 最终只保留:
const nextConfig = {
images: { unoptimized: true },
};
export default nextConfig;
四、部署上线:从本地到全世界
4.1 推代码到 GitHub
git init
git add .
git commit -m "feat: xiaoniubuniu.com 个人网站"
git remote add origin git@github.com:xiaoyunchengzhu/xiaoniubuniu-web.git
git push -u origin main
项目源码完全公开,欢迎 Star 和参考:
👉 GitHub: https://github.com/xiaoyunchengzhu/xiaoniubuniu-web
4.2 Cloudflare Pages 配置
登录 Cloudflare 控制台 → Workers & Pages → Pages → 连接到 Git → 选择 GitHub 仓库。
构建配置:
| 参数 | 值 |
|---|---|
| 构建命令 | npx @cloudflare/next-on-pages |
| 输出目录 | .vercel/output/static |
| 兼容性标志 | nodejs_compat |
点「保存并部署」,Cloudflare 自动拉代码、装依赖、构建、发布。第一次构建约 2-3 分钟。完成后得到一个 *.pages.dev 临时域名,立刻能访问。
4.3 绑定自定义域名
域名在 Namecheap 注册的 xiaoniubuniu.com。需要把 DNS 指向 Cloudflare Pages。
Namecheap 后台 → Domain List → 域名 → Advanced DNS → 添加 CNAME 记录:
| Host | Type | Value |
|---|---|---|
www |
CNAME | xiaoniubuniu-web.pages.dev |
回到 Cloudflare Pages → 项目设置 → 自定义域,添加 www.xiaoniubuniu.com。验证 DNS 生效后,SSL 证书自动签发,HTTPS 全绿。
4.4 完整链路
本地写 MD → git add & commit → git push → Cloudflare 自动构建 → 全球 CDN 部署
从 git push 到线上更新,全自动,30 秒内完成。不需要登录服务器、不拖拽文件、不重启服务。
五、日常更新:写文章只需要三步
这个站最让我满意的地方不是开发快,而是更新内容的阻力为零。
发一篇新文章:
- 在
content/blog/下新建文章名.md,写好 frontmatter 和正文 git add & git commit & git push- 等 30 秒,刷新网站,文章已经在首页了
改导航栏、加新页面、调样式——同样流程。想改什么告诉 Claude Code,它改完我 review 一遍,push 上线。
之前也折腾过 WordPress 博客,最后都断更了。后来想明白了:每次发文章要登录后台、粘贴编辑器、调格式、点发布——对一个已经花了两小时写作的人来说,这些步骤的阻力足以让你放弃。Markdown + Git 这条链路,把发布从"5 步操作"变成了"1 个命令",更新不再是一件需要心理建设的事。
六、最终成本
| 项目 | 费用 |
|---|---|
域名 xiaoniubuniu.com |
$11.48 / 年 |
| Cloudflare Pages 托管 | 免费 |
| GitHub 代码托管 | 免费 |
| Claude Code + DeepSeek V4 | 约 ¥5(token 消耗) |
一年不到 13 美元,加上 AI 消耗折合人民币不到 100 块。
作为对比:一个 WordPress 托管方案月付 $5 起步,一年 $60。还附赠 PHP 版本升级的心跳加速体验、插件兼容性的抽盲盒式冒险,以及每月至少一次的"我的网站为什么这么慢"的灵魂拷问。
七、源码
技术栈:Next.js 14 + TypeScript + Tailwind CSS + Cloudflare Pages
欢迎 Star、Fork、提 Issue。如果你也想搭一个类似的站,或者对这套方案有疑问,直接去 GitHub 提 issue 或者在博客评论区留言。
更多推荐


所有评论(0)