现代 SPA 很强大,但博客这类内容站其实只需要一点点交互——HTMX 是恰到好处的答案。
服务端返回 HTML 片段,HTMX 负责局部替换,不需要任何构建工具
<form hx-post="/comment" hx-target="#comments" hx-swap="afterbegin">
<input name="content" required>
<button>提交评论</button>
</form>
<script> 就引入,没有打包器、没有框架版本地狱。局部交互(折叠、主题切换、表单状态)交给 Alpine,两者分工明确:
这套组合让博客的前端代码总量控制在几百行以内。
]]>七月读了四本书,记录一下。
关于中国政府与经济发展的通识读物。地方政府行为是理解中国经济的一把钥匙,这本书讲得清楚、克制。
金句摘抄:
如果在一个艰难的决定上意见不统一,你就应该选择短期内更痛苦的道路。
经典中的经典,第 2 章的二分查找变体至今仍值得反复琢磨。算法题刷多了容易忘本,回到珠玑能找回初心。
“真正重要的东西,用眼睛是看不见的。” 成年后重读,每一页都是新的。
]]>SQLite 的 FTS5 提供 trigram 分词器:按连续三字符切分,天然适配中文子串匹配,不依赖词表与外部进程。
CREATE VIRTUAL TABLE posts_fts USING fts5(
title, content_md,
content='posts', content_rowid='id',
tokenize='trigram'
);
外部内容表(external content)不重复存储正文,通过触发器与 posts 同步。
bm25(posts_fts) 分数越小相关度越高(与旧版 MySQL 的 MATCH ... AGAINST 方向相反)。
if can_use_fulltext(&keyword) {
// FTS5 MATCH + bm25 排序
} else {
// 单字/双字词:LIKE 兜底
}
中文搜索没有银弹,结合业务量选择合适的召回策略即可。
]]>Axum 0.8 的路径参数语法从 :param 改为 {param}。
Router::new()
.route("/", get(index))
.route("/post/{slug}", get(post))
.route("/admin", get(admin).layer(auth_layer))
State、Query、Path、Session 可以任意组合,按声明顺序提取。
async fn handler(
State(state): State<AppState>,
Query(q): Query<SearchQuery>,
Path(slug): Path<String>,
) -> Result<Html<String>, AppError> {
Ok(Html(format!("slug={slug}")))
}
layer 是逆序叠加:后加的 layer 更靠近内核。静态资源、压缩、Session 各就各位。
个人博客是检验一个 Web 框架的好实验场:功能完整、边界清晰、可以随时推到重来。
性能与内存安全是首要理由。单进程处理静态渲染的博客毫无压力,但编译期的类型检查能帮你少写一半的调试代码。
| 层次 | 选择 |
|---|---|
| Web 框架 | Axum |
| 数据库 | SQLite(FTS5 trigram 全文索引) |
| 模板 | Askama(编译期类型安全) |
| 前端 | Tailwind + Alpine + HTMX |
文章保存时一次性渲染 Markdown 并存储 HTML,前台请求零渲染开销:
let rendered = markdown::render(&md);
let summary = markdown::summary(&md, 200);
标题支持 {#锚点} 显式指定,也可以自动生成,目录树在详情页侧栏展示。
欢迎在留言板与我交流。
]]>