项目概览

这个网站既是个人技术博客,也是工程案例的长期归档入口。它面向需要持续整理技术文章、项目复盘和基础设施笔记的个人开发者,核心目标不是堆叠功能,而是建立一条简单、可验证、容易恢复的内容发布链路。

站点当前已经实现首页、项目、博客、关于、基础设施、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 变量维护主题。正文针对标题、列表、引用、表格和代码块提供统一排版;表格与代码块允许横向滚动,避免长命令或目录树撑破移动布局。

主题与可访问性

主题切换包含“系统、浅色、深色”三种状态:

  1. 首次访问遵循 prefers-color-scheme;
  2. 用户选择浅色或深色后写入 localStorage;
  3. 内联脚本在正文绘制前应用已保存主题,减少首屏闪烁;
  4. 切回系统模式时删除持久化覆盖,继续跟随操作系统。

站点还提供跳到主要内容的链接、清晰的焦点状态、导航当前页标识、语义化目录和 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。

内容开发与发布流程

一次内容变更建议遵循下面的最小闭环:

  1. 在 src/content/blog/ 或 src/content/projects/ 新建 Markdown;
  2. 填写符合 schema 的 frontmatter;
  3. 使用 npm run dev 检查正文、目录、代码块和移动端布局;
  4. 执行 npm run check,处理类型和内容错误;
  5. 执行 npm run build,确认所有静态路由都能生成;
  6. 检查 Git 差异,避免提交 dist/、.astro/、环境文件或敏感信息;
  7. 提交源码,由 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。原子部署与版本化回滚已经上线并通过实际演练。近期路线按风险从低到高排列:

  1. 已完成:配置个人信息、生产域名和 GitHub 地址;
  2. 已完成:在真实域名上完成手工 Caddy 部署与外部验收;
  3. 已完成:纯构建 GitHub Actions 达到三个连续首次成功的 main 提交;
  4. 已完成:初始化版本目录并验证受限 SSH 手动部署;
  5. 已启用:成功的 main 推送自动部署同一次运行产物;
  6. 已完成:在两个健康 release 之间执行 B → A → B 回滚;
  7. 后续加固:收紧 SSH、固化 firewalld、评估 SELinux,并验证备份恢复;
  8. 按需评估:搜索、统计等新增能力。

任何新功能都应回答三个问题:它解决了什么已发生的问题、是否必须增加客户端或服务端状态、故障时如何降级和恢复。

项目复盘

这个项目的主要价值不是“用 Astro 做了一个博客”,而是把内容、构建、展示和运维拆成边界清晰的层次。Markdown 是源数据,内容集合负责契约,页面与组件负责表达,Astro 负责生成,Web Server 只负责交付。

静态架构不会自动带来高质量,但它让问题更容易定位:内容错误在构建期暴露,页面错误可以直接检查生成文件,服务器故障集中在 DNS、TLS、端口、权限和文件服务几个层面。对个人技术站而言,这种可解释性比引入更多平台能力更重要。

当前源码、生产域名、Caddy 静态访问链路、首次手工发布、构建 CI、受限 SSH 原子发布和版本化回滚都已经通过实际验证。自动部署只发布同一次运行生成的静态产物,回滚演练也验证了两个已有版本之间的可逆切换。本文不使用虚构的性能分数、访问量或线上稳定性数据,后续只有在持续观测后才补充可验证结果。