PersonalWebApplication/docs/superpowers/specs/2026-06-25-blog-writing-design.md
2026-06-25 22:41:59 +08:00

6.9 KiB
Raw Blame History

写博客功能 设计方案

日期2026-06-25 状态:已与作者确认,待评审

目标

把博客页从写死的占位 POSTS 数组,变成能真正写、存、读、改、删的个人博客。

确认的产品决策:

  • 作者范围:每个登录用户有自己的博客,按用户隔离(像照片一样只看得到/改得了自己的文章)。
  • 正文格式Markdown阅读时渲染成排版。
  • 阅读体验:点列表里一篇 → 进独立全文阅读页。
  • 路由#post/<id> 真路由,文章 URL 可分享、可刷新。

非目标YAGNI

  • 不做评论、点赞、标签筛选、全文搜索。
  • 不做草稿/发布状态机(保存即"已发布",对自己可见)。
  • 不做图片上传进正文Markdown 里可引用外链图,但不新增上传管线)。
  • 不引 @tailwindcss/typography,手工套设计风格。

架构总览

前端 (React)                         后端 (axum)
─────────────                        ───────────
BlogPage  ── api/posts.ts ──HTTP──>  /api/web/posts  ── PostStore ──> blog/<user>/<id>.json
  ├ 列表                              (AuthUser 鉴权)
  ├ PostArticle  (#post/<id>)
  ├ PostEditor   (内部 state)
  └ Markdown (react-markdown)

后端

PostStore新增 Server/src/posts.rs

沿用 PhotoStore 套路(按用户隔离的磁盘目录、廉价 Clone、串行化临界区):

  • 根目录取 env BLOG_DIR,默认 blog/(相对进程 cwd

  • 用户目录 blog/<user>/,复用 crate::photos::sanitize_user 防路径穿越(仅 [A-Za-z0-9_-],长度 164

  • 每篇文章一个文件 <hex_id>.jsonid 用和照片同样的 16 位 hex 文件名干(new_stem 思路;读取已鉴权,无需密码学随机)。

  • 文件内容(持久化结构):

    {
      "title": "...",
      "tag": "...",
      "body": "...(Markdown 原文)",
      "createdAt": 1719300000,
      "updatedAt": 1719300000
    }
    
  • list(user):读目录下全部合法 <hex>.json,解析后按 createdAt 倒序返回。

  • read(user, id) / create(user, draft) / update(user, id, draft) / delete(user, id)

  • id 合法性:<hex>.json,仅小写 hex 干,杜绝路径穿越(仿 valid_id)。

上限与校验

上限 超限错误
单用户文章数 MAX_POSTS = 200 409 Conflict
正文字节 MAX_BODY = 64 * 1024 413 Payload Too Large
标题字符 200 400 Bad Request
标签字符 40 400 Bad Request

标题去除首尾空白后不得为空400SaveError 枚举映射 HTTP 状态码,和 photos.rs 同构。

接口(Server/src/routes/web.rs,全部 AuthUser 鉴权)

新增子路由 post_routesbody 上限用默认 2MB正文是文本足够

方法 路径 请求体 成功响应
GET /api/web/posts { items: Post[], max }
POST /api/web/posts {title,tag,body} 201 {id}
GET /api/web/posts/{id} Post
PUT /api/web/posts/{id} {title,tag,body} 204
DELETE /api/web/posts/{id} 204

Post(线上 JSONcamelCase{ id, title, tag, body, createdAt, updatedAt }

readingTimeexcerpt 不入库、不走接口,由前端按正文实时算(阅读时长 ≈ 字数/200 分钟;摘要取正文前 ~80 字、去掉 Markdown 标记)。

state.rs

AppState 加一个字段 pub posts: PostStoreAppState::new()PostStore::from_env()

前端

api/posts.ts新增

仿 api/photos.tslistPosts / getPost / createPost / updatePost / deletePost,统一带 Authorization: Bearer <token>API_BASE 取法一致。Post 类型与后端对齐。可区分错误(full / too-large / bad-input / failed)供 UI 提示。

Markdown.tsx新增

react-markdown + remark-gfm 封装。给一组自定义组件,套上暖色编辑风:

  • h1/h2/h3 → Frauncesfont-display)、text-text,逐级缩小。
  • ptext-muted 行距宽松;atext-accent 下划线偏移。
  • code(行内)→ 暖色背景小药丸;pre code(块)→ surface 底、line 描边、等宽。
  • blockquote → 左侧 accent 竖条;ul/ol/liimg(圆角、最大宽度)。
  • 仅渲染受信任的自有内容;react-markdown 默认不渲染原始 HTML保持默认即可防注入

路由(改 App.tsx

在现有 hash 解析器里识别 post/<id> 前缀:

  • hashpost/ 开头 → 当作博客视图(登录后可见),把 <id> 作为 initialPostId 传给 BlogPage走 folder/gate 检查。
  • 未登录访问 #post/<id> → 复用登录页(像 #photo),登录后落到该文章。
  • 空 hash + 已登录 → BlogPage 列表(不变)。

BlogPage 负责把"列表 ↔ 阅读"反映到地址栏(列表抹掉 hash阅读 history 切到 #post/<id>),并监听 hashchange 与浏览器后退保持同步。编辑态用组件内 state不进 URL。

组件拆分(职责单一、可独立理解/测试)

  • BlogPage.tsx(改):编排 + 列表。挂载时 listPosts;渲染真实文章卡(替换写死的 POSTS,保留现有卡片视觉:日期/标签、彩虹标题、摘要、阅读时长)。顶部 Hero 旁加"写文章"入口(每人都是自己博客作者,常驻)。initialPostId 存在则首屏直接进阅读页。视图 statelist | reading | editing
  • PostArticle.tsx(新增):全文阅读页。getPost 取单篇 → Markdown 渲染;含返回、编辑、删除(删除二次确认)。
  • PostEditor.tsx(新增):标题输入 + 标签输入 + 正文 textarea + 实时预览(复用 Markdown+ 保存/取消。新建与编辑共用(有 id 即编辑)。
  • Markdown.tsx(新增):见上。

边界情况

  • 空列表:友好引导态("还没有文章,写第一篇吧")。
  • 加载中 / 取单篇 404落回列表并提示
  • 保存失败:按状态码区分提示(满 / 太长 / 输入非法 / 失败),沿用 photos 的错误区分风格。
  • 删除:二次确认。

测试

  • 后端 posts.rs 单测,仿 photos.rssanitize/valid_id 守卫、create→list→read→update→delete 往返、MAX_POSTS/MAX_BODY/空标题的拒绝、跨用户隔离(读不到别人的)。
  • 前端以手工验证为主(列表/写/读/改/删/刷新回到文章/未登录深链)。

涉及文件

新增:Server/src/posts.rsClient/src/api/posts.tsClient/src/components/PostArticle.tsxClient/src/components/PostEditor.tsxClient/src/components/Markdown.tsx。 修改:Server/src/main.rsmod postsServer/src/state.rsServer/src/routes/web.rsClient/src/App.tsxClient/src/components/BlogPage.tsxClient/package.jsonreact-markdown、remark-gfm