前言
个人博客作为日常折腾笔记等记录的平台,我不希望他搞得太过复杂,同时能够随时随地编写我认为也是必不可缺的。
在此之前我用过很多博客平台,动态、静态的都有,经过很长时间的折腾,目前我的个人博客完全是基于Github搭建的,不需要服务器也不需要本地维护Git仓库。基本上完美实现了我心目中的目标,在此特意记录一下大致思路。
平台
首先肯定是选用静态博客,轻量方便嘛,单独维护一个服务器很麻烦,重点是我对自己的技术水准也不是很自信,说不准哪天就把服务器配置搞坏了,所以我不太想使用WordPress这样框架。静态博客由于可以将站点配置,博客文章托管到Github让我不用担心数据安全的问题。
博客生成器选择了Hugo,在此之前很长一段时间使用Hexo,但是由于Hexo复杂的插件机制,虽然很方便,但是在使用过程中经常出现莫名其妙就会报错不能运行的问题,最终我选择使用Hugo,它提供一个二进制包这点我是非常满意的,维护一堆文件太太太烦了。当然Hugo也有不足,在主题方面可选的相比Hexo就差多了,我本人也是喜欢简洁的风格所以这对我来说不是什么问题。
结构
使用博客仓库的不同分支管理多种主题与我的文章。
- main:主分支,保存博客文章
- hugo-next:Hugo NexT 主题的相关配置
- taichi:Taichi 主题相关配置
Taichi 原名Reimu,由于该我自己更改覆盖了很多样式后与原来差别较大,所以我改叫他Taichi,因为它的太极动画做的很显眼
主题分支下一般使用 Git子模块
的方式引用官方主题仓库到theme文件夹
博客内容与主题配置分开放置的好处在于,我可以随时修改构建脚本中拉取的主题分支,来实现切换不同的主题。
部署平台
我是采用Github Pages部署自己的博客,为了加快国内的访问速度还配置了CDN。写这篇文章的时候腾讯云的EdgeOne没出多久,优惠力度很大,因此花费50左右购入了一年的EdgeOne
核心 Github Action
Github Action 承包了我所有的博客构建发布以及其他必要操作等的任务。
具体有以下事情
- 下载Hugo二进制包,配置必要依赖(如
Dart Sass
) - 克隆主题分支
- 克隆文章分支(main)到posts文件夹
- 触发构建(动态传入当前GitHub Pages设置的Url为BaseUrl)
- 上传Algolia索引文件
- 上传构建产物(public文件夹)
- 部署到GitHub Pages
- 清空腾讯云EdgeOne原先的缓存
具体的Action配置如下所示
注意配置下列环境密钥
ALGOLIA_ADMIN_KEY
:用于上传Algolia的密钥
TENCENT_SECRET_ID
:用于清空EdgeOne缓存的腾讯云ID
TENCENT_SECRET_KEY
:用于清空EdgeOne缓存的腾讯云相应密钥
EDGEONE_ZONE_ID
:EdgeOne的ZONE ID
EDGEONE_HOSTNAMES
:需要清空的hostname,多个可以用逗号隔开例如:www.1think2program.cn,1think2program.cn
# 用于构建和部署Hugo网站到GitHub Pages的示例工作流程
name: 发布Hugo网站到Pages
on:
push:
branches:
- main
workflow_dispatch:
permissions:
contents: read
pages: write
id-token: write
concurrency:
group: "pages"
cancel-in-progress: false
defaults:
run:
shell: bash
jobs:
build:
environment:
name: github-pages
runs-on: ubuntu-latest
env:
HUGO_VERSION: 0.139.3
steps:
- name: 安装Hugo CLI
run: |
wget -O ${{ runner.temp }}/hugo.deb https://github.com/gohugoio/hugo/releases/download/v${HUGO_VERSION}/hugo_extended_${HUGO_VERSION}_linux-amd64.deb \
&& sudo dpkg -i ${{ runner.temp }}/hugo.deb
- name: 安装Dart Sass
run: sudo snap install dart-sass
- name: 检出 主题 分支
uses: actions/checkout@v4
with:
ref: taichi
submodules: recursive
fetch-depth: 0
- name: 检出 main 分支到 ./content/posts
uses: actions/checkout@v4
with:
ref: main
path: ./content/posts
submodules: recursive
fetch-depth: 0
- name: 设置Pages
id: pages
uses: actions/configure-pages@v3
- name: 安装Node.js依赖
run: "[[ -f package-lock.json || -f npm-shrinkwrap.json ]] && npm ci || true"
- name: 使用Hugo构建
env:
HUGO_ENVIRONMENT: production
HUGO_ENV: production
run: |
hugo \
--gc \
--minify \
--baseURL "${{ steps.pages.outputs.base_url }}/"
- name: 上传Algolia索引
run: |
# 安装依赖
npm install algoliasearch
echo "APPLICATION_ID: $APPLICATION_ID"
echo "ADMIN_API_KEY: $ADMIN_API_KEY"
echo "INDEX_NAME: $INDEX_NAME"
echo "FILE_PATH: $FILE_PATH"
# 输出脚本
cat > uploadToAlgolia.js << 'EOF'
const { algoliasearch } = require('algoliasearch');
const fs = require('fs');
const path = require('path');
const APPLICATION_ID = process.env.APPLICATION_ID;
const ADMIN_API_KEY = process.env.ADMIN_API_KEY;
const INDEX_NAME = process.env.INDEX_NAME;
const FILE_PATH = process.env.FILE_PATH;
// 初始化Algolia客户端
const client = algoliasearch(APPLICATION_ID, ADMIN_API_KEY);
// 读取索引文件
const filePath = path.resolve(FILE_PATH);
console.log('Reading file:', filePath);
fs.readFile(filePath, 'utf8', async (err, data) => {
if (err) {
console.error('Error reading file:', err);
return;
}
try {
const objects = JSON.parse(data);
// 上传记录到Algolia
const response = await client.saveObjects({indexName: INDEX_NAME, objects: objects, waitForTasks: true});
console.log('Records uploaded successfully:', response);
} catch (parseError) {
console.error('Error parsing JSON file:', parseError);
}
});
EOF
# 执行脚本
node uploadToAlgolia.js
env:
APPLICATION_ID: "68EZJSYL8I"
ADMIN_API_KEY: ${{ secrets.ALGOLIA_ADMIN_API_KEY }}
INDEX_NAME: "shokaX"
FILE_PATH: "./public/algolia.json"
- name: 上传构建产物
uses: actions/upload-pages-artifact@v3
with:
path: ./public
deploy:
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
runs-on: ubuntu-latest
needs: build
steps:
- name: 部署到GitHub Pages
id: deployment
uses: actions/deploy-pages@v4
- name: 设置Node.js环境
uses: actions/setup-node@v3
with:
node-version: "18"
- name: 创建缓存刷新脚本
run: |
cat > purge-edgeone-hostname.js <<EOF
const crypto = require('crypto');
function sha256(message, secret, encoding) {
const hmac = crypto.createHmac('sha256', secret);
return hmac.update(message).digest(encoding);
}
function getHash(message, encoding = 'hex') {
const hash = crypto.createHash('sha256');
return hash.update(message).digest(encoding);
}
function getDate(timestamp) {
const date = new Date(timestamp * 1000);
return date.toISOString().slice(0, 10).replace(/-/g, '-');
}
async function purgeHostname() {
try {
// 环境变量配置
const SECRET_ID = process.env.TENCENT_SECRET_ID;
const SECRET_KEY = process.env.TENCENT_SECRET_KEY;
const ZONE_ID = process.env.EDGEONE_ZONE_ID;
const HOSTNAMES = process.env.EDGEONE_HOSTNAMES.split(',');
// API 参数配置
const endpoint = "teo.tencentcloudapi.com";
const service = "teo";
const region = "ap-guangzhou";
const action = "CreatePurgeTask";
const version = "2022-09-01";
const timestamp = Math.floor(Date.now() / 1000);
const date = getDate(timestamp);
// 构造请求体
const payload = JSON.stringify({
ZoneId: ZONE_ID,
Type: "purge_host",
Targets: HOSTNAMES
});
// ************* 签名计算部分 *************
// 步骤1:规范请求
const httpRequestMethod = "POST";
const canonicalUri = "/";
const canonicalQueryString = "";
const canonicalHeaders = [
\`content-type:application/json; charset=utf-8\`,
\`host:\${endpoint}\`,
\`x-tc-action:\${action.toLowerCase()}\`
].join('\\n') + '\\n';
const signedHeaders = "content-type;host;x-tc-action";
const hashedRequestPayload = getHash(payload);
const canonicalRequest = [
httpRequestMethod,
canonicalUri,
canonicalQueryString,
canonicalHeaders,
signedHeaders,
hashedRequestPayload
].join('\\n');
// 步骤2:待签字符串
const algorithm = "TC3-HMAC-SHA256";
const hashedCanonicalRequest = getHash(canonicalRequest);
const credentialScope = \`\${date}/\${service}/tc3_request\`;
const stringToSign = [
algorithm,
timestamp,
credentialScope,
hashedCanonicalRequest
].join('\\n');
// 步骤3:计算签名
const kDate = sha256(date, 'TC3' + SECRET_KEY);
const kService = sha256(service, kDate);
const kSigning = sha256('tc3_request', kService);
const signature = sha256(stringToSign, kSigning, 'hex');
// 步骤4:构造Authorization头
const authorization = \`\${algorithm} Credential=\${SECRET_ID}/\${credentialScope}, SignedHeaders=\${signedHeaders}, Signature=\${signature}\`;
// 发送API请求
const response = await fetch(\`https://\${endpoint}\`, {
method: 'POST',
headers: {
'Authorization': authorization,
'Content-Type': 'application/json; charset=utf-8',
'Host': endpoint,
'X-TC-Action': action,
'X-TC-Timestamp': timestamp.toString(),
'X-TC-Version': version,
'X-TC-Region': region
},
body: payload
});
const result = await response.json();
if (!response.ok) {
throw new Error(\`API请求失败: \${JSON.stringify(result.Response)}\`);
}
// 检查失败列表
if (result.Response.FailedList?.length > 0) {
console.error('部分Host刷新失败:', result.Response.FailedList);
process.exit(1);
}
console.log('Hostname刷新成功:', {
JobId: result.Response.JobId,
RequestId: result.Response.RequestId
});
} catch (error) {
// 网络错误处理
if (error.code === 'ENOTFOUND') {
console.error('DNS解析失败,请检查endpoint配置');
}
// 签名错误处理
if (error.message.includes('AuthFailure')) {
console.error('凭证验证失败,请检查SecretId/SecretKey');
}
// 业务逻辑错误
if (error.Response?.Error) {
console.error(\`API错误: [\${error.Response.Error.Code}] \${error.Response.Error.Message}\`);
}
console.error('❌ 刷新操作失败:', error.message);
process.exit(1);
}
}
purgeHostname();
EOF
- name: 执行Hostname缓存刷新
env:
TENCENT_SECRET_ID: ${{ secrets.TENCENT_SECRET_ID }}
TENCENT_SECRET_KEY: ${{ secrets.TENCENT_SECRET_KEY }}
EDGEONE_ZONE_ID: ${{ secrets.EDGEONE_ZONE_ID }}
EDGEONE_HOSTNAMES: ${{ secrets.EDGEONE_HOSTNAMES }}
run: node purge-edgeone-hostname.js
总结
记录了以下自己博客部署的整体流程,最主要还是文章和主题分离的思路以及实际操作中Action的脚本