这是这个博客的第一篇文章。
我不是专业做 Web 开发的,平时写代码更多是做些小工具、跑数据。这次想给自己搭一个博客,不是为了秀技术,只是想有一个地方能安安静静地把东西写下来——技术上踩过的坑、读到喜欢的东西、偶尔写的小说。
于是就有了这个博客。代码量不大,但第一次从零开始做一个能发文章、能评论、能被搜索引擎收录的完整小网站,对我来说也费了些力气。这篇就是一份记事,把过程和一些取舍记下来,顺便也算测试一下博客自己的排版。
一、为什么不用现成的方案
一开始我确实考虑过:
- WordPress:功能最全,但我不太想维护 PHP 和 MySQL。
- Hugo / Hexo 这类静态博客:输出就是 HTML,又快又省心,但评论、搜索这些动态功能得另外接服务。
- Next.js 之类的现代方案:看起来很酷,但 JS 生态变化快,我担心过两年回来看不懂自己写的代码。
最后选了 Python + Flask,主要是因为我平时写 Python 最多,出了问题我知道去哪里查。Flask 本身也很小,文档看得懂。这对我这种”业余选手”很重要——得是一个出了 bug 我能自己修的框架。
# app/__init__.py
def create_app():
app = Flask(__name__, template_folder="../templates", static_folder="../static")
app.config.from_object(Config)
db.init_app(app)
mail.init_app(app)
# 注册蓝图...
return app
照着 Flask 官方文档的 “application factory” 模式写的。刚开始其实不理解为什么要包一层工厂函数,后来加了蓝图、加了扩展才慢慢体会到——这样拆分以后每个部分都能独立调试。
二、文章放在哪里:文件 or 数据库?
这个问题纠结了挺久。
一种做法是把文章存数据库里,后台写个编辑器发布。但这样每改一次文章都得登后台,对我自己用来说太麻烦了。
另一种就是把每篇文章当成一个 Markdown 文件存在文件夹里,像这样:
---
title: "我的新文章"
published: 2026-04-16
tags: ["Python", "Flask"]
---
正文写在下面……
这种方式的好处是:
- 想写就用任何编辑器打开写,VS Code、Typora、甚至记事本都行
- 备份很简单,整个文件夹
git push一下就完事 - 哪篇写错了用
git log就能看历史
我选了后者。文件头上的那一小段叫 frontmatter,是 python-frontmatter 这个库解析的,读一篇文章的核心代码就这么几行:
def load_post(filepath: Path) -> Optional[dict]:
post = frontmatter.load(str(filepath))
return {
"slug": _slug_from_filename(filepath.name),
"title": post.metadata.get("title"),
"published": post.metadata.get("published"),
"tags": post.metadata.get("tags", []),
"content": post.content,
"draft": bool(post.metadata.get("draft", False)),
}
评论这种必须要存状态的东西就用了 SQLite——一个文件的数据库,不用额外装 MySQL 服务。
三、一边做一边学着加的小功能
基本框架跑起来后,陆续补了一些功能。说实话每个都踩了点小坑,下面是按实现顺序的记事。
1. 草稿和定时发布
想写到一半先存着、过两天再继续?加个 draft: true 就行。想在五一当天发文?把 published 写成 2026-05-01,那天之前它就不会出现在列表里。
刚开始我以为需要一个定时任务每天检查,后来想想根本不用——每次网页访问的时候比一下当前日期就行了:
if post.get("draft"):
continue
if post["published"] > date.today().isoformat():
continue
字符串比较对 YYYY-MM-DD 这种格式天然就是按时间比,不用把字符串转成日期对象。这算是小惊喜。
2. 给评论加一点安全保护
评论是整个博客唯一能让陌生人往服务器写数据的地方,所以得小心。我叠了三层:
- 蜜罐字段:表单里塞一个用 CSS 隐藏的输入框
website_url,真人看不到也不会填,脚本看到了就会老老实实填上——填了就直接拦掉。 - IP 限速:同一个 IP 每分钟最多 3 条评论,内存里记个时间戳数组。
- CSRF token:这个最开始我没做,后来看资料才知道如果没有 token,别人可以在自己的网站上放个隐藏表单,骗你在登录状态下提交到我的博客。
CSRF 的核心代码很短:
def validate_csrf_token():
if request.method == "POST":
token = session.get("_csrf_token", "")
form_token = request.form.get("_csrf_token", "")
if not token or not hmac.compare_digest(token, form_token):
abort(403)
本来可以装 Flask-WTF 那个扩展的,不过看了源码发现我只需要它 5% 的功能,干脆就自己写了十几行。
3. 代码块的”复制”按钮
技术文章里代码块很多,读的人需要复制下来跑。手动选中又容易选到行号或者多余空格。于是写了个几十行的 JS:页面加载后扫一遍 <pre>,给每个加个按钮。
这里踩过一个坑:navigator.clipboard.writeText() 只在 HTTPS 或 localhost 下才能用。HTTP 的测试环境完全不报错,只是静默失败。查了半天才明白。
4. 内容缓存
刚开始每次刷新首页都要扫一遍 content/posts/ 文件夹,文章少的时候没感觉,但我想着以后多了肯定会慢,提前加了个非常简单的缓存:
_cache: dict[str, tuple[float, Any]] = {}
def _cached(key: str, ttl: int, fn):
if ttl <= 0:
return fn()
now = time.time()
if key in _cache and now < _cache[key][0]:
return _cache[key][1]
value = fn()
_cache[key] = (now + ttl, value)
return value
就是一个带过期时间的字典。默认 300 秒失效,发新文章后最多等五分钟。对我这种频率够用。
5. 暗色模式
这个做起来比想的简单。CSS 里早就用了变量,比如 --bg、--text。再加一段 html.dark { --bg: #0f172a; --text: #e2e8f0; ... },然后 JS 控制 <html> 加不加 dark 这个 class。用户的偏好存在 localStorage,第一次来还会读浏览器的 prefers-color-scheme。
6. 文章目录(右侧那一栏)
这个功能我很喜欢。长文章有个目录能一下子跳过去,看起来也更像样。Python 的 markdown 库自带 TocExtension,渲染完能拿到一段目录 HTML,直接塞到模板里。
滚动时高亮当前标题是一段纯 JS:
function onScroll() {
var y = window.scrollY + 120;
var current = null;
for (var i = 0; i < headings.length; i++) {
if (headings[i].el.offsetTop <= y) current = headings[i];
}
links.forEach(a => a.classList.remove('toc-active'));
if (current) current.link.classList.add('toc-active');
}
本来想用更”现代”的 IntersectionObserver,结果写了半天 API 没搞明白。遍历一遍标题位置就够用,几十个标题扫一遍肉眼也感觉不到卡。
屏幕窄的时候目录就自动隐藏,手机上不占地方。
7. 让搜索引擎能找到
这部分其实挺枯燥但挺重要。包括:
- 每个页面的
<title>和<meta description>写对 - 补上 Open Graph 标签(被分享到社交媒体时显示的预览图和标题)
- 一个
canonical链接告诉搜索引擎哪个是”正本” - 生成一份
/sitemap.xml列出所有页面
后面几个我以前都没做过,查文档 + 看别人博客的源代码学的。
8. 搜索功能
最土的办法:拿到关键字,把所有文章的标题、摘要、标签、正文都拼起来,看看关键字在不在里面。
if query in title or query in excerpt or query in tags or query in content:
results.append(post)
对几十篇文章完全没问题。如果以后文章多到这招不行了再说——到那时候也许我才真的需要学一下 SQLite FTS 或者 Meilisearch。
四、刻意没做的事
做的过程里,也有很多东西我犹豫了一下最后没加:
- 富文本编辑器 —— 我已经习惯了 Markdown,装个编辑器反而多一道障碍。
- 注册登录系统 —— 博客读者不需要注册。评论填个昵称邮箱就够。
- 第三方评论(Disqus、Giscus) —— 数据还是想自己存着。
- Docker 容器 —— 一个
python run.py能跑的东西,暂时不想再去学 Docker。 - Vue/React 做前端 —— 这些我不熟,而且对一个博客来说确实不需要。
这不是说它们不好,只是对目前的我来说不需要。
五、还没做的
写到这里,心里还惦记几件事:
- 文章的阅读量统计
- 把关于我页面写得像样点
- 更好看的 404 页面
- 可能的邮件订阅(也可能永远不做,RSS 已经够了)
不急,以后慢慢加。
六、写下来的收获
做这个博客花的时间比我预期的多,但学到的东西也比我预期的多:
- 第一次真正理解了 CSRF 是个什么东西(以前只听说过名字)
- 第一次自己写 sitemap 和 Open Graph
- 第一次做一个能按时自动发文的东西
- 第一次处理移动端适配这种细碎但重要的事
最大的一个感受是:一个网站看起来简单,真要做到”能用、好用、别人看得顺眼”,需要叠很多层小细节。每一层单拎出来都不难,难的是一样样都去做。
所以这个博客说到底也不是什么技术作品,就是一份写给自己的记录:我想把会的东西慢慢写出来,不会的东西慢慢学。
接下来可能会更新:
- 平时踩的坑(数据处理、Python 的各种小陷阱)
- 工具折腾笔记(编辑器、终端、Linux)
- 读书、看片的碎碎念
- 偶尔写的小说(会发在”小说专栏”,不和这里混在一起)
如果有人读到这里,谢谢你。下一篇见。
评论(0)
还没有评论,来说几句吧 :)
留下评论