基于 React + TypeScript + Vite + Express + PostgreSQL 构建的现代博客系统,支持自托管 (Self-Hosted) 部署。
npm install
npm run devnpm run build
npm run db:init # 初始化数据库表结构
npm run serve # 启动生产服务器MyBlog/
├── server.ts # Express 后端服务器入口
├── dist/ # 前端构建产物 (自动生成)
├── dist-server/ # 服务端构建产物 (自动生成)
├── scripts/
│ ├── init-db.ts # 数据库初始化脚本
│ └── process-photos.cjs # 照片墙历史/手动回填脚本(非主流程)
├── src/
│ ├── assets/ # 前端静态资源
│ ├── db/ # 数据库连接模块 (PostgreSQL)
│ ├── middleware/ # 服务端中间件
│ ├── routes/ # 服务端 API 路由
│ ├── components/ # React 组件
│ │ ├── admin/ # 后台管理组件
│ │ ├── BlogPost/ # 文章详情页组件集
│ │ ├── PhotoWall/ # 照片墙组件集
│ │ └── ...
│ ├── content/ # Markdown 文章
│ │ ├── tech/ # 技术类文章
│ │ └── life/ # 生活类文章
│ ├── data/
│ │ ├── posts.ts # 文章数据加载与解析
│ │ ├── images-metadata.example.json # 照片墙元数据示例
│ │ └── images-metadata.json # 本地/线上运行时元数据(不入库)
│ ├── hooks/ # 自定义 Hooks
│ ├── pages/ # 页面级组件
│ │ ├── admin/ # 后台页面
│ │ └── ...
│ ├── utils/ # 工具函数模块
│ ├── services/ # 服务模块(含图片处理工具)
│ ├── types/ # TypeScript 类型定义
│ ├── App.tsx # 主应用入口
│ ├── main.tsx # 前端入口
│ └── polyfill.ts # 运行时补丁
├── public/
│ ├── images/ # 文章图片资源
│ ├── resources/ # 网站资源(背景/头像等)
│ ├── photowall/ # 照片墙资源
│ │ ├── origin/ # 原始照片(HEIC/JPG/PNG)
│ │ └── thumbnails/ # 缩略图集合
│ │ ├── medium/ # 中等缩略图
│ │ └── tiny/ # 模糊预览
│ └── avatar/ # 默认头像
├── index.html
├── package.json
├── vite.config.ts
├── tsconfig.json
├── tsconfig.app.json
├── tsconfig.node.json
├── tsconfig.server.json # 服务端 TypeScript 配置
└── .env.example # 环境变量模板
首页支持通过 Tab 切换显示不同类型的内容:
| Tab | 说明 |
|---|---|
| 技术笔记 | 默认显示,包含技术相关文章 |
| 生活随笔 | 生活类内容,如随笔、摄影、旅行等 |
在 Markdown 文件的 Frontmatter 中通过 type 字段设置:'tech' 或 'life',不填默认为 'tech'。
通过导航栏的「时间线」按钮进入,按年份展示所有文章,支持滚动同步高亮当前年月。
文章底部内置匿名评论功能,用户只需填写昵称即可评论。评论数据存储于 PostgreSQL 数据库。
右上角导航栏提供以下功能:
- 首页 - 返回主页
- 分类 - 按分类筛选文章(笔记、项目记录等)
- 搜索 - 关键词搜索文章
- 时间线 - 按时间展示所有文章
- 相册 - 照片墙页面
- 友链 - 友链页面
- 关于 - 个人介绍页面
文章详情页右侧(桌面端)或抽屉菜单(移动端)显示自动生成的目录,支持:
- 自动提取 Markdown 标题(H1-H2)
- 点击平滑滚动到对应章节
- 阅读进度高亮
- 回到顶部按钮
独立的友链页面,支持:
- 卡片式布局展示友链
- 亮/暗主题自适应
- 悬停动画效果
访问 /admin 进入后台管理界面 (需鉴权),支持:
- 文章管理 - 在线增删改查文章,支持 Markdown 实时预览
- 相册管理 - 上传原图到 OSS、自动生成缩略图、管理展示状态
- 安全登录 - 基于 JWT + bcrypt 的认证系统
| 功能 | 说明 |
|---|---|
| 年内分页 | 每年默认显示 4 篇文章,点击「更多文章」按钮加载更多 |
| 年份分段 | 默认显示最近 2 年,点击「加载更多年份」展开历史内容 |
| 优化项 | 实现方式 | 效果 |
|---|---|---|
| 懒加载 | 原生 loading="lazy" |
首屏加载提速 30-50% |
| 渐进式加载 | ProgressiveImage 组件 |
Shimmer 动画 + 淡入效果 |
| 骨架屏 | Skeleton 组件 |
消除内容布局偏移,提升感知速度 |
| 事件防抖 | useDebounce Hook |
优化 Resize 等高频事件性能 |
- 代码高亮 - 使用
highlight.js语法高亮 - Markdown 渲染 - 支持 GFM 扩展语法
- 动画效果 - 使用
framer-motion流畅动画
- 在
src/content/tech/或src/content/life/目录下创建.md文件 - 文件头添加 Frontmatter 元数据:
---
slug: my-post-id
title: 文章标题
date: 2026-01-29
year: 2026
type: tech # tech | life
description: 简介...
coverImage: /images/xxx/cover.png
categories: 技术笔记
tags:
- React
- Node.js
---posts.ts会自动扫描并加载所有.md文件,无需手动注册
| 字段 | 用途 | 推荐比例 |
|---|---|---|
coverImage |
首页卡片封面 | 1:1 |
headerImage |
文章详情头图 | 16:9 |
# 进入后台
# /admin/photos
# 选择图片后上传,服务端会自动写入 OSS 并维护 metadata特性: HEIC/HEIF/JPG/PNG 原图保留 | 统一生成 medium/tiny 缩略图 | EXIF 提取 | 瀑布流布局 | 灯箱预览
当前主分支已切换为后台直传 OSS 模式,不再启用 OneDrive 自动同步链路。
当前主流程为:管理员在 /admin/photos 上传/删除照片,服务端在 src/routes/photos.ts 中同步维护 OSS 对象与 src/data/images-metadata.json。
数据流:
Admin (/admin/photos) 上传图片
│
▼
POST /api/photos/upload
- 保存原图到 OSS (origin)
- 统一生成 JPEG 的 medium/tiny 缩略图
- 更新运行时 `src/data/images-metadata.json`
│
▼
Gallery / Admin 读取 /api/photos/metadata
删除流程:
Admin (/admin/photos) 删除图片
│
▼
DELETE /api/photos/:filename
- 删除 OSS 原图与缩略图对象
- 更新 src/data/images-metadata.json
上传限制与建议:
- 管理端会自动分批上传:默认每批最多
10张、每批总大小最多50MB。 - 后端当前单文件大小限制为
50MB,且默认每个上传请求最多接收10个文件。 10张/批限制已改为前后端可配置(默认值保持一致),以避免规则漂移。- 为了更稳定的上传体验,建议单张图片尽量控制在
5MB以内。
OSS 对象路径约定:
- 原图:
photowall/origin/<filename> - 全尺寸缩略图:
photowall/thumbnails/full/<filename>.jpg - 中图缩略图:
photowall/thumbnails/medium/<filename>.jpg - 小图缩略图:
photowall/thumbnails/tiny/<filename>.jpg
说明:
<filename>为上传后的安全文件名(保留原扩展名)。- 原图格式保持不变(如 HEIC/HEIF/JPG/PNG)。
- 缩略图统一为 JPEG,命名方式为“保留完整原文件名,再额外追加
.jpg”,例如IMG_1234.HEIC对应IMG_1234.HEIC.jpg。 src/data/images-metadata.json属于运行时数据文件,不再纳入 git 跟踪。- 仓库内提供
src/data/images-metadata.example.json作为格式参考与初始化模板。
当运行时 src/data/images-metadata.json 丢失、损坏,或需要按 OSS 现状重建时,可执行:
npm run rebuild-oss-metadata脚本行为:
- 自动加载项目根目录
.env - 扫描 OSS 下的:
photowall/origin/photowall/thumbnails/full/photowall/thumbnails/medium/photowall/thumbnails/tiny/
- 以
origin原图文件名作为filename - 按
origin/<filename>→thumbnails/*/<filename>.jpg规则匹配缩略图 - 优先读取原图 EXIF 中的拍摄时间(如
DateTimeOriginal/CreateDate/ModifyDate)作为date - 若 EXIF 不存在或读取失败,则回退为 OSS 对象时间
- 重新生成运行时
src/data/images-metadata.json,并尽量保留已有记录中的videoSrc、isVisible、visibilityUpdatedAt等字段
必需环境变量:
OSS_REGION=oss-cn-hangzhou
OSS_BUCKET=myblog-photowall
OSS_ACCESS_KEY_ID=your_access_key_id
OSS_ACCESS_KEY_SECRET=your_access_key_secret
# 可选
OSS_ENDPOINT=oss-cn-hangzhou.aliyuncs.com注意:
- 该脚本只重建 metadata,不修改现有上传 / 删除主流程。
- 首次部署或新环境初始化时,可先复制示例文件:
cp src/data/images-metadata.example.json src/data/images-metadata.json- 若线上 metadata 丢失,但 OSS 原图/缩略图仍在,可直接执行
npm run rebuild-oss-metadata恢复。 - 由于真实 metadata 已不再纳入 Git,后续
git pull/git reset --hard不会再把仓库里的旧 metadata 覆盖回来;但如果部署目录会清空未跟踪文件,仍建议在部署后补跑一次恢复脚本。
当 src/data/images-metadata.json 丢失、损坏,或需要按 OSS 现状重建时,可执行:
npm run rebuild-oss-metadata脚本行为:
- 扫描 OSS 下的
photowall/origin/、photowall/thumbnails/full/、photowall/thumbnails/medium/、photowall/thumbnails/tiny/ - 以
origin原图文件名作为filename - 优先读取原图 EXIF 中的拍摄时间(
DateTimeOriginal/CreateDate/ModifyDate)作为date - 若 EXIF 不存在或读取失败,则回退为 OSS 对象时间
- 重新生成
src/data/images-metadata.json,并尽量保留已有记录中的videoSrc、isVisible、visibilityUpdatedAt等字段
必需环境变量:
OSS_REGION=oss-cn-hangzhou
OSS_BUCKET=myblog-photowall
OSS_ACCESS_KEY_ID=your_access_key_id
OSS_ACCESS_KEY_SECRET=your_access_key_secret
# 可选
OSS_ENDPOINT=oss-cn-hangzhou.aliyuncs.com注意:
- 该脚本只重建 metadata,不修改现有上传 / 删除主流程。
- 当前恢复依赖
origin与thumbnails/full都存在;若某张图缺少 full 缩略图,会被跳过并在 OSS 侧补齐后重跑。
/admin/photos 支持:
- 批量上传图片到 OSS
- 手动触发处理接口(兼容入口,实际上传时已自动处理)
- 设置照片可见/隐藏状态
- 删除照片(同时删除 OSS 对象与 metadata)
npm run generate-metadata(底层脚本为 scripts/process-photos.cjs)仍可用于从本地 public/photowall/ 做历史数据回填或手动重建缩略图/metadata,但它不是当前生产环境的主流程,也不应替代后台上传/删除接口对 metadata 的日常维护。
仓库中仍保留 readme-OSS.md 与 src/services/onedrive-sync/ 作为历史实现参考;当前 server.ts 未注册 OneDrive 同步路由,默认不启用该链路。
| 文件 | 函数 | 说明 |
|---|---|---|
storage.ts |
safeGetItem/SetItem/RemoveItem |
SSR 兼容的 localStorage |
date.ts |
parseMonthFromDate, parseDate |
日期解析 |
theme.ts |
getAppTheme, getGalleryTheme, getNavTheme |
主题配置 |
format.ts |
formatFileSize, formatResolution, formatMegapixels |
文件与图片格式化 |
| Hook | 用途 |
|---|---|
useDebounce |
防抖回调(resize、搜索等) |
- Linux 服务器 (Ubuntu 推荐)
- Node.js v20+
- PostgreSQL 数据库
- Nginx (反向代理)
# 克隆项目
git clone <repository-url>
cd MyBlog
# 安装依赖
npm install
# 配置环境变量
cp .env.example .env
# 编辑 .env 文件,配置 DATABASE_URL 等
# 初始化数据库
npm run db:init
# 构建项目
npm run build
# 构建服务器
npm run build:server
# 启动服务(使用 PM2)
pm2 start npm --name "myblog" -- run serve
# 停止服务
pm2 stop myblog
# 重启服务
pm2 restart myblog
# 删除服务
pm2 delete myblogPORT=3000
DATABASE_URL=postgres://user:password@localhost:5432/myblog
NODE_ENV=production
# 管理员配置
ADMIN_USERNAME=admin
ADMIN_PASSWORD=admin123 # 开发环境明文密码
# ADMIN_PASSWORD_HASH=$2a$10$... # 生产环境哈希密码
JWT_SECRET=your_jwt_secret_key
# 照片墙 OSS 读取域名
OSS_PHOTOWALL_BASE_URL=https://myblog-photowall.oss-cn-hangzhou.aliyuncs.com
VITE_OSS_PHOTOWALL_BASE_URL=https://myblog-photowall.oss-cn-hangzhou.aliyuncs.com
# OSS 上传写入权限(后台照片上传)
OSS_REGION=oss-cn-hangzhou
OSS_BUCKET=myblog-photowall
OSS_ACCESS_KEY_ID=your_access_key_id
OSS_ACCESS_KEY_SECRET=your_access_key_secret
OSS_ENDPOINT=oss-cn-hangzhou.aliyuncs.com # 可选
# 单文件上传大小限制(后端 multer,默认 50MB)
PHOTO_UPLOAD_MAX_MB=50
# 每批最多上传文件数(后端请求限制,默认 10)
PHOTO_UPLOAD_MAX_FILES_PER_BATCH=10
# 前端自动分批上传每批最多文件数(默认 10,应与后端限制保持一致)
VITE_PHOTO_UPLOAD_MAX_FILES_PER_BATCH=10
# 前端自动分批上传大小(每批总大小,默认 50MB)
VITE_PHOTO_UPLOAD_BATCH_MB=50server {
listen 80;
server_name your-domain.com;
client_max_body_size 64m;
location / {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
}
}在初始化表结构之前,需要先创建 PostgreSQL 数据库:
# 方式一:使用 createdb 命令
sudo -u postgres createdb myblog
# 方式二:进入 PostgreSQL 交互式终端创建
sudo -u postgres psql
CREATE DATABASE myblog;
CREATE USER myblog_user WITH PASSWORD 'your_password';
GRANT ALL PRIVILEGES ON DATABASE myblog TO myblog_user;
\qnpm run db:init该命令将创建以下数据库表:
pageviews- 文章浏览量统计comments- 评论数据photo_visibility- 照片可见性状态(后台展示开关)
兼容旧版本时,数据库中可能仍保留 onedrive_sync_state / onedrive_sync_items 历史表,不影响当前 OSS 直传模式。
-- 浏览量表
CREATE TABLE pageviews (
post_id VARCHAR(255) PRIMARY KEY,
views INTEGER DEFAULT 0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- 评论表
CREATE TABLE comments (
id SERIAL PRIMARY KEY,
post_id VARCHAR(255) NOT NULL,
parent_id INTEGER REFERENCES comments(id) ON DELETE CASCADE,
nickname VARCHAR(100) NOT NULL,
content TEXT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- 照片可见性表
CREATE TABLE photo_visibility (
photo_key TEXT PRIMARY KEY,
is_visible BOOLEAN NOT NULL DEFAULT TRUE,
updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
);| 方法 | 路径 | 说明 |
|---|---|---|
| GET | /api/pageview?id=<postId> |
获取文章浏览量 |
| POST | /api/pageview?id=<postId> |
增加文章浏览量 |
| GET | /api/comments?postId=<postId> |
获取文章评论列表 |
| POST | /api/comments |
发表评论 |
| GET | /api/photos/metadata |
获取可见照片列表 |
| GET | /api/photos/metadata?includeHidden=1 |
获取全部照片(含隐藏) |
| POST | /api/photos/upload |
上传照片到 OSS(管理员) |
| POST | /api/photos/process |
手动处理入口(管理员,兼容接口) |
| PATCH | /api/photos/visibility |
设置单张照片是否展示(管理员) |
| DELETE | /api/photos/:filename |
删除照片与 OSS 对象(管理员) |
| 类别 | 技术 |
|---|---|
| 前端 | React 19 + TypeScript 5 |
| 构建 | Vite 7 |
| 样式 | Tailwind CSS 3 |
| 动画 | Framer Motion |
| 路由 | React Router 7 |
| Markdown | react-markdown + remark-gfm |
| 后端 | Express.js |
| 数据库 | PostgreSQL (pg library) |
| 认证 | JWT + bcryptjs |
| 部署 | PM2 + Nginx |
| 命令 | 说明 |
|---|---|
npm run dev |
启动前端开发服务器 |
npm run build |
构建前端和后端 |
npm run serve |
启动生产服务器 |
npm run db:init |
初始化数据库表结构 |
npm run generate-metadata |
手动回填/重建照片墙缩略图与 metadata(历史工具,非主流程) |
MIT License