用 Github Action,让一个网站拥有两种架构

背景

一个站点同时使用 Astro 和 Hexo 两个静态站点生成器,并非主流做法。但只要两者的输出路径不冲突,完全可以合并到同一个 GitHub Pages 部署中。本文记录一种可行方案:Astro 作为主站,Hexo 挂载在 /blog 子路径,通过 GitHub Actions 完成全部构建与部署,最终发布在自定义域名下。

目标

  • 主站使用 Astro,部署在 https://e-2.top/
  • 博客使用 Hexo,部署在 https://e-2.top/blog/
  • 两个项目独立构建,合并后统一发布
  • 全流程由 GitHub Actions 自动化,无需本地构建

目录结构

仓库根目录 e2/ 下包含两个独立项目:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
e2/
├── site/ # Astro 主站
│ ├── src/
│ ├── public/
│ ├── astro.config.mjs
│ ├── package.json
│ └── package-lock.json
├── hexo-blog/ # Hexo 博客
│ ├── source/
│ ├── _config.yml
│ ├── package.json
│ └── package-lock.json
└── .github/
└── workflows/
└── deploy.yml

技术栈

组件 技术
主站框架 Astro
主站 UI 集成 React (@astrojs/react)
主站 CSS Tailwind CSS v4 (@tailwindcss/vite)
博客框架 Hexo
博客主题 通过 npm 安装
CI/CD GitHub Actions
运行时 Node.js 24
托管 GitHub Pages

架构设计

合并策略

GitHub Pages 每个仓库只能发布一个站点,因此必须先将两个构建产物合并再上传。合并方式为:以 Astro 的 dist 目录作为发布根,将 Hexo 的 public 目录复制到 dist/blog/ 下。

合并后的发布目录结构:

1
2
3
4
5
6
7
8
9
site-dist/
├── index.html # Astro 主站首页
├── assets/ # Astro 静态资源
├── blog/ # Hexo 博客
│ ├── index.html
│ ├── archives/
│ └── ...
├── CNAME # 自定义域名
└── .nojekyll

工作流作业划分

工作流包含三个作业:

  1. build-astro:构建 Astro 主站,上传 artifact astro-dist
  2. build-hexo:构建 Hexo 博客,上传 artifact hexo-public
  3. deploy:下载两个 artifact,合并后发布至 GitHub Pages

deploy 通过 needs 依赖前两个作业,确保合并时两个产物均已完成。

配置文件

Astro 配置

site/astro.config.mjs:

1
2
3
4
5
6
7
8
9
10
11
12
import { defineConfig } from 'astro/config';
import react from '@astrojs/react';
import tailwindcss from '@tailwindcss/vite';

export default defineConfig({
site: 'https://e-2.top',
base: '/',
integrations: [react()],
vite: {
plugins: [tailwindcss()]
}
});

site 用于生成绝对 URL(canonical、sitemap、RSS 等)。若不设置,这些位置将使用错误的地址。base 显式指定为 /,表示主站部署在根路径。

Hexo 配置

hexo-blog/_config.yml 中与部署相关的字段:

1
2
url: https://e-2.top
root: /blog/

root 必须以 / 结尾。若写成 /blog,拼接出的资源路径会缺少分隔符,导致子路径下的 CSS、JS 请求 404。

GitHub Actions 工作流

.github/workflows/deploy.yml:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
name: Deploy Astro + Hexo to Pages

on:
push:
branches: ["master"]
workflow_dispatch:

permissions:
contents: read
pages: write
id-token: write

concurrency:
group: pages
cancel-in-progress: true

jobs:
build-astro:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- uses: actions/setup-node@v4
with:
node-version: 24
cache: npm
cache-dependency-path: site/package-lock.json

- name: Install Astro deps
working-directory: site
run: npm ci

- name: Build Astro
working-directory: site
run: npm run build

- name: Upload Astro artifact
uses: actions/upload-artifact@v4
with:
name: astro-dist
path: site/dist

build-hexo:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- uses: actions/setup-node@v4
with:
node-version: 24
cache: npm
cache-dependency-path: hexo-blog/package-lock.json

- name: Install Hexo deps
working-directory: hexo-blog
run: npm ci

- name: Build Hexo
working-directory: hexo-blog
run: npx hexo clean && npx hexo generate

- name: Upload Hexo artifact
uses: actions/upload-artifact@v4
with:
name: hexo-public
path: hexo-blog/public

deploy:
needs: [build-astro, build-hexo]
runs-on: ubuntu-latest
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
steps:
- name: Download Astro artifact
uses: actions/download-artifact@v4
with:
name: astro-dist
path: site-dist

- name: Download Hexo artifact
uses: actions/download-artifact@v4
with:
name: hexo-public
path: hexo-public

- name: Merge Hexo into /blog
run: |
mkdir -p site-dist/blog
cp -r hexo-public/. site-dist/blog/

rm -f site-dist/blog/CNAME

echo "e-2.top" > site-dist/CNAME

touch site-dist/.nojekyll

- name: Upload Pages artifact
uses: actions/upload-pages-artifact@v3
with:
path: site-dist

- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4

关键步骤说明

分离构建:build-astro 与 build-hexo 是两个独立作业,并行执行,互不干扰。每个作业只检出仓库并安装自身依赖。

缓存:两个作业通过 cache-dependency-path 分别缓存各自的 package-lock.json,避免相互失效。

合并:deploy 作业下载两个 artifact 后,将 Hexo 产物复制到 site-dist/blog/。随后删除 Hexo 可能生成的 CNAME,避免覆盖主域;再写入主域的 CNAME;最后创建 .nojekyll,防止 GitHub Pages 的 Jekyll 处理以下划线开头的目录。

发布:actions/upload-pages-artifact 上传合并后的 site-dist,actions/deploy-pages 完成发布。concurrency 组 pages 保证同时只有一个部署任务运行,新的推送会取消进行中的任务。

GitHub Pages 设置

  • Settings → Pages → Source 选择 GitHub Actions
  • Custom domain 填写 e-2.top
  • DNS 校验通过后启用 Enforce HTTPS

DNS 记录

类型 主机记录 值
A @ 185.199.108.153
A @ 185.199.109.153
A @ 185.199.110.153
A @ 185.199.111.153
CNAME www e-2.top(可选)

若使用 Cloudflare 等 DNS 服务,所有记录需设为 DNS only(关闭代理),以便 GitHub 验证域名并签发证书。

注意事项

package-lock.json 必须提交。两个子项目的 lock 文件若缺失,npm ci 会失败。

Hexo 主题配置文件需提交。通过 npm 安装的主题,其自定义配置通常位于 hexo-blog/_config.<theme>.yml,若不提交,CI 构建将使用主题默认配置,本地与线上表现不一致。

Astro 不应生成 /blog 路由。若 site/src/pages/ 下存在 blog.astro 或 blog/ 目录,会与 Hexo 子路径冲突。内容集合(如 src/content/blog/)不生成路由,不影响。

本地预览合并结果。在本地分别执行两个项目的构建,手动复制合并到同一目录后,用 npx serve 等静态服务器预览,可提前发现路径问题。

Node 版本。工作流指定 Node 24。Astro 6 要求 Node >= 22.12.0,Hexo 对 Node 24 兼容。

总结

该方案的核心在于:两个静态站点生成器分别构建、分别上传 artifact,在部署阶段合并为单一目录树,再由 GitHub Pages 统一发布。合并点位于构建产物层面,而非源码层面,因此两个项目可以各自独立演进,互不感知对方的存在。子路径部署的关键在于 Hexo 的 root 配置与合并时的目录对齐,两者一致即可避免资源路径错误。

项目已开源:https://github.com/2025studentdev/e2

(部分AI生成)