全栈开发

在留白与墨线之间:从零搭建自己的博客

这是我的第一个前端作品。我借助 vibe coding,把喜欢的留白、墨线与醒目标题写进网页。本文从界面参观开始,带你获取源码、替换个人资料、写第一篇文章并部署上线。后来,随着发布内容增多,我全面重写了个人网站,也想邀请你看看它现在的样子。

代码开源

个人博客静态站点源码


如果你也想有个自己的博客,喜欢宽阔的留白、醒目的大字和利落的深色线条,先来看看这件作品。

这个网站把首页、文章、归档和个人介绍放在同一套视觉里。页面留白比较多,按钮有明确的边框,标题也不藏着掖着。拿来放学习笔记、项目记录,或者偶尔写点生活,都能用上。

下面先带你逛一遍,再从获取源码开始,把它变成自己的博客。跟着做完,你会得到一个可以在本地编辑、发布文章,并交给静态托管平台上线的网站。

先看效果:这个博客长什么样

首页,把想说的话放大一点

留白、醒目大字与深色线条构成的博客首页

首屏是「思考、记录、分享」三个大字组,下面接一小段介绍、几个主题标签,以及「浏览文章」「关于我」两个入口。再往下就是最新文章。

这里靠字重、字号、线条和留白分层:最想让人看到的内容放大,次要信息收进灰色文字,边框把不同区域分开。

你可以保留这个布局,把三个词换成自己的表达。比如「学习、动手、记录」,或者干脆放上自己的名字。

文章列表,先让人找到想读的内容

文章列表,展示分类、标题、摘要、日期和阅读全文入口

文章按列表展开,每篇带分类、标题、摘要和日期。长标题能换行,不必先给每篇做一张封面才能发布。

点开「筛选」,可以按分类浏览,也可以搜索。搜索范围包含正文,所以哪怕只记得文章里的一条命令,也能拿它试着找回那篇笔记。筛选条件会写进地址栏,刷新后还能保留。

阅读页,长文也有路标

长文正文与右侧目录,展示黑白排版和章节导航

正文在左,目录在右。文章里的标题会成为目录项,读者可以直接跳到想看的章节。阅读页也会展示发布日期、估算阅读时间和相关文章。

写文章时,正常使用 Markdown 标题、列表和代码块即可,不用为每篇重新拼一个页面。标题层级保持清楚,目录才会好用。

归档,把文章排成时间线

归档页按年份和月份组织文章,展开三月的文章列表

归档页可以按年份、月份展开文章,也有分类统计视图。写得久了,它就像一本可以翻回去的目录。

归档依赖文章里的日期,所以后面创建第一篇文章时,我们会把发布日期明确写进文件,避免文章被归到错误的月份。

关于页,让读者认识你

关于页,展示个人介绍、真实头像、阅读入口和技术栈

这里放头像、个人介绍、技术栈,再往下可以放自己的项目、经历和联系方式。纸白与墨线的排版保留下来,照片则保留原来的颜色。

别把模板里的经历照搬成自己的介绍。暂时没有项目就少放几个区块,写清楚你正在做什么,也足够让人认识你。

代码来得更快以后,我想把时间留给人的决定

这是我的第一个前端作品,也是一次 vibe coding 实践。页面和代码很快有了雏形。人不必把全部精力花在一行行编辑代码上,省下来的时间,可以拿来想清楚这件作品要为谁解决什么问题。

读者来这里最想找到什么?文章应该怎样归类?首页先讲作者,还是先把内容摆出来?这些问题关乎业务逻辑,也决定页面的样子。醒目的大字、深色线条和大片留白,是我喜欢的表达;内容怎么安排、入口放在哪里,则要贴合真实的阅读需要。

vibe coding 让代码更快地长出来。业务逻辑和审美仍然需要人来判断、取舍。对我来说,它带来的自由,是把注意力从「下一行怎么敲」挪到「我到底想让这页呈现什么」。

vibe coding 中人的判断、AI 协助实现与页面预览的关系示意图

我很喜欢这个版本的气质。后来,随着发布的文章和内容逐渐增多,我全面重写了个人网站。如果你想看看它现在的样子,可以直接访问鸿尘客:zhcmqtt.top。

鸿尘客个人网站新版首页的桌面端实景

看到这里,如果你也喜欢这份留白与墨线的气质,我们就从这件作品搭起,亲手做一个属于自己的博客。

准备工具,让网站先在电脑上亮起来

这份项目使用 React、TypeScript、Vite 和 Tailwind CSS。文章来自本地 Markdown 文件。本教程从现成的开源项目搭建,不需要先准备数据库、服务器后端或博客管理后台。

你需要一台电脑、一个浏览器、Git、代码编辑器,以及 Node.js 22,版本不低于 22.13。在 Node.js 官网选择 22.x 的安装包,按自己的系统完成安装,npm 会一起装好。Git 也从官网下载对应系统的安装包。编辑器用 VS Code、Cursor,或者你熟悉的其他工具都可以。

打开终端,检查工具是否已经能用:

node -v
npm -v
git --version

如果提示找不到命令,先完成对应工具的安装,再重新打开终端。这里先把环境准备好,后面的报错会少一些。

获取源码,进入正确的目录

在准备存放项目的目录中执行:

git clone https://github.com/zhanghongchen1213/Web_blog.git my-blog
cd my-blog
git checkout 267a119244ad0bbda71745b4771b182e4587d52d

最后一行把源码固定在本文对应的版本,方便对照文件和步骤。Git 可能提示进入 detached HEAD;如果准备长期修改,可以马上建立自己的分支:

git switch -c my-blog

接下来的 npm 命令都在 my-blog 根目录执行。 看到 package.json、package-lock.json 和 vite.config.ts 才是对的,不要进入 client 后再安装。

仓库 README 还保留着旧版全栈方案的介绍,本文对应的是当前 Markdown 静态版本,按下面的步骤走即可。

安装依赖,启动开发服务器

npm ci
npm run dev

npm ci 按锁文件安装依赖,仓库里的 .npmrc 也需要保留。先不要把包管理器换来换去,或者顺手升级全部依赖。

启动后,终端会显示一个 Local 地址。打开它,通常就能看到前面的黑白首页。端口被占用时地址可能变化,以终端实际输出为准。

这个终端需要一直开着。按下 Ctrl+C,开发服务器就停止了。

试着点开「文章」「归档」「关于」,再读一篇文章。确认模板能正常运行后,我们再开始换内容。先看到页面,再改页面,出了问题也容易知道是哪一步引起的。

换上自己的名字、头像和介绍

先认几个会经常打开的文件:

my-blog/
├── package.json
├── vite.config.ts
├── scripts/
│   └── generate-seo-assets.mjs
└── client/
    ├── index.html
    └── src/
        ├── pages/
        │   ├── Home.tsx
        │   └── About.tsx
        ├── components/
        │   ├── Navbar.tsx
        │   └── Footer.tsx
        ├── lib/
        │   ├── seo.ts
        │   └── staticContent.ts
        └── content/
            └── blog/

不用一口气把整个项目看完。先处理下面这些位置,网站就会开始有你的样子。

你想修改什么 打开哪个文件 具体改哪里
首页大标题、简介、主题标签 client/src/pages/Home.tsx 「思考、记录、分享」、介绍段落和几个标签
导航左侧的站点标识 client/src/components/Navbar.tsx 显示 BLOG 的位置
头像、姓名、技术栈、项目与经历 client/src/pages/About.tsx 个人介绍、图片的 src,以及下方各个区块
页脚介绍与联系方式 client/src/components/Footer.tsx 介绍文字、账号链接,以及联系方式按钮中的内容
网站名称、描述与作者 client/src/lib/seo.ts SITE_NAME、SITE_DESCRIPTION、AUTHOR_NAME
文章数据里的作者名 client/src/lib/staticContent.ts author 对象中默认的 name: "站长"
初始网页标题与描述 client/index.html <title> 和 description 的内容

有些页面还单独写了 SEO 描述和个人资料。比如 Home.tsx 的 SEOHead、About.tsx 的 ProfilePage 数据,也要一起换成自己的内容。改完用编辑器全局搜索原站名称、账号和联系方式,能找到遗漏的地方。

首页、个人介绍、导航页脚与站点信息的资料替换位置示意图

把头像存进自己的项目

在 client 下新建 public/images,把自己的照片放进去,比如 avatar.jpg:

client/public/images/avatar.jpg

然后在 About.tsx 找到头像的 <img>,修改它的属性,其他样式先保留:

src = '/images/avatar.jpg'
alt = '我的头像'

路径从 /images 开始,浏览器访问时不需要带上 client/public。后面文章插图也按同样的方式放。

头像出现后,再换介绍、技术栈和项目。联系方式没有对应账号就删掉入口;项目链接不要留下 #;页脚的备案信息使用自己的真实信息,没有可展示的备案信息就删除原来的备案区块。

client/index.html 末尾还带着原站的统计脚本。搜索 data-cf-beacon,删除包含它的整行 <script>。以后需要访问统计时,再配置自己的账号。

现在刷新首页和关于页,检查名字、头像、链接是不是都属于自己。模板负责搭架子,介绍里的主角得换人。

写下第一篇文章

这个博客的文章放在 client/src/content/blog。文件保存后,开发服务器会重新加载内容;首页、文章列表和归档会从这些文件中读取数据。

先把目录里的示例文章备份到工程外,再从这个目录移走。只要 .md 文件还留在 client/src/content 下面,换个子文件夹也会继续被扫描,不适合当备份。

新建一份 Markdown

在 client/src/content/blog 新建 my-first-post.md,填入下面的内容:

---
title: 你好,这是我的第一篇博客
slug: my-first-post
summary: 记录我搭建个人博客的第一步。
category: 生活记录
categorySlug: life
type: blog
status: published
publishedAt: 2026-01-20
---

## 为什么想写博客

我想给学习笔记和项目记录找一个固定的地方。先写下一篇,再慢慢把这里填满。

## 接下来想记录什么

- 学习中遇到的问题和解决办法。
- 项目从想法到完成的过程。
- 值得回头看的生活片段。

顶部两条 --- 之间是文章信息,后面才是正文。示例日期请换成你这篇文章的实际发布日期。

几个字段可以这样理解:

字段 用来做什么 怎么填写
title 列表与阅读页的标题 写你的文章标题
slug 文章地址的最后一段 使用不重复的英文短名,发布后尽量不改
summary 列表摘要 用一两句话说清文章内容,保持单行
category 展示给读者的分类名 比如生活记录、学习笔记
categorySlug 分类的标识 同一分类始终使用同一个英文标识
type 内容类型 普通文章填写 blog
status 是否展示 要出现在网站上,填写 published
publishedAt 发布日期与归档位置 填写真实日期,格式为 YYYY-MM-DD

这里用的是简单的单行元信息解析。先按示例填写,别把 summary 写成多行 YAML,也别给多个文件重复使用同一个 slug。

Markdown 文章信息与正文映射到列表、阅读页和归档的示意图

保存文件,回到文章列表。示例文章替换后,页面会变成下面这样:

按教程替换文章后的本地示例,列表中显示第一篇博客

这里的 published 表示文章在构建的网站中可见。此时它仍运行在你的电脑上,真正上线还要完成后面的构建和上传。

给文章放一张自己的图片

准备一张真实的照片或截图,命名为 my-project.png,放到:

client/public/images/my-project.png

在文章末尾追加:

## 留下第一张图片

![我的项目记录](/images/my-project.png)

文件名要和真实文件一致,图片格式也保持一致。不要把一张 PNG 只改后缀,当成 JPG 使用。

打开 /article/my-first-post,就能看到标题、正文、图片和右侧目录。下面是本地示例的实际页面,插图使用前面的博客首页截图:

第一篇示例文章的阅读页,展示标题、日期、目录与插入的首页截图

再做三个小动作:到文章列表搜索「生活片段」,去归档找这篇文章,最后在阅读页直接刷新。文章都能找到、刷新后还能读,第一篇就接进来了。

顺便给目录跳转留一点空间。顶部导航固定在页面上,在 client/src/index.css 末尾追加下面的样式,跳转后的标题就能留在导航下方:

.prose-brutalist :is(h1, h2, h3) {
  scroll-margin-top: 100px;
}

保存后点一次目录,确认对应标题能完整露出来。

把发布目录准备好

本地页面跑起来后,我们要把它打包成托管平台能接收的文件。这个版本有两处配置需要一起调整,避免上线时漏掉站点地图,或仍然指向原作者的域名。

让 SEO 文件和图片进入同一个目录

打开 scripts/generate-seo-assets.mjs,找到原来的这一行:

const OUTPUT_DIR = resolve(ROOT_DIR, 'public')

改成:

const OUTPUT_DIR = resolve(ROOT_DIR, 'client/public')

Vite 在这个项目里读取的是 client/public。这样生成的 sitemap.xml、robots.txt 和你放进去的图片,才会一起复制到发布目录。

让域名配置被两边读到

在项目根目录新建 .env.production,先填入下面这一行:

VITE_SITE_URL=http://localhost:4173

这是本地预览用的临时地址,上线前会换成托管平台给你的实际地址。文件名包含开头的点,确认没有被编辑器保存成 .env.production.txt。

Vite 会读取这个文件,但前面的 Node.js 生成脚本不会自动读取。打开 package.json,只替换 scripts 中的 seo:generate 值,其他脚本保留:

"seo:generate": "node --env-file=.env.production scripts/generate-seo-assets.mjs"

现在页面和 SEO 生成脚本可以使用同一份地址。VITE_ 开头的变量会进入前端构建结果,这里只存网站公开地址,不放密码或密钥。

检查、构建,再看一次发布版本

在项目根目录依次执行:

npm run check
npm run build
npm run start

如果前一条失败,先解决报错,再运行下一条。check 检查 TypeScript,build 生成发布文件,start 在本地预览构建结果。

构建产物放在 dist/public,结构大致如下:

dist/public/
├── index.html
├── assets/
├── images/
├── sitemap.xml
└── robots.txt

打开终端显示的预览地址,再看首页、第一篇文章和图片。分别访问 /sitemap.xml、/robots.txt,确认前者显示 XML,后者显示文本,里面的网站地址也是刚刚设置的地址。

start 用于本地预览。下面上线时,我们上传的是构建文件,开发服务器不用搬到云端常驻。

把博客上线到 Cloudflare Pages

这条路线使用 Cloudflare Pages 的 Direct Upload,直接上传构建好的文件。你需要自己的 Cloudflare 账号,暂时没有个人域名也可以先用平台提供的默认地址。

Cloudflare Pages 首次上传、填写实际域名与再次构建上传的流程示意图

先完成第一次上传

  1. 登录 Cloudflare 控制台,进入 Workers & Pages。
  2. 选择创建应用,进入 Pages 的文件拖放上传入口。控制台通常会显示 Drag and drop your files。
  3. 输入项目名称,上传本地 dist/public 文件夹。上传内容的根层应直接包含 index.html 和 assets,不要把整个源码工程或多包了一层的 dist 上传。
  4. 点击部署,等待平台完成处理,记录控制台实际提供的默认访问地址。

打开这个地址,首页应该能显示。再直接访问 /article/my-first-post,按刷新键,确认文章也能正常打开。

这个项目使用浏览器路由。Pages 在没有顶层 404.html 时会按单页应用方式回退到首页入口,再由 React 匹配文章地址,因此不要额外塞入一个顶层 404.html 来「补页面」。项目里已有站内 404 路由。

把临时地址换成真正的站点地址

第一次上传是为了拿到实际可访问的默认地址。接下来打开 .env.production,把本地预览地址换成控制台给你的地址。下面只是填写格式,不能照抄项目名:

VITE_SITE_URL=https://your-blog.pages.dev

重新构建:

npm run check
npm run build

在这个 Pages 项目中创建一次新部署,选择 Production,再上传新生成的 dist/public。域名信息也就跟着换过去了。

更新完成后,检查线上首页、文章直达与刷新、图片、搜索、归档,以及 /sitemap.xml、/robots.txt。站点地图里的文章地址应属于你的网站,不应再出现本地地址或原站域名。

有站点地图不代表已经被搜索引擎收录。这个版本的正文由浏览器执行 JavaScript 后显示;如果以后更在意内容抓取和收录,可以再做预渲染。先把可访问、可阅读的博客搭好。

有自己的域名时,再接上去

默认地址能正常访问后,再进入项目的 Custom domains,选择添加自定义域名。

使用子域名时,比如自己的 blog 子域,先在 Pages 中关联它,再到域名的 DNS 管理处设置 CNAME,目标填写这个项目实际的默认域名。域名仍在其他 DNS 服务商那里,也可以走这条子域路线。

如果直接使用根域名,则需要按 Cloudflare 的要求把域名加入对应账号,并配置它的名称服务器。跟着控制台提示完成,不用为了第一次发布同时折腾两套域名方案。

域名生效后,再把 .env.production 改成新的正式地址,重新构建并部署一次。打开站点地图核对域名,确认旧地址没有继续留在里面。

Direct Upload 后续需要你主动上传新构建。同一个项目不能直接切换为 Git 集成模式;如果以后想提交代码后自动部署,可以另建 Git 集成的 Pages 项目。开始阶段先把这一次上传做明白就够了。

第二篇文章怎么更新

以后写文章,重复的是这几个动作:

  1. 在 client/src/content/blog 新建一份 .md,填写自己的标题、摘要、分类、唯一 slug 和真实日期。
  2. 用 npm run dev 在本地检查正文、目录和图片。
  3. 执行 npm run check 和 npm run build。
  4. 在同一个 Pages 项目里上传新的 dist/public,创建生产部署。

后续文章从编写、本地检查、构建到上传部署的更新流程示意图

已经上线的网站不会因为你保存了本地文件就自动更新。忘了重新构建或上传,浏览器看到的仍然是上一次的内容。

留草稿时,可以把 status 改成 draft,它不会出现在正常的文章列表中。不过 Markdown 会进入前端构建,不适合存放私人记录或密钥。真正不打算公开的内容,留在工程外。

遇到问题时,先按现象查对应的位置:

现象 先检查哪里
npm ci 报 Node 版本不支持 node -v 是否满足前面的版本要求,安装目录和 .npmrc 是否正确
安装时出现依赖漏洞提示 安装报错与审计提示分开看,按 npm audit 的结果处理依赖;不要直接强制升级全部包来跳过问题
文件写好了,文章列表里没有 文件是否在内容目录、type 是否为 blog、status 是否为 published,以及元信息边界是否完整
图片在本地显示,上线后不见了 文件是否进入 dist/public/images,文件名大小写和 Markdown 路径是否一致
首页能打开,文章刷新却失败 上传层级是否正确,是否添加了顶层 404.html,托管平台是否按单页应用回退
文章日期或归档月份不对 publishedAt 是否填写了有效的真实日期
页面是新内容,站点地图还是旧域名 .env.production、seo:generate 的读取配置,以及是否重新构建并上传

回到开头那张首页图:大标题、几条标签,一小段介绍。它可以装下不少东西,但不用在第一天就填满。

先把名字换成自己的,再写完第一篇。黑白框架已经在那里,接下来让它长出你的内容。

参考资料

Vite 7 的 Node.js 版本要求

Vite 静态站点部署与预览说明

Vite 静态资源与 public 目录说明

鸿尘客 Honchic · 把脑洞焊到电路板上

接着看

再读几篇项目与技术手记。

返回全栈开发

读到这里,也聊聊你的想法。

项目复刻、技术交流,或者一起折腾点什么。