Files
nanocode/personal-blog/docs/05-markdown-rendering.md
T
Ubuntu 32b43f85bd a
2026-07-03 16:58:28 +08:00

443 lines
12 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.
# Personal Blog - Markdown 渲染技术方案
> 后端生成 HTML → 访客端直接渲染
> 管理端实时预览 → react-markdown
---
## 1. 整体策略
项目采用 **"后端预渲染 HTML + 前端直接展示"** 的架构:
```
数据库 (Markdown 文本)
│
▼
后端 FastAPI ── article_to_html() ──► HTML 字符串
│ │
│ API 响应 │
│ { content_html: "..." } │
│ │
└──────────────┬───────────────┘
│
▼
访客端 React
dangerouslySetInnerHTML
│
▼
浏览器渲染最终页面
```
### 1.1 为什么选择后端预渲染?
1. **性能**:访客端不需要加载 Markdown 解析库,减少 JS bundle 体积
2. **一致性**:前后端解析规则统一,避免渲染差异
3. **SEO**:服务端返回的 HTML 可被搜索引擎抓取(配合 SSR 或预渲染更优)
4. **安全**:后端可控 HTML 净化,防止 XSS
---
## 2. 后端渲染 (app.py)
### 2.1 核心函数:`article_to_html`
```python
def article_to_html(content: str) -> str:
"""将 Markdown 内容转换为 HTML"""
if not content:
return ""
# 转换 Markdown → HTML
html_content = markdown.markdown(
content,
extensions=[
'extra', # 表格、属性列表、定义列表等
'codehilite', # 代码语法高亮
'toc', # 目录生成
'fenced_code', # 围栏代码块
]
)
# 包装容器
return f'<div class="article-content">{html_content}</div>'
```
### 2.2 Markdown 扩展说明
| 扩展 | 功能 |
|------|------|
| `extra` | 支持表格、缩写、属性列表、定义列表、围栏代码块、脚注等 |
| `codehilite` | 代码块语法高亮(需 Pygments) |
| `toc` | 自动生成目录(Table of Contents) |
| `fenced_code` | GitHub 风格的三反引号代码块 |
### 2.3 支持的 Markdown 语法
| 语法 | 示例 | 输出 |
|------|------|------|
| 标题 | `# 一级标题` | `<h1>` |
| 加粗 | `**文字**` | `<strong>` |
| 斜体 | `*文字*` | `<em>` |
| 链接 | `[文字](url)` | `<a>` |
| 图片 | `![描述](url)` | `<img>` |
| 引用 | `> 引用内容` | `<blockquote>` |
| 列表 | `- 项目` / `1. 项目` | `<ul>` / `<ol>` |
| 代码行 | `` `代码` `` | `<code>` |
| 代码块 | ` ```python ` | `<pre><code class="language-python">` |
| 表格 | `| 列1 | 列2 |` | `<table>` |
| 分隔线 | `---` | `<hr>` |
### 2.4 代码高亮
`codehilite` 扩展配合 Pygments 实现代码高亮:
```python
# 输入 (Markdown)
```python
def hello():
print("Hello World")
```
# 输出 (HTML)
<div class="codehilite">
<pre><span></span><code>
<span class="k">def</span> <span class="nf">hello</span><span class="p">():</span>
<span class="nb">print</span><span class="p">(</span><span class="s2">"Hello World"</span><span class="p">)</span>
</code></pre>
</div>
```
**注意**:当前项目 `requirements.txt` 未包含 `pygments`,代码高亮样式可能不生效。如需完整高亮,需额外安装:
```bash
pip install pygments
```
### 2.5 文章 API 的 HTML 字段
文章详情接口返回双字段:
```json
{
"id": 1,
"title": "文章标题",
"content": "# Markdown 原文\n\n正文内容...",
"content_html": "<div class=\"article-content\"><h1>Markdown 原文</h1><p>正文内容...</p></div>",
"summary": "摘要",
"...": "..."
}
```
- `content`:原始 Markdown,用于管理端编辑
- `content_html`:预渲染 HTML,用于访客端展示
---
## 3. 访客端渲染 (PostDetail.jsx)
### 3.1 渲染方式
访客端直接使用 `dangerouslySetInnerHTML` 注入后端返回的 HTML:
```jsx
// PostDetail.jsx
<div
className="article-content"
dangerouslySetInnerHTML={{ __html: post.content_html }}
/>
```
### 3.2 样式隔离
通过 CSS 类 `.article-content` 对文章正文进行样式隔离,避免与页面其他样式冲突:
```css
/* index.css 中的文章样式 */
.article-content {
@apply text-gray-800 leading-relaxed;
}
.article-content h1 {
@apply text-2xl font-bold mt-8 mb-4;
}
.article-content h2 {
@apply text-xl font-bold mt-6 mb-3;
}
.article-content p {
@apply mb-4;
}
.article-content pre {
@apply bg-gray-100 p-4 rounded-lg overflow-x-auto mb-4;
}
.article-content code {
@apply bg-gray-100 px-1 py-0.5 rounded text-sm;
}
.article-content pre code {
@apply bg-transparent p-0;
}
.article-content blockquote {
@apply border-l-4 border-brutal-yellow pl-4 italic my-4;
}
.article-content img {
@apply max-w-full rounded-lg;
}
.article-content table {
@apply w-full border-collapse mb-4;
}
.article-content th,
.article-content td {
@apply border border-gray-300 px-3 py-2;
}
.article-content th {
@apply bg-gray-100 font-bold;
}
```
### 3.3 XSS 防护
当前项目依赖 **后端输出可信 HTML** 来防范 XSS:
- Markdown 解析在后端完成
- 不渲染用户直接输入的 HTML(Markdown 语法会被转义为纯文本)
- 图片 URL 经过下载到本地处理,避免外链图片的潜在风险
**局限性**:当前未使用 `bleach` 等 HTML 净化库对输出进行过滤。如果管理员在 Markdown 中嵌入恶意 HTML(如 `<script>`),会直接输出到页面。
**建议改进**:
```python
# 使用 bleach 净化 HTML
import bleach
allowed_tags = [
'p', 'br', 'strong', 'em', 'a', 'img', 'h1', 'h2', 'h3',
'h4', 'h5', 'h6', 'ul', 'ol', 'li', 'blockquote', 'pre',
'code', 'table', 'thead', 'tbody', 'tr', 'th', 'td', 'hr',
'div', 'span'
]
allowed_attrs = {
'a': ['href', 'title'],
'img': ['src', 'alt', 'title'],
'*': ['class'],
}
html_content = bleach.clean(html_content, tags=allowed_tags, attributes=allowed_attrs)
```
---
## 4. 管理端实时预览 (PostEdit.jsx)
### 4.1 预览实现
管理端文章编辑页面使用 `react-markdown` 实现实时预览:
```jsx
import ReactMarkdown from 'react-markdown'
// 编辑页面
<div className="grid grid-cols-2 gap-4">
{/* 左侧:编辑区 */}
<textarea
value={content}
onChange={(e) => setContent(e.target.value)}
/>
{/* 右侧:实时预览 */}
<div className="prose max-w-none">
<ReactMarkdown>{content}</ReactMarkdown>
</div>
</div>
```
### 4.2 react-markdown 配置
```jsx
import ReactMarkdown from 'react-markdown'
import remarkGfm from 'remark-gfm'
<ReactMarkdown
remarkPlugins={[remarkGfm]} // 支持 GitHub Flavored Markdown
components={{
// 自定义组件映射
img: ({ node, ...props }) => (
<img {...props} className="max-w-full rounded-lg" />
),
code: ({ node, inline, className, children, ...props }) => {
if (inline) {
return <code className="bg-gray-100 px-1 py-0.5 rounded" {...props}>{children}</code>
}
return (
<pre className="bg-gray-100 p-4 rounded-lg overflow-x-auto">
<code className={className} {...props}>{children}</code>
</pre>
)
}
}}
>
{content}
</ReactMarkdown>
```
### 4.3 预览 vs 访客端渲染差异
| 维度 | 管理端预览 | 访客端渲染 |
|------|-----------|-----------|
| **库** | react-markdown + remark-gfm | Python markdown 库 |
| **扩展** | remark 插件生态 | Python-markdown 扩展 |
| **代码高亮** | 需额外配置 prismjs/highlight.js | codehilite + Pygments |
| **表格** | ✅ 支持 | ✅ 支持 |
| **任务列表** | ✅ (remark-gfm) | ❌ 不支持 |
| **数学公式** | ❌ 不支持 | ❌ 不支持 |
| **目录** | ❌ 不支持 | ✅ (toc 扩展) |
**说明**:由于前后端使用不同的 Markdown 解析引擎,极端情况下可能出现渲染差异。建议在管理端预览后,再到访客端确认最终效果。
---
## 5. 图片渲染
### 5.1 图片 URL 类型
| 来源 | URL 格式 | 示例 |
|------|---------|------|
| 本地上传 | `/uploads/{filename}` | `/uploads/abc123.jpg` |
| 采集图片 | `/uploads/{filename}` | `/uploads/crawl_abc123.jpg` |
| 微信图片 | `/uploads/wechat_{timestamp}/{filename}` | `/uploads/wechat_123/xxx.jpg` |
| 外部链接 | `https://...` | `https://example.com/img.jpg` |
### 5.2 图片路径解析
后端 `app.py` 注册了静态文件服务:
```python
app.mount("/uploads", StaticFiles(directory="uploads"), name="uploads")
```
前端开发环境通过 Vite proxy 代理到后端:
```javascript
// vite.config.js
server: {
proxy: {
'/uploads': { target: 'http://localhost:8889', changeOrigin: true },
},
}
```
生产环境 Nginx 直接代理:
```nginx
location /uploads/ {
proxy_pass http://localhost:8889/uploads/;
}
```
---
## 6. 首页摘要渲染 (Home.jsx)
首页文章列表不展示完整正文,而是展示摘要:
### 6.1 摘要来源
1. **优先使用 `summary` 字段**:管理员手动输入的摘要
2. **自动截取正文**:如 `summary` 为空,从 `content` 截取前 120 个字符
### 6.2 截取逻辑
```javascript
// Home.jsx
const readMore = (content, maxLength = 120) => {
if (!content) return ''
// 移除 Markdown 标记
const plainText = content
.replace(/#+ /g, '') // 移除标题标记
.replace(/\*\*/g, '') // 移除加粗
.replace(/\*/g, '') // 移除斜体
.replace(/`{1,3}/g, '') // 移除代码标记
.replace(/\[([^\]]+)\]\([^)]+\)/g, '$1') // 链接 → 文本
.replace(/!\[([^\]]*)\]\([^)]+\)/g, '') // 移除图片
.replace(/\n+/g, ' ') // 换行 → 空格
.trim()
if (plainText.length <= maxLength) return plainText
return plainText.substring(0, maxLength) + '...'
}
```
### 6.3 摘要渲染
```jsx
<p className="text-gray-600 text-sm line-clamp-3">
{post.summary || readMore(post.content, 120)}
</p>
```
使用 Tailwind 的 `line-clamp-3` 限制最多 3 行显示。
---
## 7. 渲染流程总结
```
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ 管理端编辑 │ │ 后端存储 │ │ 访客端展示 │
├─────────────────┤ ├─────────────────┤ ├─────────────────┤
│ │ │ │ │ │
│ Textarea 输入 │────►│ SQLite │────►│ GET /api/posts │
│ Markdown 文本 │ │ content 字段 │ │ │
│ │ │ │ │ 返回 content │
│ react-markdown │ │ article_to_html │ │ + content_html │
│ 实时预览 │ │ 预渲染 HTML │ │ │
│ │ │ content_html │ │ dangerouslySet │
│ │ │ │ │ InnerHTML 渲染 │
└─────────────────┘ └─────────────────┘ └─────────────────┘
```
---
## 8. 扩展建议
### 8.1 增加语法高亮
```bash
pip install pygments
```
后端 `article_to_html` 已配置 `codehilite` 扩展,安装 Pygments 后即可自动生效。
前端管理端可集成 `react-syntax-highlighter`:
```bash
npm install react-syntax-highlighter
```
### 8.2 增加数学公式支持
后端:使用 `python-markdown-math` 扩展
前端:使用 `remark-math` + `rehype-katex`
### 8.3 增加目录生成
后端 `toc` 扩展已启用,可在 `content_html` 中生成 `<div class="toc">` 目录结构。访客端需在 PostDetail.jsx 中单独展示目录。
### 8.4 HTML 净化
建议引入 `bleach` 库对 `content_html` 输出进行白名单过滤,防止 XSS。
---
*本文档由 nanobot 根据项目源码自动生成。*