用 Next.js 搭建个人博客并部署到 Cloudflare
先确定技术路线
个人博客大致有两种做法:一种是 WordPress 这类动态程序,需要 PHP 运行环境和数据库;另一种是预先生成静态 HTML,再交给 Nginx、Apache 或对象存储托管。这个站点选择的是后者:Next.js 负责页面、路由和构建,文章是 content/ 目录下的 Markdown 文件,next build 时一次性生成全部页面。
静态方案的好处很直接:线上不需要数据库和 Node 进程,攻击面小,HTML 可以被 Cloudflare 缓存,流量上来时也不容易被打垮。代价是发布流程多一步构建,动态功能(例如评论、搜索)需要借助第三方服务或单独的接口。
动手前准备三样东西:本地安装 Node.js(Next.js 16 对版本有要求,具体以官方文档为准)、一个已备案或可解析的域名、一台能运行 Apache 的服务器。Cloudflare 账号免费注册,把域名接进去即可。
初始化项目
用官方脚手架创建项目:
npx create-next-app@latest my-blog
cd my-blog
npm install gray-matter
交互选项里建议开启 TypeScript、App Router 和 Tailwind CSS。初始化完成后执行 npm run dev,浏览器打开 http://localhost:3000,能看到默认页面就说明环境正常。
为了后续维护方便,我习惯把文章解析逻辑单独放在 lib/posts.ts,目录结构大致如下:
my-blog/
├─ app/
│ ├─ blog/
│ │ ├─ page.tsx
│ │ └─ [slug]/page.tsx
│ └─ layout.tsx
├─ content/
│ └─ hello-world.md
├─ lib/
│ └─ posts.ts
└─ next.config.ts
其中 content/ 放 Markdown,app/blog/page.tsx 是文章列表,app/blog/[slug]/page.tsx 是文章详情,两者都在构建时读取本地文件。
用 Markdown 管理文章
每篇文章一个 .md 文件,顶部用 YAML frontmatter 记录标题、日期、标签等信息。frontmatter 与正文由 gray-matter 负责拆分,这个库只做解析,不涉及渲染。
一份可用的 frontmatter 长这样:
---
title: "第一篇文章"
slug: hello-world
date: "2026-11-14"
tags: ["建站", "Next.js", "Cloudflare"]
description: "一句话说明这篇文章讲什么,方便列表页和搜索结果展示。"
---
读取与排序的逻辑可以写得很短:
import fs from "node:fs";
import path from "node:path";
import matter from "gray-matter";
const CONTENT_DIR = path.join(process.cwd(), "content");
export type Post = {
slug: string;
title: string;
date: string;
tags: string[];
description: string;
content: string;
};
export function getAllPosts(): Post[] {
return fs
.readdirSync(CONTENT_DIR)
.filter((file) => file.endsWith(".md"))
.map((file) => {
const raw = fs.readFileSync(path.join(CONTENT_DIR, file), "utf8");
const { data, content } = matter(raw);
return {
slug: String(data.slug || file.replace(/\.md$/, "")),
title: String(data.title || ""),
date: String(data.date ?? ""),
tags: Array.isArray(data.tags) ? data.tags.map(String) : [],
description: String(data.description || ""),
content,
};
})
.sort((a, b) => (a.date < b.date ? 1 : -1));
}
几个实际踩过的点:
date一定要加引号。YAML 会把不带引号的日期解析成Date对象,String()之后变成很长的带时区字符串,排序和展示都会出问题。slug要保证唯一,否则静态导出时后一篇文章会覆盖前一篇的目录。可以在读取时用Set去重并打印警告。- slug 可以用中文,但 URL 里会出现百分号编码。站内链接统一写成
/blog/中文slug/,浏览器和 Next.js 会自动处理编码。 content/目录必须提交到仓库,并且构建机要能读到;如果使用 CI,别把 Markdown 放在被忽略的目录里。
配置静态导出
Next.js 的静态导出只需在 next.config.ts 里加一个字段:
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
output: "export",
trailingSlash: true,
images: {
unoptimized: true,
},
poweredByHeader: false,
};
export default nextConfig;
output: 'export' 做了什么
开启后,next build 会把每个路由预渲染成 HTML 文件并输出到 out/ 目录,不再生成需要 Node 运行的产物。也就是说 next start 在这种模式下没有意义,部署的只是纯静态文件。
代价是部分能力不可用:服务端组件里不能调用 cookies()、headers() 这类请求时 API,不能使用 ISR,Route Handler 需要显式声明为静态,图片优化服务也不可用,所以要把 images.unoptimized 设为 true。Next.js 16 在这块的细节可能调整过,升级前先看对应版本的官方文档。
trailingSlash 为什么建议开启
默认情况下,Next.js 生成的是 /blog/hello 这样的链接,导出后对应 out/blog/hello.html。而 Apache 更习惯目录加索引文件的模型,访问 /blog/hello/ 时会去找 out/blog/hello/index.html。
开启 trailingSlash: true 后,导出结构变成 out/blog/hello/index.html,与 Apache 的 DirectoryIndex 行为一致,/blog/hello/ 可以直接命中。同时要保证站内链接也带尾斜杠,否则 Cloudflare 和源站可能各做一次跳转,形成多余的重定向甚至循环。
构建与上传
执行构建:
npm run build
产物在 out/ 目录。可以先本地检查:
npx serve out
确认页面、图片、CSS 都正常后再上传。Linux 服务器上我一般用 rsync:
rsync -avz --delete out/ user@your-server:/var/www/blog/
--delete 会删掉源站多余文件,适合整站覆盖;第一次操作前先用 --dry-run 看一眼,避免误删。Windows 下用 WinSCP 或 FileZilla 直接拖 out/ 里的内容也可以。
Apache 站点配置保持简单即可:
<VirtualHost *:80>
ServerName blog.example.com
DocumentRoot /var/www/blog
<Directory /var/www/blog>
Options -Indexes +FollowSymLinks
AllowOverride None
Require all granted
</Directory>
ErrorDocument 404 /404.html
</VirtualHost>
静态导出默认会生成 404.html,把它交给 ErrorDocument 就能得到统一的错误页。
Cloudflare DNS 与 SSL
域名接入 Cloudflare 后,把 NS 记录改成 Cloudflare 分配的两个地址,等待生效。然后添加一条 A 记录指向服务器公网 IP,代理状态建议开启(橙色云朵),这样源站 IP 不会直接暴露,也能用到缓存、WAF 和限速。
SSL 模式选 Full (strict),并在源站安装 Cloudflare Origin Certificate 或有效的 Let's Encrypt 证书。不要用 Flexible,否则 Cloudflare 到源站走 HTTP,源站再跳 HTTPS 时容易出现重定向循环。
页面规则里可以顺手打开 Always Use HTTPS 和 Automatic HTTPS Rewrites。缓存方面,HTML 建议不缓存或设置很短的 TTL,/_next/static/ 下的文件名带内容哈希,可以长期缓存。每次发布后如果页面还是旧的,先在 Cloudflare 控制台清理缓存再排查。
源站防火墙最好只放行 Cloudflare 的 IP 段,具体网段会更新,以官方文档公布的列表为准。
常见 404 与资源路径问题
页面 404,但文件确实存在
先确认 out/blog/xxx/index.html 是否存在,再检查链接有没有尾斜杠、大小写是否一致。Linux 文件系统区分大小写,本地 Windows 能访问的 /Blog/xxx/ 到线上就会 404。若部署在子目录,还需要在 next.config.ts 设置 basePath 和 assetPrefix。
CSS、JS 全部 404
多半是资源前缀不对。用浏览器开发者工具看请求的完整路径,如果是 /assets/... 这种与 out/ 结构对不上的地址,检查 basePath、反向代理的路径重写以及 CDN 的缓存规则。
本地正常,线上白屏
先看控制台报错。常见原因是 CDN 缓存了旧 HTML,而旧 HTML 引用的哈希文件已经被新构建覆盖,结果是 404。解决办法是发布时先上传静态资源,最后替换 HTML,或者用软链接做原子切换,再清理缓存。
部署后部分文章打不开
检查是否存在重复 slug,或者文件名里的特殊字符在 URL 编码后与期望不一致。中文 slug 建议保持全站统一,不要混用空格和百分号。
内容维护
日常写作流程就是新建 Markdown、填好 frontmatter、本地预览、提交仓库、构建上传。文章多了以后,可以按标签生成归档页,也可以把构建交给 CI,推送到主分支后自动发布。如果不想手写 Markdown,可以给站点接入图形化后台,具体做法见 如何给自己的网站接入CMS。
常见问题
静态导出后还能使用 API 路由吗?
不能使用依赖请求上下文的动态接口。Route Handler 只能在构建时执行并输出静态结果,登录、提交表单这类功能需要外部服务或独立的服务端。
为什么必须设置 images.unoptimized?
静态导出没有 Next.js 图片优化服务器,next/image 默认会请求优化接口,线上就会 404。设为 true 后直接输出原始图片地址,需要压缩和转格式的话可以交给 Cloudflare 或构建脚本处理。
trailingSlash 开启后旧链接会失效吗?
旧的带斜杠链接能正常工作,不带斜杠的链接可能多一次 301 跳转。若已经有外部收录,建议在 Cloudflare 或 Apache 上配置统一跳转规则,避免出现重复 URL。
每次更新都要清理 Cloudflare 缓存吗?
只更新文章时通常不需要,因为 HTML 没有被长时间缓存。如果改了 _next/static 下的资源或调整了缓存规则,发布后清理一次整站缓存最稳妥。
Cloudflare 代理开启后源站还能直接访问吗?
可以,但建议用防火墙只允许 Cloudflare 网段访问 80 和 443,避免别人绕过 CDN 直连源站。需要本地调试时再临时放行自己的 IP。