# 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 模式插入 `![描述](/uploads/xxx.jpg)` - 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()` 过滤,禁止: - `