- 新增 docs/blog.md:功能文档(入口/路由、涉及文件、接口、实现注记、上线) - plans/2026-06-25-blog-writing.md:29 个任务步骤全部勾选完成,加状态/偏差说明 - plans/2026-06-18-deploy.md:补写博客全量重部署说明(BLOG_DIR 等) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
5.8 KiB
写博客 Blog(个人 Markdown 博客 + #post/<id> 路由)
登录后的个人博客:按登录用户隔离,可写 / 存 / 读 / 改 / 删 Markdown 文章。文章正文走
Markdown 渲染,单篇文章有 #post/<id> 可分享路由(刷新/直链都能定位到该文章)。
职责切分:文章正文按登录用户存服务器(最多 200 篇/人、严格私有、JWT 鉴权),
每篇一个 JSON 文件;readingTime/excerpt 等派生信息不入库,前端实时算。
设计方案见 specs/2026-06-25-blog-writing-design.md, 实现计划见 plans/2026-06-25-blog-writing.md。
入口与路由
博客页是开发者入口门(dev_gate)登录后的默认页(非 #photo、非特殊文件夹时)。
列表↔文章的唯一真相源是 App.tsx 的地址栏 hash:
浏览器(已登录,无 #photo / 文件夹 hash)
└─ BlogPage:Hero + 文章列表
点某篇文章 → 地址栏置 #post/<id>
└─ App 解析 #post/<id> → 传 initialPostId 给 BlogPage → 渲染 PostArticle 全文
未登录直接开 #post/<id> → 复用登录页,登录后落到该文章
- 列表↔文章由 hash 驱动(可分享、可刷新、浏览器后退回列表);编辑器浮层不进 URL (不可分享,符合设计)。
- 文章页「编辑」保存后,靠
articleNonce作PostArticle的key强制重挂载, 保证显示最新内容(否则 id 不变其 fetch effect 不重跑)。
涉及文件
前端:
Client/src/App.tsx— 解析#post/<id>:置postId,传token/initialPostId给BlogPage;#post/<id>未登录时也走登录页。Client/src/components/BlogPage.tsx— 外壳(Hero / 三栏 / 页脚)+ 中心三态内容 (列表 / 文章 / 编辑器),列表读真实数据,写死的占位数组已删除。Client/src/components/PostArticle.tsx— 全文阅读页(Markdown 渲染、编辑入口、 删除二次确认)。Client/src/components/PostEditor.tsx— 写 / 改文章(标题 / 标签 / 正文 + Markdown 预览)。Client/src/components/Markdown.tsx—react-markdown+remark-gfm,套站点暖色编辑风; 默认不解析原始 HTML(未引rehype-raw),天然防注入。Client/src/api/posts.ts— 文章接口封装(见下)+ 派生工具readingTime/excerpt。
后端(Rust):
Server/src/posts.rs—PostStore:按用户隔离到BLOG_DIR/<用户>/<hex>.json,含 路径安全(valid_id仅小写 hex ≤32)、限额、create加锁防并发越额;单测覆盖 CRUD / 倒序 / 坏输入 / 上限 / 用户隔离 / 坏 id。Server/src/state.rs—AppState新增posts: PostStore字段。Server/src/routes/web.rs— 文章接口(见下);CORS 放行PUT。Server/src/main.rs—mod posts;。- 复用
Server/src/routes/extract.rs的AuthUser提取器、photos::sanitize_user。
接口(均经 AuthUser 鉴权,按用户隔离)
| 方法 | 路径 | 行为 |
|---|---|---|
| GET | /api/web/posts |
列出当前用户文章(按 createdAt 倒序,{ items:[Post], max:200 }) |
| POST | /api/web/posts |
新建(body { title, tag, body })→ 201 + Post;满 200 篇 409、超 64KB 413、标题空/标签过长 400 |
| GET | /api/web/posts/{id} |
取单篇 Post(属主校验,非法/不存在 404) |
| PUT | /api/web/posts/{id} |
更新 → 204(创建时间不变,更新时间刷新) |
| DELETE | /api/web/posts/{id} |
删除 → 204(不存在 404) |
Post 字段 camelCase:id, title, tag, body, createdAt, updatedAt(秒级时间戳)。
上限常量:MAX_POSTS=200、MAX_BODY=64KiB、标题 ≤200 字、标签 ≤40 字,标题 trim 后不得为空。
存储:BLOG_DIR(默认 blog,线上 /opt/internet-project/blog/)下每用户一子目录,
每篇随机十六进制文件名 <hex>.json。读取已鉴权,文件名干无需密码学随机。
功能
- 写 / 改:标题、标签、Markdown 正文;编辑器内「Write / Preview」切换实时预览。
- 读:全文 Markdown 渲染(标题/列表/引用/代码块/链接/图片/分隔线/GFM 表格等), 顶部显示标签、日期、估算阅读时长。
- 删:文章页二次确认(「删除」→「确认删除」)。
- 列表:最新在前,每篇卡片显示日期、标签、摘要(剥除 Markdown 标记)、阅读时长; 空态引导「写文章」。
- 派生:
readingTime(~200 字/分钟,最少 1 分钟)、excerpt(剥标记取前 80 字) 均前端实时算,不入库。
实现注记
- 路由真相源单一:列表↔文章统一由
App.tsx经 hash 决定(initialPostId),BlogPage仅管编辑器浮层,避免双方争用 URL。 - 就地编辑刷新:
PostArticle的 fetch effect 依赖[token, id],id 不变时不重拉; 故编辑保存后 bumparticleNonce并用作key强制重挂载。 - Markdown code 边界:以
language-类名区分行内/块级,未指定语言的围栏块按行内样式 渲染(写```lang即按块级)——个人博客可接受,不引额外判定增复杂度。 - 防注入:未引
rehype-raw,原始 HTML 不解析;正文区data-selectable放开选择以便复制。
线上访问
http://8.130.143.54:8548/(开发者入口门登录后即默认进博客页)。博客引入后端改动 →
走全量重部署(重编 Rust → 传二进制 → restart_rust.sh 重启),非 frontend-only。
存储目录 BLOG_DIR 默认 blog(即 /opt/internet-project/blog/),首次写文章自动建
blog/<用户>/。复用现有 weather.env 的 DATABASE_URL(登录校验)与 JWT_SECRET(token 签名)。