从零搭一个属于自己的博客——RuiBlog 开发记事

这是这个博客的第一篇文章。

我不是专业做 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 是个什么东西(以前只听说过名字)
  • 第一次自己写 sitemapOpen Graph
  • 第一次做一个能按时自动发文的东西
  • 第一次处理移动端适配这种细碎但重要的事

最大的一个感受是:一个网站看起来简单,真要做到”能用、好用、别人看得顺眼”,需要叠很多层小细节。每一层单拎出来都不难,难的是一样样都去做。

所以这个博客说到底也不是什么技术作品,就是一份写给自己的记录:我想把会的东西慢慢写出来,不会的东西慢慢学

接下来可能会更新:

  • 平时踩的坑(数据处理、Python 的各种小陷阱)
  • 工具折腾笔记(编辑器、终端、Linux)
  • 读书、看片的碎碎念
  • 偶尔写的小说(会发在”小说专栏”,不和这里混在一起)

如果有人读到这里,谢谢你。下一篇见。

评论(0)

还没有评论,来说几句吧 :)

留下评论