项目概览

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

站点当前已经实现首页、项目、博客、关于、基础设施、404、RSS、robots 和 Sitemap,并提供文章与项目详情页。内容使用 Markdown 或 MDX 编写,Astro 在构建阶段把它们转换为静态 HTML;生产环境不需要数据库,也不需要常驻 Node.js 服务。

当前仓库可以完整执行类型检查和生产构建,但仍使用 https://example.com、示例邮箱和 GitHub 地址。Rocky Linux、Caddy 与 GitHub Actions 是后期目标运维架构,仓库中暂时没有对应的服务器配置和部署工作流。本文会明确区分“已经实现的站点源码”和“准备实施的生产部署”。

目标与设计原则

项目围绕以下目标展开:

  • 内容优先:文章和案例应当是普通文本文件,可以审查、比较和长期迁移;
  • 静态优先:能在构建阶段完成的工作不留到请求阶段,减少线上故障面;
  • 渐进增强:没有 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 checknpm 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 → /var/www/blog → Static Files

生产请求不会执行 Markdown 解析、数据库查询或 Node.js 业务逻辑。Caddy 负责 TLS、压缩、安全响应头和文件服务,Astro 的职责在构建完成时已经结束。

目录结构

下面是与运行和内容维护直接相关的实际结构:

Blog/
├── .gitignore
├── docs/
│   └── 个人技术博客_Astro_Rocky_实施方案.md
└── 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://example.com、邮箱和 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,
}

这一配置同时作用于 devpreview,只允许本机访问,也与现有 localhost127.* 和私有地址直连规则兼容。排查同类问题时可以先验证:

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. 提交源码,由手工流程或未来 CI 生成生产产物。

项目案例涉及客户或设备时,必须删除客户名称、真实 IP、访问凭据、工艺参数和未公开指标。无法验证的结果应明确标为示例,不把假设数据写成真实成果。

目标部署架构

本节描述后期运维目标,当前仓库尚未包含 Caddyfile、服务器初始化脚本或 GitHub Actions 工作流。

Git Repository
      │ push production branch

CI or trusted build host
      │ npm ci
      │ npm run check
      │ npm run build

    dist/
      │ rsync over SSH

Rocky Linux /var/www/blog


Caddy → HTTPS → Browser

服务器只接收 dist/,不保存项目源码、.git/node_modules/.env。这样可以降低低配 VPS 的内存占用,也能把构建失败与线上请求处理隔离开。

第一阶段:手工发布

先手工跑通完整链路,再自动化同一套步骤:

npm ci
npm run check
npm run build
rsync -avz --delete dist/ deploy@example.com:/var/www/blog/

这里的用户、域名和路径必须按真实服务器替换。上传后恢复 SELinux 标签,并分别验证 HTTP 重定向、HTTPS、首页、详情页、CSS、RSS 和 Sitemap。

目标 Caddy 配置可以从最小静态站开始:

example.com {
    root * /var/www/blog
    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/CD

手工部署稳定后,CI 只自动化已验证的过程:检出代码、安装锁定依赖、类型检查、静态构建、上传 dist/、恢复标签并做 HTTP 健康检查。部署使用独立普通用户和受限 SSH Key,不使用 root,也不把服务器密钥写进仓库。

任何一步失败都应停止发布;构建失败时不得覆盖线上目录,上传完成但健康检查失败时应保留上一版本用于恢复。

后期运维

系统与权限

  • Rocky Linux 保持 SELinux Enforcing,不通过关闭安全机制解决文件访问问题;
  • 使用持久化文件上下文标记 /var/www/blog,部署后执行 restorecon
  • firewalld 公网只开放实际需要的 TCP/80 和 TCP/443;
  • SSH、监控面板和其他管理入口优先通过 Tailscale 或限制来源地址;
  • Caddy 和部署用户只获得完成各自职责所需的最小权限。

静态目录的 SELinux 配置示例:

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://example.com
curl -I https://example.com
curl -I https://example.com/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 仓库是文章、项目、配置和构建逻辑的源数据;/var/www/blog 只是可重建产物。需要备份的是 Git 远端、/etc/caddy/Caddyfile、DNS 信息和必要的服务器安全配置,而不是 node_modules/dist/

灾难恢复流程应能够在新主机上完成:安装 Caddy、恢复配置和权限、重新构建源码、上传 dist/、恢复 SELinux 标签、验证 DNS 与 HTTPS。定期验证恢复步骤比单纯确认“存在备份文件”更重要。

回滚策略

MVP 阶段可以对错误提交执行 git revert,重新构建并发布。需要更快回滚时,再升级为版本化目录:

/var/www/blog/
├── releases/
│   ├── commit-a/
│   ├── commit-b/
│   └── commit-c/
└── current -> releases/commit-c/

新版本先上传到独立目录并完成检查,最后原子切换 current 软链接;失败时切回上一版本。该方案属于后期优化,不是当前仓库已经实现的能力。

依赖维护

定期检查 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.mjsurl 替换占位域名后重新构建全部静态文件
详情页 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. 固化 Caddy、SELinux、firewalld 和备份配置;
  4. 将已验证流程迁移到 GitHub Actions;
  5. 根据真实使用需求评估搜索、统计与版本化发布目录。

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

项目复盘

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

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

当前源码已经具备可重复检查和静态构建能力;生产域名、真实服务器和自动部署仍需按本文的目标运维步骤实施。本文不使用虚构的性能分数、访问量或线上稳定性数据,后续只有在真实部署和持续观测后才补充可验证结果。