个人主页 + 服务导航 + 状态监控 + 博客,前后端一体化部署。
| 层 | 技术 |
|---|---|
| 后端 | Go + Gin + GORM/SQLite + go-redis + JWT + zap |
| 前端 | Next.js 16 + React 19 + shadcn/ui + TailwindCSS 4 + shiki |
| 部署 | Go embed 嵌入前端静态产物,单一二进制 |
前端含 PWA(manifest.json + public/sw.js):核心页可离线预缓存(含 /blog/archive/);用户导航(mode === 'navigate')为网络优先,失败再读预缓存或 503 离线提示,在线始终易拿到新 HTML;子资源仍为缓存优先且非主文档可写入运行时缓存;仅同源 GET;/api、/uploads、*.xml、/admin、/login、/external 不拦截。嵌入部署时 _next/static/ 为 1 年 immutable,icons/ 为 7 天 缓存。
- Go 1.21+
- Node.js 18+ / Bun 1.0+
- (可选)Redis
# 后端
go run ./cmd/main.go
# 前端(另一个终端)
cd frontend && bun install && bun run devmake build
# 生成 my-blog 二进制,运行:
./my-blogWindows 用户可使用 PowerShell:.\build.ps1 -Target build,运行 .\my-blog.exe。
编辑 config.yaml:
server:
port: 8080
mode: debug # debug / release
trusted_proxies: [] # 可信代理列表(CIDR/IP)
trusted_platform: "" # 可信平台(如 Cloudflare/Fly)
cors:
allow_origins: ["*"]
allow_methods: ["GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH"]
allow_headers: ["Origin", "Content-Type", "Authorization", "Accept"]
expose_headers: ["Content-Length"]
allow_credentials: false
max_age_seconds: 86400
database:
path: ./data/blog.db
redis:
addr: "" # 留空使用内存缓存
jwt:
secret: "" # 必须修改
admin:
username: "" # 必须修改
password: "" # 必须修改
email: "" # 必须修改
upload:
path: ./data/uploads
max_size_mb: 10
allowed_extensions: [".jpg", ".jpeg", ".png", ".gif", ".webp", ".svg", ".ico", ".bmp", ".pdf", ".zip", ".md", ".txt"]
site:
name: "My Blog"
description: "一个个人博客站点"
url: "" # 必须修改
# 可选:正文最大长度(Unicode 码点),0 为不限制;嵌入 Base64 图片时请酌情调大或改用上传
blog:
max_content_runes: 0站点配置(site-config)扩展键(示例):nav_page_intro(导航页说明)、home_background_mode(none / fixed_url / random_pool)、home_background_url、home_background_pool(每行一图 URL),在管理后台「站点设置」中填写。
安全提醒
- 生产环境必须修改
jwt.secret与admin.password - 若使用反向代理/CDN,请配置
trusted_proxies或trusted_platform site.url影响 RSS/Atom/Sitemap 链接生成- 默认响应含基础安全头(
X-Content-Type-Options、X-Frame-Options、Referrer-Policy等);HSTS 建议在网关/反代上开启 HTTPS 后配置 - 登录接口与
/api/v1/admin/*、POST /api/v1/comments响应带Cache-Control: no-store,降低共享缓存误存含 Token/管理数据的风险
# 构建并启动
docker compose up -d --build
# 访问
http://localhost:8080容器 healthcheck 默认探测 GET /live(轻量、不访问数据库)。编排 readiness 可选用 GET /ready(SQLite + 缓存 Ping;启用 Redis 时包含 Redis,失败返回 503)。
| 路径 | 说明 |
|---|---|
GET /live |
仅确认 HTTP 服务响应,适合 liveness |
GET /ready |
Ping SQLite + 缓存;配置 Redis 时会探测 Redis,失败返回 503 |
My-Blog/
├── cmd/main.go # 入口
├── internal/
│ ├── app/ # DI 容器
│ ├── model/ # 数据模型(UUID 主键)
│ ├── repository/ # 数据访问层(接口 + 实现)
│ ├── cache/ # 缓存层(Redis/Memory 双实现)
│ ├── service/ # 业务逻辑
│ │ ├── checker/ # 监控检测器(HTTP/TCP/Ping/DNS/SSL/Push)
│ │ ├── notifier/ # 通知发送器(Webhook/Email/Telegram)
│ │ └── scheduler/ # 监控任务调度器
│ ├── handler/ # HTTP 处理器
│ ├── middleware/ # 中间件(JWT/CORS/频率限制)
│ ├── router/ # 路由注册
│ └── static/ # 前端静态产物嵌入
├── frontend/ # Next.js 前端
│ ├── app/ # 页面层
│ ├── services/ # API 调用层
│ ├── stores/ # 状态管理层
│ ├── hooks/ # 交互逻辑层
│ ├── components/ # UI 组件层
│ └── lib/ # 工具层
├── config.yaml # 配置文件
└── Makefile # 构建脚本
- 个人主页 — 头像/简介/社交链接/一言/时间
- 博客 — Markdown 全量渲染(shiki 代码高亮 + KaTeX 公式 + GFM)+ 渐进式渲染防卡死 + 密码保护 + 评论(嵌套回复;后端校验文章状态、父评论归属与最大 3 层嵌套);列表/归档/上下篇等站内链接对 slug 使用
encodeURIComponent,与 RSS/Sitemap 的 PathEscape 语义一致;公开列表搜索可选 同时搜索正文(search_in_content,有关闭时仅标题/摘要类字段,减轻大库扫描)。 - 服务导航 — 分类卡片网格 + 搜索
- 状态监控 — HTTP/TCP/Ping/DNS/关键字/SSL/Push 7种监控 + 可用性统计 + 告警通知
- 后台管理 — 文章/评论/导航/监控/站点设置全 CRUD
所有接口以 /api/v1/ 为前缀。管理接口需 JWT 认证(/api/v1/admin/)。
前端 frontend/services/* 对 URL 路径参数(slug、UUID、site-config key 等)统一使用 encodeApiPathSegment(encodeURIComponent),避免特殊字符截断路由;Gin 会按标准解码路径段。
frontend/lib/api-client 的 api.get 查询参数 支持 boolean,会序列化为 true/false 字符串,与后端 ParseBoolQuery(如 search_in_content、include_config、include_stats)一致。
Push 心跳:POST /api/v1/push/:id 带约 300 次/分钟/IP 限流(正常 Push 监控上报频率远低于此);需正确 Authorization: Bearer <token>。
OpenAPI 3.0 描述见仓库根目录 docs/openapi.yaml,可用 Swagger UI / Redoc 等工具浏览;文档内使用 components/responses(通用错误体)与 components/parameters(分页)做复用。健康检查:GET /live(存活)、GET /ready(就绪,含 DB + 缓存)在站点根路径,不在 /api/v1 下。
文章 Slug:不可使用保留字 archive,否则与公开接口 GET /api/v1/posts/archive(归档列表)路由冲突,详情页与 getPostBySlug('archive') 会无法正确取文。
站点配置键(site-config):仅允许 ASCII 字母、数字、_、-、.,长度 ≤128,且不得包含 ..;单条配置值长度 ≤512KiB,避免异常键污染缓存键或超大写入。公开 GET /api/v1/site-config 会额外附带只读派生项 upload_max_size_mb(上传单文件上限 MB,来自 config.yaml 的 upload.max_size_mb,≤0 时按 10),不可通过管理接口写入或删除。
文件上传:upload.allowed_extensions 须至少包含一个有效扩展名,否则进程启动失败;管理端 POST /api/v1/admin/upload 带约 20 次/分钟/IP 限流(与 max_size_mb、白名单一并约束)。max_size_mb ≤ 0 时按 10MB 处理,且 Gin 的 MaxMultipartMemory 与此对齐,避免大请求体占用过多内存后才被拒。
/feed.xml或/rss.xml/atom.xml/sitemap.xml
RSS/Atom/Sitemap 中文章链接对 slug 使用 PathEscape 编码,避免空格、Unicode 或特殊字符导致 URL 非法。
MIT