PersonalWebApplication/docs/blog.md
cc edde910cfe docs(blog): 写博客功能文档 + 计划标记完成 + 部署说明
- 新增 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>
2026-06-27 17:17:43 +08:00

5.8 KiB
Raw Permalink Blame History

写博客 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
  └─ BlogPageHero + 文章列表
       点某篇文章 → 地址栏置 #post/<id>
  └─ App 解析 #post/<id> → 传 initialPostId 给 BlogPage → 渲染 PostArticle 全文
       未登录直接开 #post/<id> → 复用登录页,登录后落到该文章
  • 列表↔文章由 hash 驱动(可分享、可刷新、浏览器后退回列表);编辑器浮层不进 URL (不可分享,符合设计)。
  • 文章页「编辑」保存后,靠 articleNoncePostArticlekey 强制重挂载, 保证显示最新内容(否则 id 不变其 fetch effect 不重跑)。

涉及文件

前端:

  • Client/src/App.tsx — 解析 #post/<id>:置 postId,传 token/initialPostIdBlogPage#post/<id> 未登录时也走登录页。
  • Client/src/components/BlogPage.tsx — 外壳Hero / 三栏 / 页脚)+ 中心三态内容 (列表 / 文章 / 编辑器),列表读真实数据,写死的占位数组已删除。
  • Client/src/components/PostArticle.tsx — 全文阅读页Markdown 渲染、编辑入口、 删除二次确认)。
  • Client/src/components/PostEditor.tsx — 写 / 改文章(标题 / 标签 / 正文 + Markdown 预览)。
  • Client/src/components/Markdown.tsxreact-markdown + remark-gfm,套站点暖色编辑风; 默认不解析原始 HTML未引 rehype-raw),天然防注入。
  • Client/src/api/posts.ts — 文章接口封装(见下)+ 派生工具 readingTime/excerpt

后端Rust

  • Server/src/posts.rsPostStore:按用户隔离到 BLOG_DIR/<用户>/<hex>.json,含 路径安全(valid_id 仅小写 hex ≤32、限额、create 加锁防并发越额;单测覆盖 CRUD / 倒序 / 坏输入 / 上限 / 用户隔离 / 坏 id。
  • Server/src/state.rsAppState 新增 posts: PostStore 字段。
  • Server/src/routes/web.rs — 文章接口见下CORS 放行 PUT
  • Server/src/main.rsmod posts;
  • 复用 Server/src/routes/extract.rsAuthUser 提取器、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 字段 camelCaseid, title, tag, body, createdAt, updatedAt(秒级时间戳)。 上限常量:MAX_POSTS=200MAX_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 不变时不重拉; 故编辑保存后 bump articleNonce 并用作 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.envDATABASE_URL(登录校验)与 JWT_SECRETtoken 签名)。