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

141 lines
6.9 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 写博客功能 设计方案
日期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>.json`id 用和照片同样的 16 位 hex 文件名干(`new_stem` 思路;读取已鉴权,无需密码学随机)。
- 文件内容(持久化结构):
```json
{
"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 |
标题去除首尾空白后不得为空400。`SaveError` 枚举映射 HTTP 状态码,和 photos.rs 同构。
### 接口(`Server/src/routes/web.rs`,全部 `AuthUser` 鉴权)
新增子路由 `post_routes`body 上限用默认 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 }`。
`readingTime``excerpt` **不入库、不走接口**,由前端按正文实时算(阅读时长 ≈ 字数/200 分钟;摘要取正文前 ~80 字、去掉 Markdown 标记)。
### state.rs
`AppState` 加一个字段 `pub posts: PostStore``AppState::new()` 里 `PostStore::from_env()`
## 前端
### api/posts.ts新增
仿 `api/photos.ts``listPosts / 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` → Fraunces`font-display`)、`text-text`,逐级缩小。
- `p``text-muted` 行距宽松;`a` → `text-accent` 下划线偏移。
- `code`(行内)→ 暖色背景小药丸;`pre code`(块)→ `surface` 底、`line` 描边、等宽。
- `blockquote` → 左侧 accent 竖条;`ul/ol/li`、`img`(圆角、最大宽度)。
- 仅渲染受信任的自有内容;`react-markdown` 默认不渲染原始 HTML保持默认即可防注入
### 路由(改 `App.tsx`
在现有 hash 解析器里识别 `post/<id>` 前缀:
-`hash``post/` 开头 → 当作博客视图(登录后可见),把 `<id>` 作为 `initialPostId` 传给 `BlogPage`**不**走 folder/gate 检查。
- 未登录访问 `#post/<id>` → 复用登录页(像 `#photo`),登录后落到该文章。
- 空 hash + 已登录 → `BlogPage` 列表(不变)。
`BlogPage` 负责把"列表 ↔ 阅读"反映到地址栏(列表抹掉 hash阅读 `history` 切到 `#post/<id>`),并监听 `hashchange` 与浏览器后退保持同步。编辑态用组件内 state不进 URL。
### 组件拆分(职责单一、可独立理解/测试)
- `BlogPage.tsx`(改):编排 + 列表。挂载时 `listPosts`;渲染真实文章卡(替换写死的 `POSTS`,保留现有卡片视觉:日期/标签、彩虹标题、摘要、阅读时长)。顶部 Hero 旁加"写文章"入口(每人都是自己博客作者,常驻)。`initialPostId` 存在则首屏直接进阅读页。视图 state`list | reading | editing`。
- `PostArticle.tsx`(新增):全文阅读页。`getPost` 取单篇 → `Markdown` 渲染;含返回、编辑、删除(删除二次确认)。
- `PostEditor.tsx`(新增):标题输入 + 标签输入 + 正文 `textarea` + 实时预览(复用 `Markdown`+ 保存/取消。新建与编辑共用(有 id 即编辑)。
- `Markdown.tsx`(新增):见上。
### 边界情况
- 空列表:友好引导态("还没有文章,写第一篇吧")。
- 加载中 / 取单篇 404落回列表并提示
- 保存失败:按状态码区分提示(满 / 太长 / 输入非法 / 失败),沿用 photos 的错误区分风格。
- 删除:二次确认。
## 测试
- 后端 `posts.rs` 单测,仿 `photos.rs``sanitize`/`valid_id` 守卫、create→list→read→update→delete 往返、`MAX_POSTS`/`MAX_BODY`/空标题的拒绝、跨用户隔离(读不到别人的)。
- 前端以手工验证为主(列表/写/读/改/删/刷新回到文章/未登录深链)。
## 涉及文件
新增:`Server/src/posts.rs`、`Client/src/api/posts.ts`、`Client/src/components/PostArticle.tsx`、`Client/src/components/PostEditor.tsx`、`Client/src/components/Markdown.tsx`。
修改:`Server/src/main.rs`mod posts、`Server/src/state.rs`、`Server/src/routes/web.rs`、`Client/src/App.tsx`、`Client/src/components/BlogPage.tsx`、`Client/package.json`react-markdown、remark-gfm