背景

原来的博客一直使用 Butterfly 主题,后面想额外保留一个 cactus 主题页面,用来展示更简洁的博客风格。

目标是:

  • 主站继续使用 Butterfly;
  • cactus 作为子站存在;
  • 两个主题互不覆盖静态资源;
  • 最终仍然只发布一个 GitHub Pages 站点。

最终访问结构如下:

1
2
/               Butterfly 主站
/cactus-theme/ cactus 子站

最终方案

不要让两个主题直接混在同一个输出目录里生成。

最终采用的方案是:

1
2
3
cactus-theme/       cactus 独立构建输出
public/ Butterfly 主站输出,也是最终发布目录
public/cactus-theme cactus 被复制进去后的最终位置

也就是说:

1
2
3
先生成 cactus -> cactus-theme/
再生成 Butterfly -> public/
最后复制 cactus-theme/* -> public/cactus-theme/

这样两个主题在构建阶段是分开的,不会互相覆盖 main.jsindex.css 这类同名文件。

配置文件

根目录 _config.yml 仍然保持 Butterfly 作为默认主题:

1
theme: butterfly

Butterfly 的主题配置继续放在:

1
_config.butterfly.yml

cactus 使用单独配置:

1
2
3
theme: cactus
public_dir: cactus-theme
root: /cactus-theme/

这里最重要的是 public_dirroot 要对应。

public_dir: cactus-theme 表示 cactus 先输出到本地的 cactus-theme/ 文件夹。

root: /cactus-theme/ 表示部署后 cactus 的访问路径是 /cactus-theme/

如果这两个不一致,页面可能能打开,但 CSS、JS、图片路径会错。

构建脚本

使用 PowerShell 脚本统一构建两个主题:

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
$ErrorActionPreference = "Stop"

$root = Resolve-Path (Join-Path $PSScriptRoot "..")
$public = Join-Path $root "public"
$butterflyOut = Join-Path $root "public-butterfly"
$cactusOut = Join-Path $root "cactus-theme"

function Remove-WorkspacePath {
param([string]$Path)

if (-not (Test-Path -LiteralPath $Path)) {
return
}

$resolved = Resolve-Path -LiteralPath $Path
if (-not $resolved.Path.StartsWith($root.Path, [System.StringComparison]::OrdinalIgnoreCase)) {
throw "Refusing to remove path outside workspace: $($resolved.Path)"
}

Remove-Item -LiteralPath $resolved.Path -Recurse -Force
}

function Copy-DirectoryContents {
param(
[string]$From,
[string]$To
)

if (-not (Test-Path -LiteralPath $From)) {
throw "Build output not found: $From"
}

New-Item -ItemType Directory -Force -Path $To | Out-Null
Copy-Item -Path (Join-Path $From "*") -Destination $To -Recurse -Force
}

Set-Location $root

Remove-WorkspacePath $public
Remove-WorkspacePath $butterflyOut
Remove-WorkspacePath $cactusOut


hexo clean
hexo --config "_config.yml,_config.cactus.yml" generate

hexo clean
hexo generate

Copy-DirectoryContents $cactusOut (Join-Path $public "cactus-theme")

Write-Host "Dual build assembled:"
Write-Host " Butterfly -> public/"
Write-Host " cactus-theme -> public/cactus-theme/"

Remove-WorkspacePath $cactusOut

完整脚本放在:

1
tools/build-dual.ps1

构建流程是:

  1. 清理旧的 public/cactus-theme/
  2. 使用 _config.yml,_config.cactus.yml 生成 cactus;
  3. 再普通执行 hexo generate 生成 Butterfly 到 public/
  4. 把 cactus 的输出复制到 public/cactus-theme/
  5. 最终只部署 public/

package.json 命令

package.json 中保留快捷命令:

1
2
3
4
5
6
7
8
9
10
"scripts": {
"build": "hexo generate",
"build:butterfly": "hexo clean && hexo generate",
"build:cactus": "hexo --config \"_config.yml,_config.cactus.yml\" clean && hexo --config \"_config.yml,_config.cactus.yml\" generate",
"build:dual": "powershell -NoProfile -ExecutionPolicy Bypass -File tools/build-dual.ps1",
"deploy:dual": "npm run build:dual && hexo deploy",
"clean": "hexo clean",
"deploy": "hexo deploy",
"server": "hexo server"
},

以后本地构建:

1
npm run build:dual

构建并发布:

1
npm run deploy:dual

这里 package.json 和 PowerShell 脚本是配套关系,缺一不可。

package.json 负责提供统一入口:

1
2
npm run build:dual
npm run deploy:dual

这样平时不用记住一长串 Hexo 命令,也不需要手动执行复制目录的步骤。

真正的构建流程放在:

1
tools/build-dual.ps1

PowerShell 脚本负责做具体事情:

  1. 清理旧的 public/cactus-theme/
  2. 生成 cactus 到 cactus-theme/
  3. 再生成 Butterfly 到 public/
  4. cactus-theme/ 复制进 public/cactus-theme/
  5. 删除临时的 cactus-theme/

所以整体流程就是:

1
2
3
4
5
package.json 提供命令入口

tools/build-dual.ps1 执行实际构建

public/ 成为最终发布目录

如果只有 package.json,就只能写很长的一行命令,复制目录和安全清理都不好处理。

如果只有 PowerShell 脚本,每次都要手动记路径和执行方式,不方便发布。

因此最终固定为:

1
npm run build:dual

和:

1
npm run deploy:dual

踩坑记录

1. PowerShell 里 –config 要加引号

错误写法:

1
hexo generate --config _config.yml,_config.cactus.yml

在 PowerShell 里,这个参数可能会被拆坏,导致 Hexo 报:

1
WARN Config file _config.yml _config.cactus.yml not found, using default.

正确写法:

1
hexo --config "_config.yml,_config.cactus.yml" generate

2. 不要把 _config.butterfly.yml 当站点配置合并

错误写法:

1
hexo --config "_config.yml,_config.butterfly.yml" generate

这个写法看起来很合理,但会带来一个隐藏问题:_config.yml_config.butterfly.yml 里都有 subtitle

根目录 _config.yml 里的 subtitle 是站点副标题:

1
subtitle: '异境入侵者'

_config.butterfly.yml 里的 subtitle 是 Butterfly 首页打字机配置:

1
2
3
4
5
subtitle:
enable: true
effect: false
source: 1
sub:

当使用 --config "_config.yml,_config.butterfly.yml" 时,后面的 Butterfly 配置会覆盖前面的站点配置。

于是 config.subtitle 从字符串变成了对象,网页标题就会变成:

1
<title>YJRQZ777 - [object Object]</title>

所以 Butterfly 不需要这样合并配置。

正确写法:

1
hexo generate

因为 Hexo 会根据根目录 _config.yml 里的:

1
theme: butterfly

自动加载:

1
_config.butterfly.yml

作为 Butterfly 的主题配置。

另外,还有一种错误写法:

1
hexo --config "_config.butterfly.yml" generate

这会只读取 Butterfly 配置,而不会读取根目录 _config.yml

结果就是站点标题、作者、部署配置等主配置都丢了,标题可能会变成默认的 Hexo

3. cactus 需要显式合并配置

cactus 是额外生成的子站,所以它需要指定自己的配置:

1
hexo --config "_config.yml,_config.cactus.yml" generate

这里 _config.cactus.yml 会覆盖根配置里的主题、输出目录和根路径:

1
2
3
theme: cactus
public_dir: cactus-theme
root: /cactus-theme/

这个场景和 Butterfly 不同。Butterfly 是主站默认主题,Hexo 会自动加载它的主题配置;cactus 是额外构建目标,所以要手动合并配置。

4. 两个主题不能随便共用 public

一开始尝试让两个主题都输出到 public/,后来发现容易出问题。

例如 Butterfly 页面需要自己的:

1
2
/js/main.js
/css/index.css

cactus 也有自己的:

1
2
/js/main.js
/css/style.css

如果构建顺序或 public_dir 出错,可能出现 Butterfly 页面加载到 cactus 的 JS,导致顶部栏存在但透明不可见。

Butterfly 的导航默认是:

1
2
3
#nav {
opacity: 0;
}

需要 Butterfly 的 main.js 正常执行后加上 show 类:

1
<nav id="nav" class="show">

如果加载错 JS,导航链接还在,但看起来像消失了一样。

5. cactus 的 root 必须和复制目录一致

现在 cactus 的配置是:

1
root: /cactus-theme/

所以最终必须复制到:

1
public/cactus-theme/

如果复制到:

1
public/cactus/

那就要把 cactus 配置改成:

1
root: /cactus/

这两个名字必须一致。

最终目录结构

构建完成后应该是:

1
2
3
4
5
6
7
8
9
public/
index.html
css/
js/
picture/
cactus-theme/
index.html
css/
js/

发布时仍然只需要发布 public/

GitHub Pages 看到的是一个普通静态站,但里面同时包含两个主题。

总结

Hexo 双主题的关键不是让一个主题“切换成另一个主题”,而是把它们当成两个静态站点构建。

Butterfly 负责主站:

1
/

cactus 负责子站:

1
/cactus-theme/

两个主题独立生成,最后再组装到同一个 public/ 目录里发布。这样结构清楚,也能避免静态资源互相覆盖。