Merge pull request 'update docs' (#3) from mac-update into main
Reviewed-on: Jiao77/NovaBlog#3
This commit is contained in:
@@ -171,6 +171,11 @@ heroImage: /images/hero.jpg
|
||||
- [Docker](https://www.docker.com/) - 容器化
|
||||
- [Nginx](https://nginx.org/) - 反向代理
|
||||
|
||||
## 📚 文档
|
||||
|
||||
- **[使用指南](./docs/user-guide.md)** - 博客使用教程,包括文章编写、MDX 组件、Typst 排版、主题定制等
|
||||
- **[开发文档](./docs/developer-guide.md)** - 面向开发者的技术文档,包括 API 接口、数据库结构、深度定制指南等
|
||||
|
||||
## 📜 License
|
||||
|
||||
MIT License © 2024
|
||||
804
docs/developer-guide.md
Normal file
804
docs/developer-guide.md
Normal file
@@ -0,0 +1,804 @@
|
||||
# NovaBlog 开发者文档
|
||||
|
||||
本文档面向希望深度定制 NovaBlog 的开发者,详细介绍系统架构、API 接口、数据库结构以及扩展开发指南。
|
||||
|
||||
---
|
||||
|
||||
## 目录
|
||||
|
||||
1. [系统架构](#系统架构)
|
||||
2. [项目结构](#项目结构)
|
||||
3. [API 接口文档](#api-接口文档)
|
||||
4. [数据库结构](#数据库结构)
|
||||
5. [前端组件开发](#前端组件开发)
|
||||
6. [后端服务扩展](#后端服务扩展)
|
||||
7. [部署配置](#部署配置)
|
||||
8. [性能优化](#性能优化)
|
||||
|
||||
---
|
||||
|
||||
## 系统架构
|
||||
|
||||
NovaBlog 采用 **静态渲染 + 轻量级微服务** 的解耦架构:
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ 用户浏览器 │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Nginx 反向代理 │
|
||||
│ (静态文件服务 + API 请求转发) │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
│ │
|
||||
▼ ▼
|
||||
┌─────────────────────┐ ┌──────────────────────────┐
|
||||
│ 静态文件 (CDN) │ │ API Server (Go) │
|
||||
│ HTML/CSS/JS/图片 │ │ - 用户认证 │
|
||||
│ │ │ - 评论管理 │
|
||||
│ 构建时生成 │ │ - 点赞系统 │
|
||||
└─────────────────────┘ │ - SQLite 数据库 │
|
||||
└──────────────────────────┘
|
||||
```
|
||||
|
||||
### 技术栈
|
||||
|
||||
| 层级 | 技术 | 说明 |
|
||||
|------|------|------|
|
||||
| 前端框架 | Astro 5.x | Islands Architecture,零 JS 默认输出 |
|
||||
| UI 组件 | Vue 3 | 交互式岛屿组件 |
|
||||
| 样式方案 | Tailwind CSS 4.x | 原子化 CSS |
|
||||
| 后端框架 | Go + Gin | 极致轻量,内存占用 < 20MB |
|
||||
| 数据库 | SQLite | 文件型数据库,无需额外服务 |
|
||||
| 认证方案 | JWT | 无状态认证 |
|
||||
|
||||
### 架构优势
|
||||
|
||||
1. **极致性能**:静态页面零运行时,JS 按需加载
|
||||
2. **低资源占用**:Go 服务 + SQLite 可在 512MB 内存环境运行
|
||||
3. **SEO 友好**:纯静态 HTML 输出,搜索引擎完美抓取
|
||||
4. **开发体验**:组件化开发,热重载支持
|
||||
|
||||
---
|
||||
|
||||
## 项目结构
|
||||
|
||||
```
|
||||
NovaBlog/
|
||||
├── src/ # 前端源码
|
||||
│ ├── components/ # Vue/Astro 组件
|
||||
│ │ ├── CommentSection.vue # 评论区组件
|
||||
│ │ ├── LikeButton.vue # 点赞按钮
|
||||
│ │ ├── LoginForm.vue # 登录表单
|
||||
│ │ ├── UserStatus.vue # 用户状态栏
|
||||
│ │ ├── Counter.vue # 计数器示例
|
||||
│ │ ├── TypstBlock.astro # Typst 渲染组件
|
||||
│ │ └── TableOfContents.astro # 目录组件
|
||||
│ ├── content/ # 内容集合
|
||||
│ │ ├── config.ts # 内容配置
|
||||
│ │ └── blog/ # 博客文章
|
||||
│ ├── layouts/ # 布局组件
|
||||
│ │ ├── BaseLayout.astro # 基础布局
|
||||
│ │ └── PostLayout.astro # 文章布局
|
||||
│ ├── pages/ # 页面路由
|
||||
│ │ ├── index.astro # 首页
|
||||
│ │ ├── login.astro # 登录页
|
||||
│ │ ├── blog/ # 博客相关页面
|
||||
│ │ ├── tags/ # 标签页面
|
||||
│ │ └── categories/ # 分类页面
|
||||
│ ├── styles/ # 全局样式
|
||||
│ │ └── global.css # CSS 变量和全局样式
|
||||
│ ├── env.d.ts # 类型声明
|
||||
│ └── mdx-components.ts # MDX 组件注册
|
||||
├── public/ # 静态资源
|
||||
│ ├── images/ # 图片资源
|
||||
│ └── favicon.svg # 网站图标
|
||||
├── server/ # 后端服务
|
||||
│ ├── cmd/server/main.go # 服务入口
|
||||
│ ├── internal/ # 内部模块
|
||||
│ │ ├── config/ # 配置管理
|
||||
│ │ ├── database/ # 数据库连接
|
||||
│ │ ├── handlers/ # HTTP 处理器
|
||||
│ │ ├── middleware/ # 中间件
|
||||
│ │ ├── models/ # 数据模型
|
||||
│ │ └── utils/ # 工具函数
|
||||
│ ├── data/ # 数据文件 (SQLite)
|
||||
│ ├── migrations/ # 数据库迁移
|
||||
│ ├── Dockerfile # Docker 构建文件
|
||||
│ └── go.mod # Go 依赖
|
||||
├── docs/ # 文档
|
||||
├── astro.config.mjs # Astro 配置
|
||||
├── tailwind.config.mjs # Tailwind 配置
|
||||
├── docker-compose.yml # Docker Compose 配置
|
||||
├── nginx.conf # Nginx 配置
|
||||
└── package.json # NPM 依赖
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## API 接口文档
|
||||
|
||||
### 基础信息
|
||||
|
||||
- **Base URL**: `http://localhost:8080/api`
|
||||
- **认证方式**: JWT Bearer Token
|
||||
- **内容格式**: JSON
|
||||
|
||||
### 认证接口
|
||||
|
||||
#### 注册用户
|
||||
|
||||
```http
|
||||
POST /api/auth/register
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"username": "string", // 必填,3-50字符
|
||||
"email": "string", // 必填,有效邮箱
|
||||
"password": "string", // 必填,6-50字符
|
||||
"nickname": "string" // 可选
|
||||
}
|
||||
```
|
||||
|
||||
**响应**:
|
||||
```json
|
||||
{
|
||||
"token": "eyJhbGciOiJIUzI1NiIs...",
|
||||
"user": {
|
||||
"id": 1,
|
||||
"username": "testuser",
|
||||
"email": "test@example.com",
|
||||
"nickname": "测试用户",
|
||||
"role": "user",
|
||||
"created_at": "2024-01-15T10:00:00Z"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**错误码**:
|
||||
- `400`: 请求参数无效
|
||||
- `409`: 用户名或邮箱已存在
|
||||
|
||||
#### 用户登录
|
||||
|
||||
```http
|
||||
POST /api/auth/login
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"username": "string", // 用户名或邮箱
|
||||
"password": "string"
|
||||
}
|
||||
```
|
||||
|
||||
**响应**: 同注册接口
|
||||
|
||||
#### 获取当前用户
|
||||
|
||||
```http
|
||||
GET /api/auth/me
|
||||
Authorization: Bearer <token>
|
||||
```
|
||||
|
||||
**响应**:
|
||||
```json
|
||||
{
|
||||
"user": {
|
||||
"id": 1,
|
||||
"username": "testuser",
|
||||
"email": "test@example.com",
|
||||
"nickname": "测试用户",
|
||||
"avatar": "https://...",
|
||||
"bio": "个人简介",
|
||||
"role": "user"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 更新用户资料
|
||||
|
||||
```http
|
||||
PUT /api/auth/profile
|
||||
Authorization: Bearer <token>
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"nickname": "string",
|
||||
"avatar": "string",
|
||||
"bio": "string"
|
||||
}
|
||||
```
|
||||
|
||||
### 评论接口
|
||||
|
||||
#### 获取文章评论
|
||||
|
||||
```http
|
||||
GET /api/comments?post_id={post_id}&page=1&page_size=20
|
||||
```
|
||||
|
||||
**参数**:
|
||||
- `post_id` (必填): 文章 ID
|
||||
- `page` (可选): 页码,默认 1
|
||||
- `page_size` (可选): 每页数量,默认 20,最大 100
|
||||
|
||||
**响应**:
|
||||
```json
|
||||
{
|
||||
"data": [
|
||||
{
|
||||
"id": 1,
|
||||
"post_id": "hello-novablog",
|
||||
"user_id": 1,
|
||||
"content": "很棒的文章!",
|
||||
"status": "approved",
|
||||
"created_at": "2024-01-15T10:00:00Z",
|
||||
"user": {
|
||||
"id": 1,
|
||||
"username": "testuser",
|
||||
"nickname": "测试用户",
|
||||
"avatar": "https://..."
|
||||
},
|
||||
"replies": [
|
||||
{
|
||||
"id": 2,
|
||||
"parent_id": 1,
|
||||
"content": "感谢支持!",
|
||||
"user": {...}
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"pagination": {
|
||||
"page": 1,
|
||||
"page_size": 20,
|
||||
"total": 100,
|
||||
"total_page": 5
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 创建评论
|
||||
|
||||
```http
|
||||
POST /api/comments
|
||||
Authorization: Bearer <token>
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"post_id": "string", // 必填
|
||||
"content": "string", // 必填,1-2000字符
|
||||
"parent_id": 1 // 可选,回复的评论ID
|
||||
}
|
||||
```
|
||||
|
||||
#### 删除评论
|
||||
|
||||
```http
|
||||
DELETE /api/comments/:id
|
||||
Authorization: Bearer <token>
|
||||
```
|
||||
|
||||
**权限**: 本人或管理员可删除
|
||||
|
||||
### 点赞接口
|
||||
|
||||
#### 切换点赞状态
|
||||
|
||||
```http
|
||||
POST /api/likes/toggle
|
||||
Content-Type: application/json
|
||||
Authorization: Bearer <token> // 可选
|
||||
|
||||
{
|
||||
"post_id": "string" // 必填
|
||||
}
|
||||
```
|
||||
|
||||
**响应**:
|
||||
```json
|
||||
{
|
||||
"liked": true, // 当前是否已点赞
|
||||
"like_count": 42 // 文章总点赞数
|
||||
}
|
||||
```
|
||||
|
||||
**说明**:
|
||||
- 已登录用户:基于 user_id 判断
|
||||
- 未登录用户:基于 IP Hash 判断(加盐防反向推导)
|
||||
|
||||
#### 获取点赞状态
|
||||
|
||||
```http
|
||||
GET /api/likes/status?post_id={post_id}
|
||||
Authorization: Bearer <token> // 可选
|
||||
```
|
||||
|
||||
**响应**: 同切换接口
|
||||
|
||||
### 错误响应格式
|
||||
|
||||
```json
|
||||
{
|
||||
"error": "错误信息描述"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 数据库结构
|
||||
|
||||
### users 表
|
||||
|
||||
```sql
|
||||
CREATE TABLE users (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
created_at DATETIME,
|
||||
updated_at DATETIME,
|
||||
deleted_at DATETIME, -- 软删除
|
||||
username VARCHAR(50) UNIQUE NOT NULL,
|
||||
email VARCHAR(100) UNIQUE NOT NULL,
|
||||
password VARCHAR(255) NOT NULL, -- bcrypt 哈希
|
||||
nickname VARCHAR(50),
|
||||
avatar VARCHAR(255),
|
||||
role VARCHAR(20) DEFAULT 'user', -- 'admin' | 'user'
|
||||
bio VARCHAR(500)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_users_deleted_at ON users(deleted_at);
|
||||
CREATE INDEX idx_users_username ON users(username);
|
||||
CREATE INDEX idx_users_email ON users(email);
|
||||
```
|
||||
|
||||
### comments 表
|
||||
|
||||
```sql
|
||||
CREATE TABLE comments (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
created_at DATETIME,
|
||||
updated_at DATETIME,
|
||||
deleted_at DATETIME,
|
||||
post_id VARCHAR(100) NOT NULL, -- 文章 slug
|
||||
user_id INTEGER NOT NULL, -- 关联 users.id
|
||||
parent_id INTEGER, -- 父评论ID,用于嵌套回复
|
||||
content TEXT NOT NULL,
|
||||
status VARCHAR(20) DEFAULT 'approved', -- 'pending' | 'approved' | 'spam'
|
||||
|
||||
FOREIGN KEY (user_id) REFERENCES users(id),
|
||||
FOREIGN KEY (parent_id) REFERENCES comments(id)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_comments_post_id ON comments(post_id);
|
||||
CREATE INDEX idx_comments_user_id ON comments(user_id);
|
||||
CREATE INDEX idx_comments_parent_id ON comments(parent_id);
|
||||
CREATE INDEX idx_comments_deleted_at ON comments(deleted_at);
|
||||
```
|
||||
|
||||
### likes 表
|
||||
|
||||
```sql
|
||||
CREATE TABLE likes (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
created_at DATETIME,
|
||||
post_id VARCHAR(100) NOT NULL,
|
||||
user_id INTEGER, -- 登录用户ID,可为空
|
||||
ip_hash VARCHAR(64), -- 访客IP哈希
|
||||
|
||||
FOREIGN KEY (user_id) REFERENCES users(id)
|
||||
);
|
||||
|
||||
-- 防止同一用户重复点赞
|
||||
CREATE UNIQUE INDEX idx_post_user ON likes(post_id, user_id);
|
||||
-- 防止同一IP重复点赞
|
||||
CREATE UNIQUE INDEX idx_post_ip ON likes(post_id, ip_hash);
|
||||
CREATE INDEX idx_likes_user_id ON likes(user_id);
|
||||
```
|
||||
|
||||
### like_counts 表
|
||||
|
||||
```sql
|
||||
CREATE TABLE like_counts (
|
||||
post_id VARCHAR(100) PRIMARY KEY,
|
||||
count INTEGER DEFAULT 0
|
||||
);
|
||||
```
|
||||
|
||||
### post_meta 表(预留)
|
||||
|
||||
```sql
|
||||
CREATE TABLE post_meta (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
post_id VARCHAR(100) UNIQUE NOT NULL,
|
||||
view_count INTEGER DEFAULT 0,
|
||||
like_count INTEGER DEFAULT 0,
|
||||
created_at DATETIME,
|
||||
updated_at DATETIME
|
||||
);
|
||||
|
||||
CREATE INDEX idx_post_meta_post_id ON post_meta(post_id);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 前端组件开发
|
||||
|
||||
### 创建 Vue 组件
|
||||
|
||||
在 `src/components/` 下创建 `.vue` 文件:
|
||||
|
||||
```vue
|
||||
<!-- src/components/MyComponent.vue -->
|
||||
<template>
|
||||
<div class="my-component">
|
||||
<h2>{{ title }}</h2>
|
||||
<button @click="handleClick">{{ count }}</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
|
||||
// Props 定义
|
||||
const props = defineProps<{
|
||||
title: string;
|
||||
initialValue?: number;
|
||||
}>();
|
||||
|
||||
// 响应式状态
|
||||
const count = ref(props.initialValue || 0);
|
||||
|
||||
// 方法
|
||||
const handleClick = () => {
|
||||
count.value++;
|
||||
emit('change', count.value);
|
||||
};
|
||||
|
||||
// 事件定义
|
||||
const emit = defineEmits<{
|
||||
change: [value: number];
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.my-component {
|
||||
padding: 1rem;
|
||||
background: var(--color-muted);
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
</style>
|
||||
```
|
||||
|
||||
### 在 MDX 中使用
|
||||
|
||||
```mdx
|
||||
import MyComponent from '../components/MyComponent.vue';
|
||||
|
||||
<MyComponent
|
||||
title="我的组件"
|
||||
initialValue={10}
|
||||
client:visible
|
||||
/>
|
||||
```
|
||||
|
||||
### 创建 Astro 组件
|
||||
|
||||
Astro 组件用于静态渲染,无客户端交互:
|
||||
|
||||
```astro
|
||||
---
|
||||
// src/components/StaticCard.astro
|
||||
interface Props {
|
||||
title: string;
|
||||
content: string;
|
||||
}
|
||||
|
||||
const { title, content } = Astro.props;
|
||||
---
|
||||
|
||||
<div class="card">
|
||||
<h3>{title}</h3>
|
||||
<p>{content}</p>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.card {
|
||||
padding: 1.5rem;
|
||||
background: var(--color-background);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 0.75rem;
|
||||
}
|
||||
</style>
|
||||
```
|
||||
|
||||
### 内容集合扩展
|
||||
|
||||
在 `src/content/config.ts` 中定义新的内容类型:
|
||||
|
||||
```typescript
|
||||
import { defineCollection, z } from 'astro:content';
|
||||
|
||||
const docs = defineCollection({
|
||||
type: 'content',
|
||||
schema: z.object({
|
||||
title: z.string(),
|
||||
description: z.string().optional(),
|
||||
order: z.number().default(0),
|
||||
}),
|
||||
});
|
||||
|
||||
export const collections = {
|
||||
blog: blogCollection,
|
||||
docs, // 新增文档集合
|
||||
};
|
||||
```
|
||||
|
||||
### 自定义页面路由
|
||||
|
||||
在 `src/pages/` 下创建 `.astro` 文件:
|
||||
|
||||
```astro
|
||||
---
|
||||
// src/pages/about.astro
|
||||
import BaseLayout from '../layouts/BaseLayout.astro';
|
||||
---
|
||||
|
||||
<BaseLayout title="关于我">
|
||||
<section class="py-16">
|
||||
<h1 class="text-4xl font-bold mb-8">关于我</h1>
|
||||
<p>这是一个自定义页面。</p>
|
||||
</section>
|
||||
</BaseLayout>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 后端服务扩展
|
||||
|
||||
### 添加新的 API 路由
|
||||
|
||||
1. 创建处理器 (`server/internal/handlers/`):
|
||||
|
||||
```go
|
||||
// server/internal/handlers/post.go
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type PostHandler struct{}
|
||||
|
||||
func NewPostHandler() *PostHandler {
|
||||
return &PostHandler{}
|
||||
}
|
||||
|
||||
func (h *PostHandler) GetPosts(c *gin.Context) {
|
||||
// 业务逻辑
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"data": []string{},
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
2. 注册路由 (`server/cmd/server/main.go`):
|
||||
|
||||
```go
|
||||
postHandler := handlers.NewPostHandler()
|
||||
api.GET("/posts", postHandler.GetPosts)
|
||||
```
|
||||
|
||||
### 添加中间件
|
||||
|
||||
```go
|
||||
// server/internal/middleware/rateLimit.go
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"time"
|
||||
)
|
||||
|
||||
func RateLimit() gin.HandlerFunc {
|
||||
// 实现限流逻辑
|
||||
return func(c *gin.Context) {
|
||||
// 检查请求频率
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
使用:
|
||||
```go
|
||||
api.Use(middleware.RateLimit())
|
||||
```
|
||||
|
||||
### 添加数据模型
|
||||
|
||||
```go
|
||||
// server/internal/models/models.go
|
||||
type Tag struct {
|
||||
ID uint `json:"id" gorm:"primaryKey"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
Name string `json:"name" gorm:"size:50;uniqueIndex"`
|
||||
Posts []Post `json:"posts" gorm:"many2many:post_tags;"`
|
||||
}
|
||||
```
|
||||
|
||||
自动迁移会在启动时执行。
|
||||
|
||||
---
|
||||
|
||||
## 部署配置
|
||||
|
||||
### Docker 部署
|
||||
|
||||
使用 `docker-compose.yml` 一键部署:
|
||||
|
||||
```yaml
|
||||
version: '3.8'
|
||||
services:
|
||||
frontend:
|
||||
build: .
|
||||
ports:
|
||||
- "4321:80"
|
||||
depends_on:
|
||||
- api
|
||||
|
||||
api:
|
||||
build: ./server
|
||||
ports:
|
||||
- "8080:8080"
|
||||
volumes:
|
||||
- ./server/data:/app/data
|
||||
environment:
|
||||
- JWT_SECRET=your-secret-key
|
||||
- ADMIN_USERNAME=admin
|
||||
- ADMIN_PASSWORD=admin123
|
||||
```
|
||||
|
||||
启动:
|
||||
```bash
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
### 手动部署
|
||||
|
||||
#### 前端构建
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
产物在 `dist/` 目录,可部署到任何静态托管服务。
|
||||
|
||||
#### 后端构建
|
||||
|
||||
```bash
|
||||
cd server
|
||||
go build -o novablog-server cmd/server/main.go
|
||||
```
|
||||
|
||||
运行:
|
||||
```bash
|
||||
./novablog-server
|
||||
```
|
||||
|
||||
### Nginx 配置
|
||||
|
||||
```nginx
|
||||
server {
|
||||
listen 80;
|
||||
server_name example.com;
|
||||
|
||||
# 静态文件
|
||||
root /var/www/novablog/dist;
|
||||
index index.html;
|
||||
|
||||
location / {
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
|
||||
# API 代理
|
||||
location /api {
|
||||
proxy_pass http://localhost:8080;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection 'upgrade';
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_cache_bypass $http_upgrade;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 环境变量
|
||||
|
||||
| 变量名 | 说明 | 默认值 |
|
||||
|--------|------|--------|
|
||||
| `PORT` | 服务端口 | `8080` |
|
||||
| `JWT_SECRET` | JWT 密钥 | 随机生成 |
|
||||
| `ADMIN_USERNAME` | 管理员用户名 | `admin` |
|
||||
| `ADMIN_PASSWORD` | 管理员密码 | `admin123` |
|
||||
| `DB_PATH` | 数据库路径 | `./data/novablog.db` |
|
||||
|
||||
---
|
||||
|
||||
## 性能优化
|
||||
|
||||
### 前端优化
|
||||
|
||||
1. **图片优化**
|
||||
- 使用 WebP 格式
|
||||
- 添加 `loading="lazy"` 懒加载
|
||||
- 使用 `srcset` 响应式图片
|
||||
|
||||
2. **代码分割**
|
||||
- Astro 默认零 JS
|
||||
- 交互组件使用 `client:visible` 懒加载
|
||||
|
||||
3. **缓存策略**
|
||||
- 静态资源设置长期缓存
|
||||
- HTML 设置短期缓存或 ETag
|
||||
|
||||
### 后端优化
|
||||
|
||||
1. **数据库索引**
|
||||
- 已在关键字段建立索引
|
||||
- 复杂查询使用 EXPLAIN 分析
|
||||
|
||||
2. **连接池**
|
||||
- SQLite 使用 WAL 模式
|
||||
- 设置合理的连接池大小
|
||||
|
||||
3. **响应压缩**
|
||||
- 启用 Gzip 压缩
|
||||
- API 响应体积减少 70%+
|
||||
|
||||
### 监控指标
|
||||
|
||||
```bash
|
||||
# 查看 Go 服务内存占用
|
||||
ps aux | grep novablog-server
|
||||
|
||||
# 查看数据库大小
|
||||
ls -lh server/data/novablog.db
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 附录
|
||||
|
||||
### 常用命令
|
||||
|
||||
```bash
|
||||
# 开发
|
||||
npm run dev # 启动前端开发服务器
|
||||
cd server && go run . # 启动后端服务
|
||||
|
||||
# 构建
|
||||
npm run build # 构建前端
|
||||
cd server && go build . # 构建后端
|
||||
|
||||
# 数据库
|
||||
sqlite3 server/data/novablog.db # 打开数据库 CLI
|
||||
|
||||
# Docker
|
||||
docker-compose up -d # 启动所有服务
|
||||
docker-compose logs -f # 查看日志
|
||||
docker-compose down # 停止服务
|
||||
```
|
||||
|
||||
### 技术参考
|
||||
|
||||
- [Astro 文档](https://docs.astro.build)
|
||||
- [Vue 3 文档](https://vuejs.org)
|
||||
- [Tailwind CSS 文档](https://tailwindcss.com)
|
||||
- [Gin 框架文档](https://gin-gonic.com)
|
||||
- [GORM 文档](https://gorm.io)
|
||||
- [Typst 文档](https://typst.app/docs)
|
||||
|
||||
---
|
||||
|
||||
如有问题或建议,欢迎提交 Issue 或 Pull Request!
|
||||
610
docs/user-guide.md
Normal file
610
docs/user-guide.md
Normal file
@@ -0,0 +1,610 @@
|
||||
# NovaBlog 使用指南
|
||||
|
||||
欢迎使用 NovaBlog!这是一款面向程序员和极客的轻量级混合架构博客系统。本指南将帮助您快速上手使用 NovaBlog 的各项功能。
|
||||
|
||||
---
|
||||
|
||||
## 目录
|
||||
|
||||
1. [快速开始](#快速开始)
|
||||
2. [文章管理](#文章管理)
|
||||
3. [MDX 组件使用](#mdx-组件使用)
|
||||
4. [Typst 学术排版](#typst-学术排版)
|
||||
5. [动效 HTML 块](#动效-html-块)
|
||||
6. [评论系统](#评论系统)
|
||||
7. [用户注册与登录](#用户注册与登录)
|
||||
8. [主题定制](#主题定制)
|
||||
9. [附件管理](#附件管理)
|
||||
10. [常见问题](#常见问题)
|
||||
|
||||
---
|
||||
|
||||
## 快速开始
|
||||
|
||||
### 环境要求
|
||||
|
||||
- Node.js 18+
|
||||
- Go 1.21+ (仅后端开发需要)
|
||||
- Typst 0.11+ (可选,用于数学公式渲染)
|
||||
|
||||
### 启动开发服务器
|
||||
|
||||
```bash
|
||||
# 启动前端开发服务器
|
||||
npm run dev
|
||||
|
||||
# 启动后端 API 服务器
|
||||
cd server && go run cmd/server/main.go
|
||||
```
|
||||
|
||||
访问 `http://localhost:4321` 即可预览博客。
|
||||
|
||||
---
|
||||
|
||||
## 文章管理
|
||||
|
||||
### 创建新文章
|
||||
|
||||
所有文章存放在 `src/content/blog/` 目录下,支持 `.md` 和 `.mdx` 格式。
|
||||
|
||||
#### 文章命名规范
|
||||
|
||||
推荐使用以下命名格式:
|
||||
- `my-first-post.md` - 英文,小写,连字符分隔
|
||||
- `使用指南.md` - 中文也可以,但 URL 会进行编码
|
||||
|
||||
#### Frontmatter 字段说明
|
||||
|
||||
每篇文章需要在开头添加 Frontmatter 元数据:
|
||||
|
||||
```yaml
|
||||
---
|
||||
title: 文章标题
|
||||
description: 文章描述,用于 SEO 和社交分享
|
||||
pubDate: 2024-01-15
|
||||
updatedDate: 2024-01-20 # 可选,更新日期
|
||||
heroImage: /images/cover.jpg # 可选,封面图
|
||||
heroAlt: 封面图描述 # 可选
|
||||
author: 作者名 # 可选,默认为站点名称
|
||||
tags: # 可选,标签列表
|
||||
- JavaScript
|
||||
- 教程
|
||||
category: 技术 # 可选,分类
|
||||
---
|
||||
|
||||
文章内容从这里开始...
|
||||
```
|
||||
|
||||
#### 示例文章
|
||||
|
||||
```markdown
|
||||
---
|
||||
title: 我的第一篇博客
|
||||
description: 这是我使用 NovaBlog 发布的第一篇文章
|
||||
pubDate: 2024-01-15
|
||||
heroImage: /images/hello-world.jpg
|
||||
tags:
|
||||
- 随笔
|
||||
- 博客
|
||||
category: 生活
|
||||
---
|
||||
|
||||
## 欢迎来到我的博客
|
||||
|
||||
这是正文内容。支持标准 Markdown 语法。
|
||||
|
||||
### 列表
|
||||
|
||||
- 项目 1
|
||||
- 项目 2
|
||||
- 项目 3
|
||||
|
||||
### 代码块
|
||||
|
||||
```javascript
|
||||
console.log('Hello, NovaBlog!');
|
||||
```
|
||||
|
||||
### 引用
|
||||
|
||||
> 这是一段引用文字。
|
||||
```
|
||||
|
||||
### 文章状态
|
||||
|
||||
目前文章发布即公开,后续版本将支持:
|
||||
- `draft: true` - 草稿状态
|
||||
- `published: false` - 隐藏文章
|
||||
|
||||
### 删除文章
|
||||
|
||||
直接删除 `src/content/blog/` 目录下的对应文件即可。
|
||||
|
||||
---
|
||||
|
||||
## MDX 组件使用
|
||||
|
||||
NovaBlog 原生支持 MDX,允许在 Markdown 中嵌入交互式组件。
|
||||
|
||||
### 什么是 MDX?
|
||||
|
||||
MDX = Markdown + JSX。它让您可以在 Markdown 中直接使用 React/Vue 组件。
|
||||
|
||||
### 内置组件
|
||||
|
||||
#### Counter 计数器组件
|
||||
|
||||
```mdx
|
||||
import Counter from '../components/Counter.vue';
|
||||
|
||||
<Counter initialCount={0} />
|
||||
```
|
||||
|
||||
#### TypstBlock 数学公式
|
||||
|
||||
```mdx
|
||||
import TypstBlock from '../components/TypstBlock.astro';
|
||||
|
||||
<TypstBlock code={`
|
||||
$ f(x) = x^2 + 2x + 1 $
|
||||
`} />
|
||||
```
|
||||
|
||||
### 自定义组件
|
||||
|
||||
您可以在 `src/components/` 目录下创建自己的组件:
|
||||
|
||||
```vue
|
||||
<!-- src/components/MyButton.vue -->
|
||||
<template>
|
||||
<button class="my-btn" @click="handleClick">
|
||||
<slot />
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const handleClick = () => {
|
||||
console.log('Button clicked!');
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.my-btn {
|
||||
padding: 0.5rem 1rem;
|
||||
background: linear-gradient(135deg, #3b82f6, #2563eb);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 0.5rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
```
|
||||
|
||||
然后在文章中使用:
|
||||
|
||||
```mdx
|
||||
import MyButton from '../components/MyButton.vue';
|
||||
|
||||
<MyButton>点击我</MyButton>
|
||||
```
|
||||
|
||||
### 组件交互性
|
||||
|
||||
使用 `client:*` 指令控制组件的客户端行为:
|
||||
|
||||
| 指令 | 说明 |
|
||||
|------|------|
|
||||
| `client:load` | 页面加载时立即激活 |
|
||||
| `client:visible` | 组件进入视口时激活 |
|
||||
| `client:idle` | 浏览器空闲时激活 |
|
||||
| `client:media` | 满足媒体查询时激活 |
|
||||
|
||||
示例:
|
||||
```mdx
|
||||
<MyButton client:visible>进入视口时激活</MyButton>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Typst 学术排版
|
||||
|
||||
NovaBlog 内置 Typst 支持,可以渲染高质量的数学公式和学术排版。
|
||||
|
||||
### 什么是 Typst?
|
||||
|
||||
Typst 是新一代排版系统,具有:
|
||||
- 比 LaTeX 更简洁的语法
|
||||
- 更快的编译速度
|
||||
- 原生支持数学公式
|
||||
|
||||
### 基本用法
|
||||
|
||||
```mdx
|
||||
import TypstBlock from '../components/TypstBlock.astro';
|
||||
|
||||
<TypstBlock code={`
|
||||
$ f(x) = integral_0^infinity e^(-x^2) dif x = sqrt(pi) / 2 $
|
||||
`} />
|
||||
```
|
||||
|
||||
### 数学公式示例
|
||||
|
||||
#### 积分
|
||||
|
||||
```typst
|
||||
$ integral_0^1 x^2 dif x = 1/3 $
|
||||
```
|
||||
|
||||
#### 求和
|
||||
|
||||
```typst
|
||||
$ sum_(i=1)^n i = (n(n+1))/2 $
|
||||
```
|
||||
|
||||
#### 矩阵
|
||||
|
||||
```typst
|
||||
$ A = mat(
|
||||
1, 2, 3;
|
||||
4, 5, 6;
|
||||
7, 8, 9
|
||||
) $
|
||||
```
|
||||
|
||||
#### 分数
|
||||
|
||||
```typst
|
||||
$ (a + b) / (c + d) $
|
||||
```
|
||||
|
||||
#### 上下标
|
||||
|
||||
```typst
|
||||
$ x_1^2 + x_2^2 = r^2 $
|
||||
```
|
||||
|
||||
### 高级排版
|
||||
|
||||
Typst 还支持:
|
||||
- 多行公式对齐
|
||||
- 定理环境
|
||||
- 化学方程式
|
||||
- 代码高亮
|
||||
|
||||
更多语法请参考 [Typst 官方文档](https://typst.app/docs)。
|
||||
|
||||
---
|
||||
|
||||
## 动效 HTML 块
|
||||
|
||||
在 MDX 中可以直接使用 HTML 标签和内联样式,创建带动画的内容块。
|
||||
|
||||
### 示例:渐变背景卡片
|
||||
|
||||
```mdx
|
||||
<div style={{
|
||||
padding: '2rem',
|
||||
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
||||
borderRadius: '1rem',
|
||||
color: 'white',
|
||||
textAlign: 'center'
|
||||
}}>
|
||||
<h3 style={{ marginBottom: '0.5rem' }}>✨ 特色卡片</h3>
|
||||
<p>这是一个带渐变背景的卡片</p>
|
||||
</div>
|
||||
```
|
||||
|
||||
### 示例:CSS 动画
|
||||
|
||||
```mdx
|
||||
<style>
|
||||
.pulse-box {
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0% { transform: scale(1); opacity: 1; }
|
||||
50% { transform: scale(1.05); opacity: 0.8; }
|
||||
100% { transform: scale(1); opacity: 1; }
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="pulse-box" style={{
|
||||
padding: '1.5rem',
|
||||
background: '#3b82f6',
|
||||
borderRadius: '0.5rem',
|
||||
color: 'white',
|
||||
textAlign: 'center'
|
||||
}}>
|
||||
🔔 脉冲动画效果
|
||||
</div>
|
||||
```
|
||||
|
||||
### 示例:悬停效果
|
||||
|
||||
```mdx
|
||||
<style>
|
||||
.hover-card {
|
||||
transition: transform 0.3s ease, box-shadow 0.3s ease;
|
||||
}
|
||||
.hover-card:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="hover-card" style={{
|
||||
padding: '1.5rem',
|
||||
background: 'white',
|
||||
borderRadius: '0.5rem',
|
||||
boxShadow: '0 2px 10px rgba(0, 0, 0, 0.1)',
|
||||
cursor: 'pointer'
|
||||
}}>
|
||||
🖱️ 鼠标悬停试试
|
||||
</div>
|
||||
```
|
||||
|
||||
### 示例:交互式组件
|
||||
|
||||
结合 Vue 组件创建交互式内容:
|
||||
|
||||
```vue
|
||||
<!-- src/components/InteractiveCard.vue -->
|
||||
<template>
|
||||
<div
|
||||
class="card"
|
||||
@click="toggle"
|
||||
:style="{ background: isActive ? '#3b82f6' : '#e5e7eb' }"
|
||||
>
|
||||
<h3>{{ title }}</h3>
|
||||
<p>{{ isActive ? '已激活 ✓' : '点击激活' }}</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
title: String
|
||||
});
|
||||
|
||||
const isActive = ref(false);
|
||||
const toggle = () => isActive.value = !isActive.value;
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.card {
|
||||
padding: 1.5rem;
|
||||
border-radius: 0.5rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
color: white;
|
||||
}
|
||||
</style>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 评论系统
|
||||
|
||||
NovaBlog 内置评论系统,支持多级嵌套回复和 Markdown 语法。
|
||||
|
||||
### 发表评论
|
||||
|
||||
1. 在文章页面滚动到评论区
|
||||
2. 点击登录按钮进行用户认证
|
||||
3. 输入评论内容(支持 Markdown)
|
||||
4. 点击发送
|
||||
|
||||
### 回复评论
|
||||
|
||||
1. 点击评论下方的"回复"按钮
|
||||
2. 输入回复内容
|
||||
3. 提交后将显示在原评论下方
|
||||
|
||||
### Markdown 支持
|
||||
|
||||
评论区支持基础 Markdown 语法:
|
||||
|
||||
| 语法 | 效果 |
|
||||
|------|------|
|
||||
| `**粗体**` | **粗体** |
|
||||
| `*斜体*` | *斜体* |
|
||||
| `` `代码` `` | `代码` |
|
||||
| `[链接](url)` | [链接](url) |
|
||||
| `> 引用` | 引用块 |
|
||||
|
||||
### 删除评论
|
||||
|
||||
用户可以删除自己发表的评论。管理员可以删除任何评论。
|
||||
|
||||
---
|
||||
|
||||
## 用户注册与登录
|
||||
|
||||
### 注册账号
|
||||
|
||||
1. 点击页面右上角的"登录"按钮
|
||||
2. 在登录弹窗中点击"注册"
|
||||
3. 填写用户名、邮箱和密码
|
||||
4. 提交注册
|
||||
|
||||
### 登录账号
|
||||
|
||||
支持两种登录方式:
|
||||
- 用户名 + 密码
|
||||
- 邮箱 + 密码
|
||||
|
||||
### 用户角色
|
||||
|
||||
| 角色 | 权限 |
|
||||
|------|------|
|
||||
| `user` | 发表评论、删除自己的评论 |
|
||||
| `admin` | 所有权限 + 删除任意评论 |
|
||||
|
||||
### 修改个人资料
|
||||
|
||||
登录后可以修改:
|
||||
- 昵称
|
||||
- 头像 URL
|
||||
- 个人简介
|
||||
|
||||
---
|
||||
|
||||
## 主题定制
|
||||
|
||||
### CSS 变量
|
||||
|
||||
NovaBlog 使用 CSS 变量实现主题系统,可在 `src/styles/global.css` 中修改:
|
||||
|
||||
```css
|
||||
:root {
|
||||
/* 主色调 */
|
||||
--color-primary-50: #eff6ff;
|
||||
--color-primary-100: #dbeafe;
|
||||
--color-primary-500: #3b82f6;
|
||||
--color-primary-600: #2563eb;
|
||||
|
||||
/* 背景色 */
|
||||
--color-background: #ffffff;
|
||||
--color-muted: #f3f4f6;
|
||||
|
||||
/* 文字色 */
|
||||
--color-foreground: #1f2937;
|
||||
--color-muted-foreground: #6b7280;
|
||||
|
||||
/* 边框色 */
|
||||
--color-border: #e5e7eb;
|
||||
}
|
||||
```
|
||||
|
||||
### 暗色主题
|
||||
|
||||
修改 CSS 变量实现暗色模式:
|
||||
|
||||
```css
|
||||
.dark {
|
||||
--color-background: #1a1a2e;
|
||||
--color-foreground: #eaeaea;
|
||||
--color-muted: #16213e;
|
||||
--color-border: #0f3460;
|
||||
}
|
||||
```
|
||||
|
||||
### Tailwind 配置
|
||||
|
||||
在 `tailwind.config.mjs` 中自定义主题:
|
||||
|
||||
```javascript
|
||||
export default {
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
primary: {
|
||||
50: '#eff6ff',
|
||||
// ... 更多颜色
|
||||
}
|
||||
},
|
||||
fontFamily: {
|
||||
sans: ['Inter', 'sans-serif'],
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 布局组件
|
||||
|
||||
主要布局文件:
|
||||
- `src/layouts/BaseLayout.astro` - 基础布局(Header/Footer)
|
||||
- `src/layouts/PostLayout.astro` - 文章布局(目录/评论)
|
||||
|
||||
---
|
||||
|
||||
## 附件管理
|
||||
|
||||
### 图片存放位置
|
||||
|
||||
将图片放在 `public/images/` 目录下:
|
||||
|
||||
```
|
||||
public/
|
||||
├── images/
|
||||
│ ├── posts/
|
||||
│ │ ├── hello-world.jpg
|
||||
│ │ └── tutorial-cover.png
|
||||
│ └── avatars/
|
||||
│ └── default.png
|
||||
```
|
||||
|
||||
### 在文章中引用
|
||||
|
||||
```markdown
|
||||

|
||||
```
|
||||
|
||||
### 在 Frontmatter 中使用封面图
|
||||
|
||||
```yaml
|
||||
---
|
||||
heroImage: /images/posts/my-cover.jpg
|
||||
heroAlt: 这是一张封面图
|
||||
---
|
||||
```
|
||||
|
||||
### 其他附件
|
||||
|
||||
PDF、ZIP 等文件也放在 `public/` 目录:
|
||||
|
||||
```
|
||||
public/
|
||||
├── downloads/
|
||||
│ └── source-code.zip
|
||||
└── documents/
|
||||
└── paper.pdf
|
||||
```
|
||||
|
||||
引用方式:
|
||||
```markdown
|
||||
[下载源码](/downloads/source-code.zip)
|
||||
[查看论文](/documents/paper.pdf)
|
||||
```
|
||||
|
||||
### 图片优化建议
|
||||
|
||||
- 使用 WebP 格式减少文件大小
|
||||
- 封面图推荐尺寸:1920x1080
|
||||
- 文章内图片推荐宽度:800px
|
||||
- 压缩工具:[Squoosh](https://squoosh.app/)
|
||||
|
||||
---
|
||||
|
||||
## 常见问题
|
||||
|
||||
### Q: 文章修改后没有更新?
|
||||
|
||||
A: 开发模式下 Astro 会自动热重载。如果生产构建,需要重新运行 `npm run build`。
|
||||
|
||||
### Q: Typst 公式渲染失败?
|
||||
|
||||
A: 确保 Typst 已正确安装。运行 `typst --version` 检查。
|
||||
|
||||
### Q: 评论无法发送?
|
||||
|
||||
A: 检查后端服务是否正常运行在 `localhost:8080`。
|
||||
|
||||
### Q: 如何修改站点信息?
|
||||
|
||||
A: 编辑 `src/layouts/BaseLayout.astro` 和 `astro.config.mjs` 中的站点配置。
|
||||
|
||||
### Q: 如何添加 Google Analytics?
|
||||
|
||||
A: 在 `src/layouts/BaseLayout.astro` 的 `<head>` 中添加 GA 脚本。
|
||||
|
||||
---
|
||||
|
||||
## 获取帮助
|
||||
|
||||
- 📖 查看 [开发文档](./developer-guide.md) 了解技术细节
|
||||
- 🐛 提交 Issue 反馈问题
|
||||
- 💬 在 GitHub Discussions 中讨论
|
||||
@@ -153,9 +153,9 @@ const tocItems = headings.filter(h => h.depth >= 2 && h.depth <= 3);
|
||||
|
||||
<!-- 文章内容区域 -->
|
||||
<div class="content-width">
|
||||
<div class="flex gap-8 relative">
|
||||
<div class="flex gap-8 relative justify-center">
|
||||
<!-- 文章主体 -->
|
||||
<div class={`flex-1 min-w-0 ${hasToc ? 'max-w-3xl' : 'max-w-4xl mx-auto'}`}>
|
||||
<div class="flex-1 min-w-0 max-w-3xl">
|
||||
<div class="prose-container prose-headings:font-semibold prose-headings:tracking-tight prose-h1:text-3xl prose-h2:text-2xl prose-h3:text-xl prose-p:leading-relaxed prose-a:text-primary-500 prose-a:no-underline hover:prose-a:underline prose-img:rounded-xl prose-code:text-primary-400">
|
||||
<slot />
|
||||
</div>
|
||||
@@ -163,7 +163,7 @@ const tocItems = headings.filter(h => h.depth >= 2 && h.depth <= 3);
|
||||
|
||||
<!-- 桌面端右侧目录 -->
|
||||
{hasToc && (
|
||||
<aside class="hidden xl:block w-48 flex-shrink-0">
|
||||
<aside class="hidden xl:block w-56 flex-shrink-0">
|
||||
<nav class="sticky top-24" aria-label="文章目录">
|
||||
<div class="text-sm font-semibold text-foreground mb-3">目录</div>
|
||||
<ul class="toc-list-desktop">
|
||||
|
||||
Reference in New Issue
Block a user