PersonalWebApplication/docs/blog.md

96 lines
5.8 KiB
Markdown
Raw Normal View History

# 写博客 Blog个人 Markdown 博客 + `#post/<id>` 路由)
登录后的个人博客:按登录用户隔离,可写 / 存 / 读 / 改 / 删 Markdown 文章。文章正文走
Markdown 渲染,单篇文章有 `#post/<id>` 可分享路由(刷新/直链都能定位到该文章)。
**职责切分**:文章正文**按登录用户存服务器**(最多 200 篇/人、严格私有、JWT 鉴权),
每篇一个 JSON 文件;`readingTime`/`excerpt` 等派生信息不入库,前端实时算。
设计方案见 [specs/2026-06-25-blog-writing-design.md](superpowers/specs/2026-06-25-blog-writing-design.md)
实现计划见 [plans/2026-06-25-blog-writing.md](superpowers/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**
(不可分享,符合设计)。
- 文章页「编辑」保存后,靠 `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 不变时不重拉;
故编辑保存后 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.env``DATABASE_URL`(登录校验)与 `JWT_SECRET`token 签名)。