下拉查看上一篇

从零到一:一个 AI 驱动的静态网站是如何诞生的


dranixj 网站构建过程全记录

一、引言

Claude Code 最近大发慈悲,亚洲区等于白送 4 倍 token,小杯变大杯。想着不用就浪费,拿着之前入的域名加白嫖的 AWS 免费用户,就来搞个网站吧。别管新闻新不新,最起码是自己想看的——还没广告。

dranixj 是一个知识与新闻分享网站——AI 搜索热门话题、生成文章、构建为纯静态页面,然后由 Nginx 直接配信给访问者。没有登录注册,没有评论系统,没有用户数据库。整个网站就是一堆 HTML、CSS、JSON 文件,安安静静地躺在服务器上等人来看。

听起来简单?确实简单。但走到这一步,中间经历了一次不小的架构"推倒重来"。

做啥内容呢?打开手机备忘录的小本本,里面有每天看头条留下的各种关键字——OpenClaw、Playwright、WebMCP 等等。干脆就做个给关键字帮我收集话题、组织成文章的工具,并打包挂到互联网上吧。光有这些太干了,干脆再加点乱七八糟的自行车啊、跑步啊、游戏啊。别管有没有 sense,就主打一个任性。

二、最初的架构设计:传统三层架构

项目刚开始的时候,我的第一反应是搭一个"正经"的全栈应用——React 做前端、Express 做后端 API、Aurora PostgreSQL 做数据库。经典的三层架构,教科书式的选型。

当时的想法很直觉:文章得存数据库吧?分类和标签得有关联表吧?搜索得走 SQL 查询吧?于是数据库设计文档写了一版,ORM 也挑好了,Express 路由规划得整整齐齐。

成本构成大概是这样:

项目 月费用
Aurora PostgreSQL(db.t4g.micro) ~$50
EC2 t3.small(跑 Express + Nginx) ~$15
其他(S3、数据传输、CloudWatch 等) ~$13
合计 ~$78

月费 78 美元,对于一个个人项目来说……说不上贵,但也绝对说不上便宜。AWS 复归用户的 100 美刀,只够折腾一个月的话肯定是不划算。尤其是 Aurora PostgreSQL 那 50 刀,占了大头。

这个阶段的代码后来保留在了 feature/server-version 分支,作为"历史的见证"。

当然,失去数据库也意味着失去一些互动性。但对于这个项目的需求来说,这个取舍是值得的。

三、架构转折:从全栈到纯静态

转折点出现在仔细审视了一遍网站的实际需求之后。

我问自己:这个网站有哪些写操作?答案是——没有。所有内容由 AI 在本地生成,经我审核后发布。读者只是来看文章的,不需要登录、不需要评论、不需要提交任何数据。整个网站就是一个纯只读的内容展示平台。

既然是纯只读的,那为什么要跑一个 Express 后端?为什么要养一个数据库?

于是新方案浮出水面:

content/output/*.md → build-content.js → client/public/data/*.json → Nginx 静态配信

Markdown 文件作为"数据库",构建脚本作为"后端 API",生成好的 JSON 文件直接让 Nginx 配信。没有运行时服务器进程、没有数据库连接池、没有 ORM。

迁移过程说起来也干脆——废止 Express 后端、删除 Aurora PostgreSQL、把数据模型从数据库表改为 Markdown 文件的 YAML frontmatter。原来的数据库设计文档顶部加了一行大大的"已废弃",指向新的内容管道文档。

成本的变化相当可观:

项目 月费用
EC2 t4g.micro(仅 Nginx) ~$6
CloudWatch 监控 ~$3
域名 + 杂项 ~$11
合计 ~$20

从 78 刀降到 20 刀,降幅接近 75%。主要是甩掉了 Aurora PostgreSQL 这个最大的开销,同时 EC2 也从跑应用服务的 t3.small 降到了只跑 Nginx 的 t4g.micro(ARM 架构还更便宜)。

当然,纯静态方案也不是没有代价。最明显的就是搜索功能——客户端搜索需要先加载整个 search-index.json(包含所有文章的标题和摘要),然后在本地做关键词匹配。文章数量少的时候还好,数量多了以后这个 JSON 文件会越来越大。不过对于一个个人网站来说,这个量级的文章数量在可预见的未来都不会是问题。

取舍有时就是这样:你用一点点理论上的"可扩展性",换来了架构上的极致简洁和运营成本的大幅下降。对这个项目来说,这笔买卖很划算。

四、内容管道设计

静态架构的核心就是内容管道——把 Markdown 文件变成前端能直接用的 JSON。

内容格式

每篇文章都是一个 Markdown 文件,头部用 YAML frontmatter 定义元数据:

---
title: 文章标题
slug: article-slug
date: 2026-03-17
category: ai
tags: [AI, LLM, 工具]
summary: 文章摘要,50-100 字
cover: /images/articles/article-slug/cover.jpg
author: AI 生成
readingTime: 8 分钟
difficulty: 中级
---

# 文章标题

正文内容...

这个格式够简单也够灵活。slug 用于 URL 路径,category 关联分类,tags 用于搜索和展示,coverImage 是文章卡片上显示的封面图。

构建脚本

build-content.js 是整个管道的核心。它做的事情其实不复杂:

  1. 扫描 content/output/ 下所有 .md 文件
  2. 用 gray-matter 解析 YAML frontmatter,提取元数据
  3. 用 marked + highlight.js 把 Markdown 正文转成 HTML(代码块带语法高亮)
  4. 从正文末尾的"参考来源"章节提取引用链接
  5. 读取 topics.yaml 获取分类定义
  6. 生成一系列 JSON 文件到 client/public/data/

生成的 JSON 文件一览:

文件 说明
articles-page-{N}.json 分页文章列表(每页 12 篇)
articles/{slug}.json 文章详情(完整 HTML 正文、来源、标签)
categories.json 分类列表
categories/{slug}/page-{N}.json 分类下的文章列表(分页)
search-index.json 搜索用的轻量索引(仅标题 + 摘要)

一次 node scripts/build-content.js 就把所有 JSON 生成好,React 前端直接 fetch 这些静态 JSON 文件就行——简单、快速、不需要任何运行时依赖。

build-content.js 构建输出

分类配置

分类体系在 topics.yaml 里定义。目前有 5 个分类:

分类 说明 权重
AI AI 工具、开源项目、大模型 3(最高频)
Fitness 器材评测、训练技巧、赛事 2
TV Game 主机游戏、攻略、资讯 2
Hobby 模型制作、遥控车、树莓派 1
Blog 个人博客原创内容 0(不自动生成)

每个分类都配有检索关键词列表,用于 AI 自动搜索话题。关键词还有个 recommend 标记——true 表示验证过的优质检索词,false 表示不适合的词(话题过时、质量低等),没标记的就是待评价。这样 AI 在选择话题时就有了优先级参考。

权重系统决定生成文章时各分类被选中的概率:AI 分类权重 3,意味着它被选中的概率大约是 Hobby(权重 1)的 3 倍。Blog 分类权重 0,完全不参与自动生成——博客文章由另一套 Skills 从本地素材创作。

从搜索到发布

完整的工作流是这样的:

内容管道流程:从配置到部署

generate.yaml 控制运行时参数:每次生成 3 篇、字数 1500-3000 字、搜索偏好(Hacker News、GitHub Trending、TechCrunch、36氪等)、图片配置(优先从原文获取封面图,兜底用 Unsplash)。

整套管道设计的哲学就是:配置驱动、AI 执行、人工把关。

五、前端技术选型与设计哲学

技术栈

前端选了 React 19 + Vite 6 + React Router 7。选型理由很朴素——React 用得最熟、Vite 构建速度快、React Router 是 React 生态下最成熟的路由方案。

依赖列表很短。package.json 的 dependencies 就三个包:react、react-dom、react-router-dom。没有 Redux、没有 Axios、没有 CSS-in-JS。数据获取用原生 fetch,状态管理用 useState/useEffect,样式用普通 CSS 文件。

这种"能少则少"的心态贯穿了整个前端开发。

设计理念

UI 设计受 htmlBurger 启发,走极简主义路线:

  • 内容优先:设计服务于内容,不喧宾夺主
  • 功能至上:每个视觉元素都有明确目的,拒绝纯装饰
  • 克制而有温度:色彩用量少但精准,留白充足

色彩体系也很克制:白底(#ffffff)+ 深灰文字(#353535,不用纯黑,降低视觉压力)+ 蓝色强调(#009fd1,仅用于链接和可交互元素)+ 黄色 CTA(#ffe26d,每屏最多出现一次)。页面主体只用中性色,保持干净。

字体选了 Inter——开源、可变字重、为屏幕阅读优化。6 级字号层级、8px 基数间距体系,这些基础设定让整个网站看起来有序而不死板。

响应式布局

文章网格用 CSS Grid 实现,三个断点覆盖三种布局:

屏幕宽度 列数 关键变化
> 1200px 3 列 完整布局
768px ~ 1200px 2 列 标题字号缩小
< 640px 1 列 搜索框纵向排列

桌面端用 repeat(auto-fill, minmax(320px, 1fr)) 自动填充列数,平板和手机端用固定列数。Header 导航栏在分类按钮过多时不会撑破布局——用 ResizeObserver 检测溢出,自动把放不下的分类收进汉堡菜单。

网站首页:桌面端与移动端

暗黑模式

暗黑模式用 CSS 变量 + localStorage + 系统偏好检测三重机制实现。首次访问时跟随系统设置,用户手动切换后记住偏好。没有花哨的渐变切换动画——内容型网站追求的是即时响应,点一下立刻变,别让人等。

暗黑模式切换对比:桌面端与移动端

六、规则与最佳实践

项目根目录下有一个 rules/ 文件夹,里面放了 13 个规则文件。你可能会好奇:一个个人项目,给谁定规则?

答案是——给 AI 定规则。

这个项目的大部分代码都是和 Claude Code 协作完成的。AI 写代码很快,但如果不给它"行为准则",它可能今天用 class 组件明天用函数组件,今天用 axios 明天用 fetch,代码风格飘忽不定。rules/ 就是用来解决这个问题的。

13 个规则文件覆盖了这些领域:

文件 内容
react-19-patterns.md React 19 新特性和最佳实践
react-components.md 组件设计规范
react-hooks.md Hooks 使用规范
react-performance.md 性能优化策略
react-security.md 前端安全(XSS 防护等)
react-testing.md 测试规范(Vitest + Testing Library)
uiux-design.md UI/UX 设计规范(色彩、字体、间距、组件样式)
nodejs-security.md Node.js 安全实践
nodejs-performance.md Node.js 性能优化
express-api.md Express API 规范(历史遗留)
express5-migration.md Express 5 迁移指南(历史遗留)
database.md 数据库规范(历史遗留)
README.md 规则目录索引

有几个是全栈时代的遗留——Express 和数据库相关的规则现在用不到了,但留着也不碍事。

除了 rules/,还有 CLAUDE.md 作为项目级约定。里面定了一个简单但重要的双语原则:文档用中文、代码用英文。所有注释、提交信息、文档都写中文,但变量名、函数名、文件名用英文。这样既方便阅读,又不会出现拼音命名的尴尬。

写规则这件事,其实就像给新入职的同事写 onboarding 文档——你不能假设对方知道你的偏好,得把期望说清楚。区别是,人类同事读一遍就记住了,AI 每次对话都会重新读一遍。所以这些规则越清晰、越具体,协作效果就越好。

七、AI 在构建中的角色

说到 Claude Code,它在这个项目中的角色远不止"代码补全工具"。

更准确的定位是:一个全栈搭档。

从最初的架构设计讨论("我要不要用数据库?"),到具体的代码编写、部署脚本开发、CI/CD 配置、内容生成管道搭建——甚至你现在读的这篇博客,也是 AI 基于项目素材起的草稿,然后我做审核和补充。

Skills 系统

项目用了两个核心 Skills:

generate-articles——自动搜索热门话题并批量生成文章。运行时它会:

  1. 读取配置文件(分类权重、检索关键词、生成参数)
  2. 按权重选分类 → 选检索词(优先 recommend: true 的)→ 用 WebSearch 搜索
  3. 扫描已有文章去重
  4. 逐篇生成 1500-3000 字的详细文章
  5. 保存到 content/output/

generate-blog——从本地素材生成博客文章。和 generate-articles 不同,它不需要搜索互联网,而是基于项目中已有的文档、代码、配置文件来创作。这篇文章就是这样诞生的。

人机协作的边界

AI 负责的部分:繁重的信息搜索、初稿撰写、代码实现、重复性的配置工作。 人类负责的部分:架构决策、内容审核、品质把关、最终拍板。

这不是"AI 替代人"的故事。更像是"AI 处理 80% 的体力活,人类专注在 20% 的判断和创意上"。比如 AI 能快速生成一篇关于某个开源项目的介绍文章,但它不知道这篇文章的角度是否适合我的读者、这个项目是否值得推荐、文中的技术观点是否准确——这些还是需要人来把关。

偶尔,它也会犯傻。

办傻事的 Claude

如果道歉有用的话还要警察干什么?

八、CI/CD 与部署

GitLab CI 四阶段管道

CI/CD 用的是 GitLab CI,定义在 .gitlab-ci.yml 里,四个阶段:

lint → test → build → deploy

GitLab CI 管道运行成功

  • lint:ESLint 检查前端代码
  • test:Vitest 跑单元和组件测试
  • build:构建内容 JSON + React 构建 + 打包成 tarball
  • deploy:上传 S3 → SSM 触发 EC2 部署(仅 main 分支,手动触发)

运行环境是 node:22-slim 镜像(deploy 阶段换成 amazon/aws-cli),依赖用 npm ci 安装保证一致性。

部署架构

部署这块有个有趣的设计决策——EC2 实例完全不需要访问 GitLab。

GitLab CI Runner → 构建打包 → 上传 S3 → SSM 触发 → EC2 从 S3 下载 → 解压部署

为什么不用传统的 SSH 直连部署?几个原因:

  1. 安全:EC2 不需要开放 SSH 端口(22 端口可以完全关掉),攻击面大幅缩小
  2. 权限隔离:CI 用户只需要 S3 上传和 SSM 发送命令的权限,不需要 SSH 密钥
  3. 审计:SSM 的所有命令执行都有日志记录,比 SSH 操作更容易追溯
  4. 简单:不用管理 SSH 密钥分发和轮换

SSM(Systems Manager)本质上是 AWS 提供的远程命令执行服务。CI 管道通过 SSM 发一条命令让 EC2 执行部署脚本,然后轮询命令执行状态直到成功或失败。整个过程不需要 EC2 主动连接任何外部系统——它只是被动地接收指令、从 S3 下载文件、执行本地脚本。

部署流程细节

EC2 上的部署脚本会做这些事:

  1. 从 S3 下载部署包(tarball)
  2. 备份当前版本到 /var/www/dranixj-backups/
  3. 解压新版本到 /var/www/dranixj/
  4. 重载 Nginx 配置
  5. 执行健康检查

备份机制保留最近 5 个版本。如果新版本出了问题,一行命令就能回滚:

bash infra/scripts/rollback.sh /var/www/dranixj-backups/backup-20260317-120000.tar.gz

也可以从 S3 拉取历史版本回滚。总之,回滚不应该比部署更难——这是运维的基本原则。

SSL 与健康检查

SSL 证书用 Let's Encrypt + certbot,cron 自动续期。健康检查脚本会检查 Nginx 运行状态、静态文件可用性、磁盘空间、SSL 证书过期时间。这些都是基础设施层面的"保底"措施——不出问题的时候你忘了它们的存在,出问题的时候庆幸有它们在。

S3 上的旧部署包也会自动清理,保留最近 10 个。毕竟 S3 存储虽然不贵,但无限堆积也不是好习惯。

九、AWS 权限设定(IAM)

IAM(Identity and Access Management)这一块可能是整个项目里最"无聊"但最重要的部分。权限配错了,轻则部署失败,重则安全事故。

最小权限原则

核心思想很简单:每个角色只给它需要的权限,一点都不多给。

项目里有三种 IAM 身份,职责完全分离:

身份 用途 关键权限
管理用户 日常监控、告警管理 EC2 只读、CloudWatch 读+告警管理、账单查看、S3 只读
CI/CD 部署用户 自动化部署 S3 读写、SSM 发送命令
EC2 实例角色 运行时服务 SSM Agent、S3 只读下载、CloudWatch Agent

管理用户不能部署、CI 用户不能看账单、EC2 不能往 S3 写东西——每个角色各司其职。

CI/CD 用户的权限设计

这个最有代表性。CI 部署用户只需要两件事:

  1. 把构建产物上传到 S3 部署桶
  2. 通过 SSM 给 EC2 发送部署命令

所以它的权限就精确到:

  • s3:PutObject、s3:GetObject、s3:ListBucket、s3:DeleteObject——限定在部署桶
  • ssm:SendCommand——限定到特定文档类型和特定实例
  • ssm:GetCommandInvocation——查询命令执行结果

注意 SSM 权限的拆分:SendCommand 支持资源级限制(可以精确到具体实例),但 GetCommandInvocation 不支持——Resource 必须是 *。这是 AWS API 设计的限制,不是偷懒。如果把这两个权限写在同一个 Statement 里,会因为 Resource 冲突导致权限不足。

EC2 实例角色

EC2 的角色更简单:

  • AmazonSSMManagedInstanceCore——让 SSM Agent 能正常运行
  • S3 只读策略——只能从部署桶下载,不能上传
  • CloudWatchAgentServerPolicy——让 CloudWatch Agent 能上报指标和日志

密钥管理

生产环境的 AWS 凭证(Access Key ID、Secret Access Key)存在 GitLab CI 变量里,标记为"受保护"(仅 protected 分支可用)和"隐藏"(日志中不显示)。本地开发用 .env 文件,当然这个文件在 .gitignore 里——永远不提交。

密钥轮换有标准流程:创建新 Key → 更新 GitLab 变量 → 验证管道正常 → 禁用旧 Key → 删除旧 Key。建议每 90 天轮换一次。

十、监控与运维(CloudWatch)

网站上线不是终点,运维才是长期的事。

监控指标

用 CloudWatch Agent 收集系统级指标,5 分钟采集一次(免费额度内):

指标 来源 说明
CPU 使用率 AWS/EC2 内置 无需 Agent
内存使用率 CloudWatch Agent(自定义命名空间) t4g.micro 只有 1GB,需要关注
磁盘使用率 CloudWatch Agent 根分区监控
Swap 使用率 CloudWatch Agent 内存不够时的缓冲
Nginx 5xx 错误 日志过滤指标 从 access log 中提取
EC2 状态检查 AWS/EC2 内置 实例或底层硬件异常

自定义指标用了独立的 Dranixj 命名空间,在 CloudWatch 控制台里一眼就能找到。

告警策略

告警通过 SNS(Simple Notification Service)发邮件通知:

告警 条件 说明
CPU 过高 > 80%,持续 15 分钟 3 个周期(5 分钟/次)连续超标
内存过高 > 85%,持续 15 分钟 1GB 机器上 85% 就该紧张了
磁盘过高 > 85%,持续 10 分钟 日志堆积或部署包未清理
5xx 错误突增 5 分钟内 > 10 次 静态站点理论上不该有 5xx
状态检查失败 任何失败 实例不可达,最严重的告警

恢复时也会收到通知(OK Action),这样你知道问题什么时候解决了,不用一直盯着。

日志管理

三组日志上报到 CloudWatch Logs:

日志组 来源 保留天数
/dranixj/nginx/access Nginx 访问日志 14 天
/dranixj/nginx/error Nginx 错误日志 14 天
/dranixj/system/syslog 系统日志 7 天

保留天数刻意设得比较短——长期存储日志费钱,而且对小站来说意义不大。14 天足够排查大多数问题了。

成本追踪

整套监控的费用:

方案 月费用
免费额度内(基本指标 + 10 个告警) $0
轻量监控(推荐,详细指标 + 日志 + 告警) ~$3
完整监控(含合成监控 + 日志过滤指标) ~$5-8

对于日 PV 不到 1000 的小站,基本可以在免费额度内覆盖。加上轻量监控大概 3 刀/月,完整监控也就 5-8 刀。这笔钱花得值——总比半夜被用户告诉你"网站挂了"好。

回看整个成本结构:从最初全栈架构的 $78/月,到纯静态方案的 $20/月,这 $20 里包含了 EC2 运行、CloudWatch 监控、域名续费和零散的数据传输费。对于一个功能完整、有 CI/CD、有监控告警、有备份回滚的个人网站来说,这个数字相当合理。

十一、回顾与展望

回头看这个项目,最大的感触是:简单的东西往往更难做到,但一旦做到了,维护起来就特别省心。

架构从全栈三层到纯静态配信,不是偷懒,是在认清需求后做的减法。数据库没了、后端没了、但网站该有的功能一个不少——文章展示、分类浏览、关键词搜索、响应式布局、暗黑模式、CI/CD 自动部署、监控告警、备份回滚。区别只是实现方式更轻量了。

另一个收获是 AI 协作的体验。用 Claude Code 不是"让 AI 替你写代码",更像是有了一个不知疲倦的搭档——你说想法,它给方案;你定规则,它照着执行;你审核把关,它持续迭代。rules/ 目录和 CLAUDE.md 就是这种协作模式的产物——把期望写清楚,AI 就能稳定地输出符合预期的结果。

这个网站目前还在持续更新内容。架构已经稳定,成本可控,运维靠自动化。未来可能会做的事情……先不立 flag,走着看吧。

评论

上滑查看下一篇