‹ 返回博客

个人网站(一):开篇——一个人怎么做自己的内容站

2026-09-20 · 约 9 分钟读完 · #编程

一个人做个人网站(一):开篇——一个人怎么做自己的内容站

这是系列的第一篇。我会先交代这个站「是什么」「为什么这么选」「怎么跑起来」,后面 8 篇再逐个拆开讲实现细节。

如果你也想一个人拥有一个完全属于自己的内容站——不是公众号、不是各种笔记平台,而是域名、代码、数据都在自己手里的那种——希望这个系列能给你一条可复制的路径。

一、这个网站长什么样

线上地址 wulala.cc,我叫它「小欢喜」。它不是一个纯博客,而是一个把我想放在网上的东西都收拢进来的个人内容站:

板块 内容 备注
博客 文章、分类、标签、草稿、阅读量 Markdown 写作,存源文+渲染后 HTML
相册 相册集、照片拖拽排序、灯箱浏览 图片自动压缩、生成缩略图
视频 视频卡片、在线播放 大文件分片上传、自动截帧做封面
作品 项目/作品展示 与博客共用一套内容范式
习惯打卡 GitHub 风格年度热力图 支持补卡、按年切换
小游戏 2048、合成大西瓜、恐龙跑酷 带全站排行榜,访客昵称即玩
AI 网页宠物 桌宠挂件,可以聊天 接 DeepSeek,口令门禁+限流

除了访客能看到的这些,还有一个只有我能进的后台:文章/相册/视频/作品的增删改查、标签分类管理、打卡和游戏纪录管理、站点配置(首页 banner、宠物人设、社交链接等全部后台可视化配置)。日常更新内容完全不需要登录服务器。

技术栈一句话概括:

二、技术选型:一个人维护,简单和可控压倒一切

选型没有绝对的好坏,只有约束下的取舍。这个项目的约束是:开发者只有我一个人,需求会随时变,我希望三年后还能看懂自己的代码。

为什么是 Flask 而不是 Django

Django 当然更"全"——admin、ORM、migration、表单全家桶。但这个站的后台我本来就要用 Vue 自己写(需要完全贴合内容形态的交互,比如照片拖拽排序、视频上传进度),Django admin 用不上;它的 migration 体系对单人小项目也偏重,加个字段要生成迁移脚本、部署时还要 migrate。

Flask 给我的是:一张白纸 + 足够薄的抽象。我用应用工厂组织代码、用蓝图分模块、用一个统一响应信封收口 API 风格,剩下的全部自己掌控。数据表用 create_all() 随启动自动建,加表加列重启即可——这套策略的边界后面第 2 篇会专门讲。

反过来,我没有用 FastAPI:这个站是传统的服务端渲染页面 + CRUD API,没有异步 IO 密集场景,Flask 的同步模型更直白,生态里 markdown、Pillow、oss2 这些库也都是同步的,不用在 async/await 上付出额外心智。

为什么前端是 SPA,但又"留了一只 SSR 的脚"

访客端是 Vue SPA,体验顺滑;但有个绕不过去的问题:微信里分享文章,卡片需要标题、封面、描述(Open Graph 标签),而微信爬虫不执行 JavaScript——纯 SPA 抓到的永远是一个空 <div id="app">。

所以我的方案不是"SPA or SSR"二选一,而是:

同一篇文章因此有两个"视图",但内容数据源只有一份。这套分流是整个架构里最有意思的部分,第 7 篇详谈。

其他几个选型

三、总体架构:一张图看清四条链路

                         ┌─────────────────────────────────────────────┐
 浏览器(普通访客)──────►│ Nginx :80/:443                              │
                         │  ├─ 静态资源  → frontend/dist(SPA)          │
                         │  ├─ /api/ /media/ sitemap robots rss         │
                         │  │                 → Gunicorn 127.0.0.1:5001 │
                         │  ├─ /ossimg/ → OSS 上海内网 endpoint(剥下载头)│
                         │  └─ 前台页面 + 爬虫 UA → Flask SSR(OG 标签)  │
                         └───────────────┬─────────────────────────────┘
                                         ▼
                    Gunicorn(2 worker,sync,timeout 600)
                    Flask 应用工厂 → 蓝图(auth/posts/gallery/videos/
                    works/checkin/games/pet/site/upload/oss_direct/seo)
                                         │
                              MySQL 8(personal_site, utf8mb4)
                              图片/视频 → 阿里云 OSS
                              DeepSeek API(密钥只在后端)

 管理端浏览器 ──STS 临时凭证──► 直传 OSS(图片 PUT / 视频分片,字节不经 ECS)

四条链路值得记住:

  1. 页面链路:静态文件 Nginx 直接吐,只有爬虫回源 Flask;
  2. API 链路:/api/ 全部反代 Gunicorn;
  3. 图片链路:浏览器访问 wulala.cc/ossimg/xxx,Nginx 走内网 endpoint 回源 OSS——免外网流量费,还顺手剥掉了 OSS 默认域名 2025 年底开始强制加的 Content-Disposition: attachment(不加这个,微信爬虫不认图片,分享卡片直接退化);
  4. 上传链路:后台浏览器先向后端换 STS 临时凭证,然后直传 OSS,300MB 的视频也不经过 ECS。

后端只监听 127.0.0.1:5001,对公网完全不可见,所有出入都过 Nginx——限流、真实 IP、HTTPS、超时控制在一处收口。

四、代码怎么组织

单仓库,前后端平级:

self-web/
├── backend/
│   ├── app/
│   │   ├── __init__.py       # create_app() 应用工厂
│   │   ├── api/              # 路由层:每个业务域一个蓝图
│   │   ├── models/           # SQLAlchemy 模型
│   │   ├── services/         # storage / sts_service / ai
│   │   ├── utils/            # JWT 装饰器、验证码、markdown、统一响应
│   │   └── seo.py            # 爬虫 SSR + sitemap/robots/rss
│   ├── config.py             # 全部配置从 .env 注入
│   ├── run.py                # 开发入口
│   └── seed.py               # 首次建库 + 默认管理员
├── frontend/src/
│   ├── views/                # 访客页面
│   ├── admin/                # 后台页面
│   ├── games/                # 小游戏(注册表 + 各游戏组件)
│   ├── components/           # 导航、灯箱、桌宠等
│   ├── composables/          # usePagedList / useMobile / useSeo
│   ├── stores/               # Pinia:auth / site
│   └── utils/ossUploader.js  # 直传/中转统一上传入口
└── deploy/                   # systemd、gunicorn、nginx、运维文档

后端的入口是一个很标准的应用工厂,启动时做四件事:装配置、初始化扩展、注册蓝图、建表:

def create_app():
    app = Flask(__name__)
    app.config.from_object(get_config())          # 按 FLASK_ENV 选 Dev/Prod

    db.init_app(app)
    cors.init_app(app, resources={r'/api/*': {'origins': app.config['CORS_ORIGINS']}},
                  supports_credentials=True)

    from . import models                          # 先导入模型
    from .api import register_blueprints
    register_blueprints(app)                      # 再注册蓝图

    with app.app_context():
        db.create_all()                           # 启动即建表,无需迁移脚本
    return app

所有敏感配置——数据库密码、OSS 密钥、AI key——一律走 .env,代码里只留 _env('OSS_ACCESS_KEY_ID') 这样的读取。.env 在 .gitignore 里,仓库里给的是 .env.example。

五、怎么跑起来:本地与生产的差异

本地开发

后端 python run.py 起在 5001(debug 热重载),前端 npm run dev 起在 5174,Vite 把 /api 代理到 5001,CORS 只放行这两个本地端口:

# config.py
class BaseConfig:
    CORS_ORIGINS = ['http://127.0.0.1:5174', 'http://localhost:5174']

端口刻意避开 5000/5173 这两个"谁都可能占用"的默认值,省去大量本地端口冲突。

生产部署:同源 + 进程托管

生产环境前后端同源部署,不需要 CORS:Nginx 同一个域名下既托管 dist/ 又反代 /api/。后端进程交给 systemd:

# deploy/personal-site.service
[Service]
Type=simple
WorkingDirectory=/home/luhuan/project/self-web/backend
ExecStart=/home/luhuan/project/self-web/backend/venv/bin/gunicorn \
          -w 2 -b 127.0.0.1:5001 --timeout 600 \
          --access-logfile - --error-logfile - run:app
Restart=always
RestartSec=3

几个参数背后都是有故事的:

日常更新代码的流程短到可以记在肌肉里:服务器 git pull → 有新依赖就 pip install → systemctl restart personal-site;前端改动则在本地 build 后 scp 覆盖 dist/。

为什么前端不在服务器上构建

这是一个真实的坑:服务器系统自带 Node 16.20.2,而 Vite 6 要求 Node 18/20+。我没有选择在服务器上折腾 nvm(生产机少装一套版本管理器就少一个变量),而是定了规矩:

本地 Windows(Node 24)构建,scp -r dist/* 服务器:.../frontend/dist/,服务器不装 Node。

构建产物是纯静态文件,在"哪台机器"构建对运行没有任何影响,而本地构建还顺手能先点一遍验证。这也让 ECS 保持"只装运行时依赖"的干净状态。

六、这一路踩过的坑(开篇先列部署相关的)

  1. 服务器 Python 也是源码编译的 3.9.18,绑定的 OpenSSL 较老。因此密码哈希我固定用 pbkdf2:sha256,不用较新 Python 默认的 scrypt——后者在老 OpenSSL 上会出问题。
  2. Nginx 是源码安装在 /usr/local/nginx,不是 yum 版,重载命令是 /usr/local/nginx/sbin/nginx -s reload。照着网上教程 systemctl reload nginx 是找不到服务的。
  3. HTTP 不强制跳 HTTPS。证书万一过期,http://wulala.cc 仍能完整访问,续期只需替换 pem/key 再 reload,网站不会在证书事故时整体宕掉。
  4. 上传体积上限要两处对齐:Flask MAX_CONTENT_LENGTH = 300MB 和 Nginx client_max_body_size 300m,少配任何一边都会在另一头报错。
  5. .env、venv/、uploads/、dist/ 全部不进 git。这样服务器上 git pull 永远不会冲掉线上配置和文件;代码源头永远是本地 Windows,服务器改挂了直接 git checkout -- .。
  6. 含密码的部署笔记也不要进仓库。我专门在 .gitignore 里放了一条服务器配置文件名,敏感信息只留在本地。

七、小结

一个人做站,我的核心原则是三条:

地基交代完了,接下来进入实现细节。下一篇我们钻进后端,看怎么用应用工厂、统一响应信封和配置分层,把 Flask 组织成一个虽然小但"有框架感"的项目——第 2 篇:后端骨架,把 Flask 写成"小框架"。

系列导航:① 开篇与总体架构(本文)→ ② Flask 后端骨架 → ③ 登录与安全 → ④ 博客内容模块 → ⑤ Markdown 编辑器 → ⑥ OSS 直传全攻略 → ⑦ SPA 的 SEO 与微信分享 → ⑧ 打卡热力图与小游戏 → ⑨ AI 网页宠物与收官