# Personal Blog - Markdown + HTML 双格式文章改造计划
> 目标:支持 Markdown 和 HTML 两种格式录入文章,访客端正确渲染,图片路径兼容现有规则。
---
## 1. 整体策略
```
┌─────────────────────────────────────────────────────────────┐
│ 后台编辑器 │
│ ┌──────────────┐ ┌──────────────┐ │
│ │ @uiw/react- │ │ react-quill │ │
│ │ md-editor │ │ -new (HTML) │ │
│ └──────┬───────┘ └──────┬───────┘ │
│ │ │ │
│ ▼ ▼ │
│ content_type='markdown' content_type='html' │
│ │ │ │
│ └────────┬────────┘ │
│ ▼ │
│ 后端 API (posts.py) │
│ 保存到数据库 (content_type 字段) │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ 访客端渲染 │
│ ┌─────────────────┐ ┌─────────────────┐ │
│ │ content_type │ │ content_type │ │
│ │ == 'markdown' │ │ == 'html' │ │
│ └────────┬────────┘ └────────┬────────┘ │
│ │ │ │
│ ▼ ▼ │
│ renderMarkdown() DOMPurify.sanitize() │
│ (现有轻量渲染) (XSS 过滤后渲染) │
│ │ │ │
│ └────────┬───────────┘ │
│ ▼ │
│ dangerouslySetInnerHTML │
└─────────────────────────────────────────────────────────────┘
```
---
## 2. 改造步骤清单
| 步骤 | 模块 | 状态 | 说明 |
|------|------|------|------|
| 2.1 | 后端 models.py | ✅ 已完成 | Post 表新增 `content_type` 字段,默认 `'markdown'` |
| 2.2 | 后端 schemas.py | ✅ 已完成 | PostCreate, PostUpdate, PostOut 均添加 `content_type` |
| 2.3 | 后端 posts.py | ✅ 已完成 | 创建/更新接口读写 `content_type`,列表/详情接口返回 |
| 2.4 | 前端依赖 | ✅ 已完成 | 安装 `@uiw/react-md-editor`、`react-quill-new`、`dompurify` |
| 2.5 | 前端 PostEdit.jsx | ✅ 已完成 | 重写编辑器,支持 Markdown/HTML 切换,图片上传兼容现有规则 |
| 2.6 | 前端 PostDetail.jsx | ✅ 已完成 | 根据 `content_type` 选择渲染方式,HTML 需 DOMPurify 过滤 |
| 2.7 | 前端 Posts.jsx | ✅ 已完成 | 文章列表显示 `content_type` 标签 |
| 2.8 | 数据库兼容 | ✅ 已完成 | SQLite 添加字段,现有 111 篇文章默认设为 `markdown` |
| 2.9 | 构建验证 | ✅ 已完成 | `npm run build` 通过(7.21s),有 CSS 警告但非阻塞 |
---
## 3. 后端变更详情
### 3.1 models.py
```python
class Post(Base):
# ... 现有字段 ...
content_type = Column(String(10), default='markdown') # 'markdown' | 'html'
```
### 3.2 schemas.py
```python
class PostCreate(BaseModel):
# ...
content_type: str = Field(default='markdown', pattern='^(markdown|html)$')
class PostUpdate(BaseModel):
# ...
content_type: Optional[str] = None
class PostOut(PostBase):
# ...
content_type: str
```
### 3.3 posts.py
- `create_post`: 自动写入 `content_type`
- `update_post`: 自动透传 `content_type`
- 列表/详情接口返回 `content_type`
---
## 4. 前端变更详情
### 4.1 PostEdit.jsx(已完成)
- 引入 `MDEditor`(Markdown)和 `ReactQuill`(HTML)
- 增加格式切换按钮(Markdown / 富文本 HTML)
- 图片上传复用现有 `/api/upload` 接口
- Markdown 模式插入 ``
- HTML 模式插入 `
`
- 缩略图路径格式:`/uploads/thumbnails/xxx.jpg`
### 4.2 PostDetail.jsx(待完成)
- 引入 `DOMPurify`
- 判断 `post.content_type`:
- `'markdown'` → 调用现有 `renderMarkdown()`
- `'html'` → `DOMPurify.sanitize(post.content, { FORBID_TAGS: ['script', 'iframe'] })`
### 4.3 Posts.jsx(待完成)
- 在文章列表项中增加 `content_type` 标签显示
---
## 5. 图片路径规则
| 项目 | 路径格式 | 说明 |
|------|----------|------|
| 原图 | `/uploads/{filename}` | 上传接口返回的 `file_path` |
| 缩略图 | `/uploads/thumbnails/{filename}` | 上传接口返回的 `thumb_path` |
| 完整 URL | `{API_BASE_URL.replace('/api', '')}{path}` | 前端拼接 |
编辑器插入的图片路径使用相对路径 `/uploads/xxx.jpg`,访客端渲染时自动补全为完整 URL。
---
## 6. XSS 防护策略
- **输入侧**:Quill 编辑器本身会过滤部分危险标签;后端不二次过滤 HTML 内容(保留用户原始输入)
- **输出侧**:访客端使用 `DOMPurify.sanitize()` 过滤,禁止:
- `