项目概览
这个网站既是个人技术博客,也是工程案例的长期归档入口。它面向需要持续整理技术文章、项目复盘和基础设施笔记的个人开发者,核心目标不是堆叠功能,而是建立一条简单、可验证、容易恢复的内容发布链路。
站点当前已经实现首页、项目、博客、关于、基础设施、404、RSS、robots 和 Sitemap,并提供文章与项目详情页。内容使用 Markdown 或 MDX 编写,Astro 在构建阶段把它们转换为静态 HTML;生产环境不需要数据库,也不需要常驻 Node.js 服务。
当前生产站点已经在 https://hcyblog.uk/ 上线:站点身份与 canonical 已切换到真实域名,静态产物通过 Caddy 提供服务,并完成 HTTPS、HTTP 跳转、RSS、Sitemap 和 404 验证。连续构建 CI、版本目录迁移、首次 GitHub 手动部署和后续 main 自动部署均已通过;版本化回滚也已在两个健康 release 之间完成 B → A → B 双向演练。
目标与设计原则
项目围绕以下目标展开:
- 内容优先:文章和案例应当是普通文本文件,可以审查、比较和长期迁移;
- 静态优先:能在构建阶段完成的工作不留到请求阶段,减少线上故障面;
- 渐进增强:没有 JavaScript 时正文和导航仍可阅读,脚本只承担主题切换等必要交互;
- 集中配置:个人信息、站点 URL、导航和技术栈从单一配置入口读取;
- 类型安全:内容字段、页面属性和组件接口在构建前完成校验;
- 可访问性:键盘焦点、跳过链接、语义化导航、颜色对比度和减少动画偏好属于基础能力;
- 可恢复运维:Git 仓库是源数据,服务器只保存可替换的静态产物。
为了守住这些原则,第一阶段没有加入 React Islands、搜索、评论、Analytics、CMS、登录、数据库或服务端渲染。这些能力并非永远不能增加,而是只有在需求和维护成本都明确时才进入项目。
快速开始
环境要求
建议使用 Node.js 22.19 或更高的受支持偶数版本以及 npm。Astro 本身要求受支持的 Node.js 版本,当前依赖树中的 undici 在较新的 Node.js 22 上也不会产生 engine 提示。
仓库根目录包含实施文档,真正的 Astro 项目位于外层 src/ 目录。因此从仓库根目录开始时先进入该目录:
cd src
npm ci
npm run dev
开发服务器固定监听:
http://127.0.0.1:4321/
http://localhost:4321/ 通常会回退到同一个 IPv4 回环地址。固定使用 IPv4 是为了兼容本机启用 v2rayN TUN 且未启用 TUN IPv6 的场景,不会把开发服务器暴露给局域网。
常用命令
| 命令 | 用途 | 主要输出 |
|---|---|---|
npm run dev |
启动本地开发服务器和内容热更新 | 127.0.0.1:4321 |
npm run check |
检查 Astro、TypeScript 和内容 schema | 诊断结果 |
npm run build |
生成生产静态站点 | dist/ |
npm run preview |
本地预览生产构建结果 | 127.0.0.1:4321 |
修改内容后至少执行一次 npm run check 和 npm run build。开发服务器能打开页面,不代表内容 schema、所有静态路由和生产过滤逻辑一定正确。
技术选型
| 关注点 | 选择 | 原因与取舍 |
|---|---|---|
| 站点框架 | Astro 7 | 以 HTML 为默认输出,适合内容站和静态路由,不必为了组件复用引入完整 SPA |
| 类型系统 | TypeScript Strict | 组件属性、集合条目和工具函数在构建前暴露类型问题 |
| 样式系统 | Tailwind CSS 4 Vite 插件 | 直接接入 Astro 的 Vite 管线,同时保留一份集中维护的全局设计系统 |
| 内容格式 | Markdown / MDX | 正文便于版本控制,必要时仍可嵌入结构化组件 |
| 内容索引 | Astro Content Collections | frontmatter 经过 Zod schema 校验,列表与详情页共享同一数据来源 |
| 输出方式 | Static | 线上只托管文件,不需要应用服务器、会话或数据库 |
| 客户端交互 | 原生 JavaScript | 只实现三态主题和持久化,避免为少量交互引入客户端框架 |
Astro 项目使用 astro/tsconfigs/strict,并通过 @/* 映射引用应用源码。Tailwind 4 以 @tailwindcss/vite 插件接入,没有额外的客户端运行时。MDX、RSS 和 Sitemap 分别由 Astro 官方集成提供。
整体架构
构建期数据流
site.config.mjs Markdown / MDX
│ │
│ glob() content loaders
│ │
└──────────────┬───────────────┘
▼
Astro Content Collections
│
getCollection() / sort / filter
│
getStaticPaths() / render()
│
▼
Pages + Layouts + Components + CSS
│
astro build
│
▼
dist/
HTML / CSS / RSS / robots / Sitemap
内容加载、schema 校验、草稿过滤、排序和 Markdown 渲染全部发生在构建阶段。详情路由通过内容文件 ID 生成稳定 URL,例如:
src/content/projects/personal-tech-blog.md
→ /projects/personal-tech-blog/
访问链路
本地开发时,请求直接进入 Astro 开发服务器;已经上线的生产核心链路只读取静态文件:
开发环境
Browser → 127.0.0.1:4321 → Astro Dev Server
生产核心链路(已上线)
Browser → DNS → Caddy / HTTPS → Astro Static Files
生产请求不会执行 Markdown 解析、数据库查询或 Node.js 业务逻辑。Caddy 负责 TLS、压缩、安全响应头和文件服务,Astro 的职责在构建完成时已经结束。
目录结构
下面是与运行和内容维护直接相关的实际结构:
Blog/
├── .gitignore
├── .github/
│ └── workflows/
│ ├── ci.yml # 检查、构建、归档与受控部署
│ └── rollback.yml # 手动选择已有版本回滚
├── docs/
│ └── 个人技术博客_Astro_Rocky_实施方案.md
├── ops/
│ ├── bootstrap-server.sh # 一次性服务器迁移与 Caddy 切换
│ └── hcyblog-ci-entry # CI Key 只能进入的强制命令接口
└── src/ # Astro 项目根目录
├── astro.config.mjs # 静态输出、尾斜杠、集成与本地监听地址
├── site.config.mjs # 个人信息、站点 URL、导航与技术栈
├── package.json # 依赖和 dev/check/build/preview 脚本
├── package-lock.json # 可复现安装
├── tsconfig.json # Strict 模式和 @/* 路径别名
├── public/
│ └── favicon.svg
└── src/
├── components/ # 页头、页脚、卡片、标签、主题与目录
├── layouts/ # 通用 SEO 框架、博客与项目正文布局
├── pages/ # 文件路由、RSS、robots 和 404
├── content/
│ ├── blog/ # 技术文章
│ └── projects/ # 项目案例
├── content.config.ts # Blog 与 Project schema
├── styles/global.css # 设计令牌、响应式布局与 Markdown 样式
└── utils/ # 内容筛选、排序和日期格式化
node_modules/、.astro/ 和 dist/ 都是可重新生成的目录,已经由仓库根级 .gitignore 排除,不应进入版本控制或备份。
内容模型与路由
Blog 内容接口
博客 frontmatter 包含:
| 字段 | 必填 | 作用 |
|---|---|---|
title |
是 | 页面标题、卡片标题与 RSS 标题 |
description |
是 | 摘要、SEO description 与 RSS 描述 |
pubDate |
是 | 发布时间和倒序排序依据 |
updatedDate |
否 | 更新日期和文章 Open Graph 元数据 |
tags |
否 | 标签列表与 RSS categories |
draft |
否 | 开发可见、生产过滤,默认 false |
Project 内容接口
项目 frontmatter 包含标题、描述、分类和技术栈,并用以下字段控制展示:
featured:是否进入首页精选候选;order:项目列表的升序位置;draft:生产构建是否过滤;placeholder:是否显示匿名化示例标识和发布前替换提示。
开发环境会加载草稿并显示 Draft 标识,生产构建会过滤 draft: true。博客按发布日期倒序,项目按 order 升序;首页再从精选项目中取前三项。
静态路由生成
列表页调用 getCollection() 获取可见内容。详情页通过 getStaticPaths() 为每个内容 ID 创建路径,并用 render() 得到正文组件和标题列表。二级、三级标题会进入右侧目录,不需要作者手工维护锚点。
站点统一使用尾斜杠,例如 /blog/linux-socket/。404 是独立静态页面,未知路径由生产 Web Server 按其静态站规则返回。
页面与组件设计
BaseLayout 统一输出语言、标题、description、canonical、Open Graph、favicon、RSS 和 Sitemap 链接,并装配跳过链接、页头和页脚。Blog 与 Project 布局只负责各自的元信息、标签、正文和目录,避免每个页面重复定义 SEO 和外壳。
响应式导航在桌面使用普通 <nav>,移动端使用原生 <details>,即使没有客户端框架也支持键盘操作。项目卡片和文章卡片只接收内容集合条目,链接始终由条目 ID 推导。
全局样式使用暖白浅色、石墨深色和冷青强调色,并以 CSS 变量维护主题。正文针对标题、列表、引用、表格和代码块提供统一排版;表格与代码块允许横向滚动,避免长命令或目录树撑破移动布局。
主题与可访问性
主题切换包含“系统、浅色、深色”三种状态:
- 首次访问遵循
prefers-color-scheme; - 用户选择浅色或深色后写入
localStorage; - 内联脚本在正文绘制前应用已保存主题,减少首屏闪烁;
- 切回系统模式时删除持久化覆盖,继续跟随操作系统。
站点还提供跳到主要内容的链接、清晰的焦点状态、导航当前页标识、语义化目录和 prefers-reduced-motion 适配。图形装饰不承担唯一信息来源,主题按钮同时具有文本与可访问名称。
SEO 与内容分发
SEO 数据从页面属性与 site.config.mjs 合成:
- 每页独立的标题和 description;
- 基于
siteConfig.url与当前路径生成 canonical; - Open Graph 标题、描述、类型和 URL;
- 博客文章的发布时间、更新时间和标签元数据;
/rss.xml输出所有非草稿博客文章;/robots.txt指向 Sitemap;@astrojs/sitemap在构建时生成 Sitemap;- SVG favicon 不依赖外部图片服务。
site.config.mjs 已配置生产域名 https://hcyblog.uk、联系邮箱和 GitHub 地址。修改这些值后必须重新构建并重新发布,因为 canonical、robots、RSS 和 Sitemap 都从同一配置生成。
本地网络与 TUN 兼容
开发阶段曾出现浏览器无法打开本地站点的问题。Astro 当时只监听 IPv6 回环地址 ::1:4321,而 v2rayN TUN 配置未启用 IPv6 地址并使用严格路由,因此请求没有到达开发服务器。关闭代理并不是可靠修复,也会改变正在使用的网络环境。
最终在 astro.config.mjs 中固定 IPv4 回环地址:
server: {
host: '127.0.0.1',
port: 4321,
}
这一配置同时作用于 dev 和 preview,只允许本机访问,也与现有 localhost、127.* 和私有地址直连规则兼容。排查同类问题时可以先验证:
curl.exe -I http://127.0.0.1:4321/
netstat -ano | findstr :4321
如果 127.0.0.1 可访问而 localhost 不可访问,再检查本机名称解析;如果端口没有监听,则检查开发进程和端口占用。没有必要为了本站修改全局 IPv6、Windows hosts 或关闭 TUN。
内容开发与发布流程
一次内容变更建议遵循下面的最小闭环:
- 在
src/content/blog/或src/content/projects/新建 Markdown; - 填写符合 schema 的 frontmatter;
- 使用
npm run dev检查正文、目录、代码块和移动端布局; - 执行
npm run check,处理类型和内容错误; - 执行
npm run build,确认所有静态路由都能生成; - 检查 Git 差异,避免提交
dist/、.astro/、环境文件或敏感信息; - 提交源码,由 GitHub Actions 独立执行构建检查并保存同一次运行的部署归档;
main检查成功后自动发布,Pull Request 只检查和构建,不接触服务器。
项目案例涉及客户或设备时,必须删除客户名称、真实 IP、访问凭据、工艺参数和未公开指标。无法验证的结果应明确标为示例,不把假设数据写成真实成果。
当前部署状态与演进架构
生产核心链路已经手工跑通,构建 CI 也已达到三个连续真实提交首次成功的稳定门槛。仓库当前正在把人工上传改造成“同次构建产物 + 受限 SSH + 原子软链接”的发布链路;私钥和服务器密码不会进入 Git:
Git push / Pull Request
│
▼
GitHub Actions → npm ci → check → build
│
tar.gz + SHA-256 + manifest
│
main 自动 / workflow_dispatch 手动
│ restricted SSH
▼
incoming → releases/<commit> → current symlink
│
▼
Caddy → HTTPS → hcyblog.uk
服务器只接收 CI 已验证的静态归档,不保存项目源码、.git/、node_modules/ 或 .env,也不执行 Node.js 构建。发布账户没有 sudo 权限,CI Key 只能调用固定的状态、部署和回滚命令。
第一阶段:手工发布(已完成)
站点已经通过手工流程完成首次生产发布和外部验收。真实部署账户和站点目录现已核验;下面只保留旧流程作为迁移背景,服务器地址与凭据不写入公开案例:
npm ci
npm run check
npm run build
rsync -avz --delete dist/ BlogDeploy@server:/var/www/blog/
当前 SELinux 状态已核验为 Disabled,因此本轮不会执行 restorecon 或把启用 SELinux 混入发布链路;发布后仍分别验证 HTTP 重定向、HTTPS、首页、详情页、CSS、RSS 和 Sitemap。
下面是与当前公开访问链路一致的 Caddy 参考配置,不是服务器配置文件的副本:
hcyblog.uk {
root * /var/www/blog/current
encode zstd gzip
file_server
header {
X-Content-Type-Options nosniff
Referrer-Policy strict-origin-when-cross-origin
Permissions-Policy "camera=(), microphone=(), geolocation=()"
}
}
在启用前先检查 TCP/80、TCP/443 和可能被现有代理占用的 UDP/443。若其他服务已经占用 TCP/443,需要设计统一入口,不能让两个进程竞争同一监听地址。
第二阶段:纯构建 CI(已完成)
根级 .github/workflows/ci.yml 在推送 main、面向 main 的 Pull Request 和手动触发时运行。它使用最新 Node.js 22.x,依次执行 npm ci、npm run check 和 npm run build,权限只有读取仓库内容。该阶段已经取得三个连续、不同真实提交的首次运行成功结果。
非 Pull Request 构建现在还会生成 tar 归档、SHA-256 校验文件和部署 manifest;归档只保存 14 天。构建成功仍不等于生产更新,只有部署作业成功并且公开健康检查匹配同一提交时才算发布完成。
第三阶段:SSH 原子部署与回滚(已完成)
仓库已经实现受限 SSH Key、版本化目录、归档校验、公开健康检查和 current 软链接原子切换。管理员已把原静态站复制为 bootstrap 版本并切换 Caddy root,首次 GitHub 手动发布也已验证同一 SHA 的构建与线上 manifest。任何校验失败都会停止激活,切换后的健康检查失败则恢复上一版本;普通 main 推送已接入自动部署,独立回滚工作流也完成了两个健康 release 之间的 B → A → B 验收。
后期运维
系统与权限
当前已核验的服务器基线是 Rocky Linux 9.8、systemd 管理的 Caddy、启用的 Tailscale、/var/www/blog 静态目录和无 sudo 权限的专用部署账户。SELinux 当前为 Disabled;公网 SSH 仍允许密码登录和 root 登录,这两项作为后续独立加固任务记录,不在发布改造中顺带修改。
- Caddy 以独立系统用户读取 0755 的静态目录;
BlogDeploy只拥有站点发布目录,不通过 sudo 操作服务或系统配置;- CI 公钥使用 forced command、
restrict、无 PTY 和无端口转发; - Caddyfile 只在一次性管理员初始化中修改,普通内容发布不 reload Caddy;
- SSH 全局加固、firewalld 规则固化和 SELinux 启用需要单独变更与回退方案。
如果未来决定启用 SELinux,需要先在独立维护窗口验证文件上下文,而不是直接在当前 Disabled 状态下执行以下模板:
sudo semanage fcontext -a -t httpd_sys_content_t "/var/www/blog(/.*)?"
sudo restorecon -Rv /var/www/blog
发布验收
每次发布至少检查:
sudo caddy validate --config /etc/caddy/Caddyfile
curl -I http://hcyblog.uk
curl -I https://hcyblog.uk
curl -I https://hcyblog.uk/rss.xml
同时确认 HTTPS 证书正常、HTTP 自动跳转 HTTPS、静态资源返回成功、canonical 使用生产域名、手机宽度无横向溢出。Caddy 配置修改后优先使用平滑 reload,不为普通内容更新重启整台服务器。
日志与基础监控
个人静态站第一阶段不需要复杂监控平台,先保证关键层级可观察:
systemctl --failed
systemctl status caddy
journalctl -u caddy --since today
ss -lntup
df -h
free -h
重点关注证书续期、HTTP 5xx、磁盘空间、Caddy 服务状态、端口冲突和最近一次发布结果。外部可用性检查可以定时请求首页和一个静态资源,但不应把监控复杂度做得高于站点本身。
备份与恢复
Git 仓库是文章、项目、配置和构建逻辑的源数据;生产静态目录只是可重建产物。需要备份的是 Git 远端、/etc/caddy/Caddyfile、DNS 信息和必要的服务器安全配置,而不是 node_modules/ 或 dist/。
灾难恢复流程应能够在新主机上完成:安装 Caddy、恢复配置和权限、重新构建源码、上传静态归档,并验证 DNS 与 HTTPS;只有目标主机启用 SELinux 时才需要恢复对应文件上下文。定期验证恢复步骤比单纯确认“存在备份文件”更重要。
回滚策略
源码错误仍可以执行 git revert 后重新构建;生产快速恢复则使用已经写入仓库的版本目录和手动回滚工作流:
/var/www/blog/
├── releases/
│ ├── commit-a/
│ ├── commit-b/
│ └── commit-c/
└── current -> releases/commit-c/
新版本先进入 incoming,通过哈希、结构和 manifest 检查后移动到独立目录,最后原子替换 current 软链接。服务器保留 bootstrap 和最近五个提交版本;回滚只能选择存在且再次通过结构与公开健康检查的版本。该能力已经进入仓库,但是否视为生产完成以首次发布和回滚演练结果为准。
依赖维护
定期检查 Node.js、Astro、Tailwind 和集成包的受支持版本。依赖升级应在独立提交中进行,先阅读变更说明,再执行:
npm outdated
npm audit
npm run check
npm run build
不要在没有验证的情况下批量升级所有主版本。package-lock.json 必须与 package.json 一起提交,生产构建使用 npm ci 保证依赖树可复现。
常见问题排查
| 现象 | 优先检查 | 处理方向 |
|---|---|---|
| 本地页面打不开 | 127.0.0.1:4321 是否监听 |
检查开发进程、端口占用和 IPv4 回环,不关闭 TUN |
| 内容未出现在生产站 | draft、文件位置和构建日志 |
将可发布内容设为非草稿并重新构建 |
| 构建报告 schema 错误 | frontmatter 字段与类型 | 按内容集合接口补齐字段,不绕过校验 |
| canonical 或 Sitemap 域名错误 | site.config.mjs 的 url |
确认生产域名后重新构建并发布全部静态文件 |
| 详情页 404 | 内容 ID、尾斜杠和上传结果 | 检查对应 dist/ 目录是否完整发布 |
| 样式或脚本 404 | dist/_astro/ 是否同步 |
确保部署整个 dist/,不要只复制 HTML |
| Caddy 返回 403 | 文件权限与 SELinux 标签 | 检查目录读取权限并执行 restorecon |
| HTTPS 申请失败 | DNS、TCP/80、TCP/443 | 确认解析生效、防火墙开放且端口未被占用 |
排查时应从浏览器、DNS、端口、Caddy、文件系统到构建产物逐层验证,避免同时修改代理、防火墙和应用配置后失去证据链。
当前边界与后续路线
当前版本没有搜索、评论、Analytics、CMS、登录或 React Islands。原子部署与版本化回滚已经上线并通过实际演练。近期路线按风险从低到高排列:
- 已完成:配置个人信息、生产域名和 GitHub 地址;
- 已完成:在真实域名上完成手工 Caddy 部署与外部验收;
- 已完成:纯构建 GitHub Actions 达到三个连续首次成功的
main提交; - 已完成:初始化版本目录并验证受限 SSH 手动部署;
- 已启用:成功的
main推送自动部署同一次运行产物; - 已完成:在两个健康 release 之间执行 B → A → B 回滚;
- 后续加固:收紧 SSH、固化 firewalld、评估 SELinux,并验证备份恢复;
- 按需评估:搜索、统计等新增能力。
任何新功能都应回答三个问题:它解决了什么已发生的问题、是否必须增加客户端或服务端状态、故障时如何降级和恢复。
项目复盘
这个项目的主要价值不是“用 Astro 做了一个博客”,而是把内容、构建、展示和运维拆成边界清晰的层次。Markdown 是源数据,内容集合负责契约,页面与组件负责表达,Astro 负责生成,Web Server 只负责交付。
静态架构不会自动带来高质量,但它让问题更容易定位:内容错误在构建期暴露,页面错误可以直接检查生成文件,服务器故障集中在 DNS、TLS、端口、权限和文件服务几个层面。对个人技术站而言,这种可解释性比引入更多平台能力更重要。
当前源码、生产域名、Caddy 静态访问链路、首次手工发布、构建 CI、受限 SSH 原子发布和版本化回滚都已经通过实际验证。自动部署只发布同一次运行生成的静态产物,回滚演练也验证了两个已有版本之间的可逆切换。本文不使用虚构的性能分数、访问量或线上稳定性数据,后续只有在持续观测后才补充可验证结果。