0%

搭建博客记录

仓库地址:https://github.com/magicskysword/NightBlog

前言

一直想拥有一个属于自己的独立博客,用来写点技术和杂谈。

经过一番折腾,最终选择了 Hexo + NexT 主题。

由于该博客使用Claw Cloud Run进行托管,服务商不支持API更新(蛋疼),因此改为采用 Docker 部署基础服务器 + 使用 Github Action 构建网站后主动通知服务器拉取更新的方式。

前置环境

系统:Ubuntu in wsl

编辑器:vscode

node.js:22

docker:最新发行版

一、Hexo 初始化与主题集成

万事开头难,Hexo 提供了简洁的命令行工具,很适合搭建个人博客。

1. 初始化 Hexo 博客

1
2
3
4
npm install -g hexo-cli
hexo init blog
cd blog
npm install

初始化完成后,项目结构如下:

1
2
3
4
5
6
7
blog/
├── _config.yml # 全局配置
├── package.json # Node.js 项目文件
├── source/ # 文章与静态资源目录
├── themes/ # 主题目录
├── scaffolds/ # 文章模板
└── public/ # 构建输出目录

2. 集成 NexT 主题

我选择了 NexT 主题,比较顺眼,对中文支持较好。

1
2
3
4
5
6
cd themes
git clone https://github.com/theme-next/hexo-theme-next next

# 删除.git文件,因为我会改动主题里的内容+需要托管到git里
cd next
rm -rf .git

配置 _config.yml 指定主题:

1
theme: next

3. Docker配置

为了方便开发,配置了一套本地构建的开发环境Docker

由于我初学Docker,这部分基本都使用AI生成,赞叹AI的伟大。

Dockerfile

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
FROM node:22

# 设置工作目录
WORKDIR /app

# 安装 pnpm
RUN npm install -g pnpm

# 复制包管理配置文件
COPY package.json pnpm-lock.yaml ./

# 安装主项目依赖(包含 hexo、主题依赖声明)
RUN pnpm install

# 复制所有源代码
COPY . .

# 单独为 NexT 主题安装依赖
RUN if [ -f themes/next/package.json ]; then \
cd themes/next && pnpm install || true; \
fi

# 构建静态页面
RUN pnpm run build

# 启动本地 Hexo 服务器
EXPOSE 4000
# 如果设置了 BUILD_ONLY=true,则容器启动时什么也不做
# 否则,启动 Hexo 本地服务器
CMD ["/bin/sh", "-c", "if [ \"$BUILD_ONLY\" != \"true\" ]; then pnpm run server; else echo 'Build only mode. Nothing to run.'; fi"]

docker-compose.yml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
version: '3'

services:
# 本地开发:启动 hexo server
hexo:
build: .
ports:
- "4000:4000"
volumes:
- ./source:/app/source
- ./themes:/app/themes
- ./_config.yml:/app/_config.yml
command: pnpm run server

# CI 构建服务:只构建静态文件
hexo-builder:
build: .
environment:
- BUILD_ONLY=true
volumes:
- ./public:/app/public # 导出构建结果
command: /bin/true # 什么都不做,避免执行 CMD 中的 server

第一篇帖子与构建

使用以下命令生成第一篇帖子。

1
hexo new post HelloWorld

使用以下指令构建博客

1
pnpm run build

到这里博客搭建部分就结束了。

博客自动化部署

接下来我打算使用 node.js 制作一个docker image部署到服务器上。核心步骤如下:

            graph TD
            subgraph 博客流程
A[本地编写博客] --> B[推送到Github]
B --> C[Github构建静态页面]
C --> D[发布为Release]
D --> E[通知云服务器下载]
end

subgraph 云服务器流程
F[接受通知] --> G[下载最新Release]
G --> H[部署并更新]
end

E --> F
          

1.编写 server.js 和 deploy.js

server.js 的作用就是挂载博客页面和监听更新

server.js

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
const express = require('express');
const path = require('path');
const { exec } = require('child_process');
const fs = require('fs');
const { downloadAndExtract } = require('./deploy');

const app = express();
require('dotenv').config();

const PORT = process.env.PORT || 3000;
const TOKEN = process.env.UPDATE_TOKEN;
const BLOG_ZIP_URL = process.env.BLOG_ZIP_URL;
const DEPLOY_DIR = process.env.DEPLOY_DIR || path.join(__dirname, 'public');

// Serve static website
app.use('/', express.static(DEPLOY_DIR));

// API trigger
app.post('/api/update', async (req, res) => {
const reqToken = req.query.token;
if (reqToken !== TOKEN) {
return res.status(403).json({ message: 'Forbidden: Invalid token' });
}

try {
console.log('Triggered update via API.');
await downloadAndExtract(BLOG_ZIP_URL, DEPLOY_DIR);
res.json({ message: 'Update successful' });
} catch (err) {
console.error('Update failed:', err);
res.status(500).json({ message: 'Update failed', error: err.message });
}
});

// Start server
app.listen(PORT, '0.0.0.0', async () => {
console.log(`Server running at http://localhost:${PORT}`);
console.log('Performing initial deployment...');
try {
await downloadAndExtract(BLOG_ZIP_URL, DEPLOY_DIR);
console.log('Initial deployment complete.');
} catch (err) {
console.error('Initial deployment failed:', err);
}
});

deploy.js 的作用是从GitHub下载文件。

需注意,此处使用了 follow-redirects 库替代了内置的http库,是因为GitHub的文件链接存在一次重定向,而内置的库不支持重定向。

deploy.js

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
const { https } = require('follow-redirects');
const fs = require('fs');
const path = require('path');
const AdmZip = require('adm-zip');

/**
* 下载 zip 并解压到目标目录(会先清空该目录)
* @param {string} url 下载地址
* @param {string} targetDir 解压目录
*/
async function downloadAndExtract(url, targetDir) {
const tmpZip = path.join('/tmp', 'blog.zip');

// 下载 zip 文件
console.log(`Downloading ${url}...`);
await new Promise((resolve, reject) => {
const file = fs.createWriteStream(tmpZip);
https.get(url, (response) => {
if (response.statusCode !== 200) {
return reject(new Error(`Failed to download: ${response.statusCode}`));
}

response.pipe(file);
file.on('finish', () => {
file.close((err) => {
if (err) return reject(err);
resolve();
});
});
}).on('error', reject);
});

const stats = fs.statSync(tmpZip);
console.log(`Downloaded zip size: ${stats.size} bytes`);

// 清空目标目录
if (fs.existsSync(targetDir)) {
console.log(`Removing existing directory: ${targetDir}`);
fs.rmSync(targetDir, { recursive: true, force: true });
}
fs.mkdirSync(targetDir, { recursive: true });

// 解压(使用 adm-zip)
console.log(`Extracting to ${targetDir}...`);
const zip = new AdmZip(tmpZip);
zip.extractAllTo(targetDir, true);

fs.unlinkSync(tmpZip);
console.log('Deploy complete.');
}

module.exports = { downloadAndExtract };

对应的package.json

package.json

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
{
"name": "blog-deployer",
"version": "1.0.0",
"description": "Webhook-based blog deployer for GitHub Actions",
"main": "server.js",
"scripts": {
"start": "node server.js"
},
"author": "your-name",
"license": "MIT",
"dependencies": {
"adm-zip": "^0.5.16",
"dotenv": "^16.4.1",
"express": "^4.19.2",
"follow-redirects": "^1.15.11",
"unzipper": "^0.10.11"
}
}

2.编写Docker

这一步是为了把环境打包成一个Docker,方便后续挂载到云服务器上。

Dockerfile

1
2
3
4
5
6
7
8
9
10
11
12
FROM node:18

WORKDIR /app

COPY package*.json ./
RUN npm install

COPY server.js deploy.js ./

EXPOSE 3000

CMD ["node", "server.js"]

docker-compose.yml

1
2
3
4
5
6
7
8
services:
deployer:
build: .
container_name: blog-deployer
ports:
- "3000:3000"
env_file:
- .env

3.Github Action自动化

新建一个文件 .github/workflows/build.yml,该文件用于 Github 上的自动化。

我的目的是修改了deployer目录里的文件时,自动打新的包。而 Github Action,对我而言并不是特别熟悉。

因此我选择把我的流程描述给 LLM,让 LLM 帮我来编写

Action的主要功能如下:

  • blog目录变动时,打包博客页面,并让云服务器自动拉取更新。
  • deploy目录变动时,构建新的docker镜像。

build.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
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
name: Build and Release Blog + Deployer

on:
push:
branches:
- main
paths:
- 'blog/**'
- 'deployer/**'
- '.github/workflows/**'

workflow_dispatch:

permissions:
contents: write
packages: write

jobs:
build-blog:
name: Build and Release Blog
runs-on: ubuntu-latest

steps:
- name: Checkout code
uses: actions/checkout@v3
with:
fetch-depth: 0

- name: Detect blog directory change
id: changes
uses: dorny/paths-filter@v3
with:
filters: |
blog:
- 'blog/**'

- name: Exit if no blog changes
if: steps.changes.outputs.blog != 'true'
run: echo "No changes in blog/, skipping build."

- name: Set up Git identity
if: steps.changes.outputs.blog == 'true'
run: |
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"

- name: Set up Node.js
if: steps.changes.outputs.blog == 'true'
uses: actions/setup-node@v3
with:
node-version: 22

- name: Install pnpm
if: steps.changes.outputs.blog == 'true'
run: npm install -g pnpm

- name: Install dependencies
if: steps.changes.outputs.blog == 'true'
working-directory: blog
run: |
pnpm install
if [ -f themes/next/package.json ]; then
cd themes/next
pnpm install || true
fi

- name: Build static site
if: steps.changes.outputs.blog == 'true'
working-directory: blog
run: pnpm run build

- name: Check built files
if: steps.changes.outputs.blog == 'true'
run: ls -R blog/public

- name: Zip built blog
if: steps.changes.outputs.blog == 'true'
working-directory: blog/public
run: zip -r $GITHUB_WORKSPACE/blog-release.zip .

- name: Force-update blog-main tag
if: steps.changes.outputs.blog == 'true'
run: |
git tag -f blog-main
git push origin blog-main --force

- name: Create or Update GitHub Release
if: steps.changes.outputs.blog == 'true'
uses: softprops/action-gh-release@v1
with:
tag_name: blog-main
name: Blog Release (main)
files: blog-release.zip
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

- name: Trigger Remote Deploy via Webhook
if: steps.changes.outputs.blog == 'true'
run: |
curl -X POST "${{ secrets.DEPLOY_WEBHOOK_URL }}?token=${{ secrets.DEPLOY_WEBHOOK_TOKEN }}" \
|| echo "⚠️ Webhook failed, continuing anyway."

build-deployer:
name: Build and Push Deployer Image
runs-on: ubuntu-latest
if: github.event_name == 'push' && github.ref_type != 'tag'

steps:
- name: Checkout code
uses: actions/checkout@v3

- name: Detect deployer directory change
id: changes
uses: dorny/paths-filter@v3
with:
filters: |
deployer:
- 'deployer/**'

- name: Log in to GitHub Container Registry
if: steps.changes.outputs.deployer == 'true'
uses: docker/login-action@v2
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Build and push image
if: steps.changes.outputs.deployer == 'true'
uses: docker/build-push-action@v5
with:
context: ./deployer
file: ./deployer/Dockerfile
push: true
tags: ghcr.io/${{ github.repository_owner }}/deployer:latest

总结

这次博客搭建的过程蛮曲折的,中途有各种报错,各种各种小问题。

但是现在的大语言模型还是太强大了,一点一点使用大模型辅助排查报错,在无人指导的情况下还是顺利搭建完成了。