腾讯云已加入建站黑名单,请不要使用腾讯云,因为平台会不明原因地自动扣费,且平台无法给出解释。 - 20230909

一部分的网站框架都是动态的,以著名的 WordPass (PHP),Typecho (PHP),Python Django 等,这些动态每年都有大量的 CVE(Common Vulnerabilities & Exposures)漏洞,需要时刻保持更新。一些 0Day 级别的漏洞防不胜防。

另一部分网站框架是静态的,比方说 Hexo (Node JS),Gridea (React JS),VuePress (Vue JS) 等,在反复切换后,选择了最原始的 Hexo 作为博客框架。Hexo 会将 Markdown 文件转换为 html,css,js,jpg 等静态文件,在访问网站时,省了生成的这个过程,速度更快。

一句话概括,动态网站有 登录,编辑,发布,评论 等功能,而静态网站只有 阅读。在 Javascript 的加持下,可以通过和“动态” 框架进行联动,实现评论等功能。

静态网站的缺陷是——难以实现准确的权限管理。

章节 简介
静态网站生成器 关于如何配置 Hexo 静态网站生成器,以及一些插件配置
静态网站部署 如何部署静态网站到服务商上,主要用 Actions 进行自动化部署
静态页面托管 如何部署静态网站到服务器,使用免费的服务

懒得看一大长串记录,可以直接跳转到最后面的 Actions 配置文件合辑中查看:

配置 简介
部署到 COS 并刷新 CDN 部署页面 到 存储桶
申请证书并提交到 CDN 配置 CDN 和证书
直接部署到 pages 页面 使用 Github Actions 生成页面

环境参考

  • Windows

本地主机
使用 Linux/Mac 没有区别,仅习惯

  • Ubuntu Server(可选)

89元/年:云主机
在 WSL(Windows Sub Linux) 中也可以

  • 域名(可选)

200元/年(没有钱可以用 Pages 服务提供的子域名)

  • CDN 流量包(可选)

预算 240元/年,包年 184元/年,建议做好流量预算(没有钱可以用 Pages 服务)

静态网站生成器

Hexo 是运行于 Nodejs 上的静态网站生成器,换句话说,就是 Chromium 内核家族下的又一大得力干将。Node.js (nodejs.org)

静态网站生成初体验

熟悉命令行可以直接跳过本节

考虑到我是用过很久的生成器,后面的东西都很熟悉,但一开始接触静态网站生成的肯定会很懵逼,为什么要用 Docker 环境,为什么又一步跳转到 Github Action 里面了,一会儿又手动的,很乱。

实际上这一点也不乱,从一开始,无论是 Docker 还是 Action 都是手动在 Linux 命令行下配置的,Dockerfile 和 Action yaml 都是对命令行操作的极大简化,因此现有手动,后又自动。

这里给出一个简化的 Blog 搭建流程,以减轻理解负担。

WSL 安装

适合 Windows 10 以上系统,Windows 11 系统可正常运行

Win + X 打开“终端管理员”,执行以下命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 启用虚拟化平台
Enable-WindowsOptionalFeature -Online -FeatureName VirtualMachinePlatform

# 启用 Windows 子系统
Enable-WindowsOptionalFeature -Online -FeatureName Microsoft-Windows-Subsystem-Linux

# 启用 WSL 2
wsl --set-default-version 2

# 安装 Ubuntu
wsl --install Ubuntu

# 启动 WSL
wsl

命令行环境准备

首先可以在 Git 的提供网站上注册账户,当然可以自己搭建 Git 服务器

服务商 地址
Github https://github.com/signup
Gitee https://gitee.com/signup

操作系统是 Ubuntu ,命令在 Debian 家族下都是兼容的,推荐使用 WSL 进行操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 这条只适合 Debian
#sed -i 's|deb.debian.org|mirrors.cloud.tencent.com|g' /etc/apt/sources.list

# 这条只适合 Ubuntu
sed -i "s|archive.ubuntu.com|mirrors.cloud.tencent.com|g" /etc/apt/sources.list

# 安装 git
sudo apt-get install git-core

# 配置 Nodejs 18 LTS 源
# ref: https://github.com/nodesource/distributions#rpminstall
curl -fsSL https://deb.nodesource.com/setup_18.x | sudo -E bash -

# 安装 Nodejs
sudo apt-get install -y nodejs

# 配置 Nodejs 镜像
npm config set registry http://mirrors.cloud.tencent.com/npm/

# 安装 Hexo
npm install hexo-cli -g

hexo -v

在 Window 下也能运行,稍微复杂一点,因此推荐使用 Linux

看到提示一堆版本信息就代表成功了:

1
2
hexo-cli: 4.3.1
...

初始化 Blog

接下来是初始化 Blog:

1
2
3
4
5
6
7
8
9
10
mkdir blog && cd blog

# 初始化
hexo init

# 生成 blog
hexo generate

# 运行预览服务器
hexo server

至此,打开 http://localhost:4000/ 就能在浏览器中预览博客的样子了。

输出:

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
> hexo init
INFO Cloning hexo-starter https://github.com/hexojs/hexo-starter.git
INFO Install dependencies
INFO Start blogging with Hexo!

> hexo generate
INFO Validating config
INFO Start processing
INFO Files loaded in 95 ms
INFO Generated: archives/index.html
INFO Generated: index.html
INFO Generated: archives/2023/08/index.html
INFO Generated: fancybox/jquery.fancybox.min.css
INFO Generated: js/script.js
INFO Generated: css/style.css
INFO Generated: archives/2023/index.html
INFO Generated: fancybox/jquery.fancybox.min.js
INFO Generated: js/jquery-3.6.4.min.js
INFO Generated: css/images/banner.jpg
INFO Generated: 2023/08/05/hello-world/index.html
INFO 11 files generated in 110 ms

> hexo server
INFO Validating config
INFO Start processing
INFO Hexo is running at http://localhost:4000/ . Press Ctrl+C to stop.

新建文章

比方说需要新加一篇文章:

1
hexo new new_post

输出:

1
2
3
> hexo new new_post
INFO Validating config
INFO Created: /blog/source/_posts/new-post.md

新建的文件就在 /blog/source/_posts/new-post.md,新手建议使用 nano 来编辑,即:

1
nano /blog/source/_posts/new-post.md

一般在 WSL 里面自带了 VScode,只需要在主机安装 Visual Studio Code 就能直接在主机编辑:

1
code /blog/source/_posts/new-post.md

保存后预览文章:

1
hexo server

使用 git 仓库

1
2
3
4
5
6
7
8
9
10
11
12
# 设置邮箱和用户名,可以直接这样子复制进命令行
git config --global user.email "[email protected]"
git config --global user.name "Your Name"

# 初始化 Git 仓库
git init

# 添加所有文件
git add .

# 提交
git commit -m "My blog."

完毕,整个流程就这么简单。

Hexo-Docker

保留备忘,是在写 Actions 文件中留下的,是是个中间过程。

不会用 Docker 建议跳过本节,没有影响。

准备 Docker 环境

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 更新
sudo apt update

# 安装 Docker
sudo apt install docker.io -y

# 启动 Docker
sudo service docker start

# 推荐使用 Dockerfile 进行,可以一键完成
#docker run \
# -it \
# --name=hexo \
# -p 4000:4000 \
# node \
# /bin/bash
# 如果使用 --rm,每次结束容器运行,都会删除容器

视网络情况而定,有时会特别久。

一般推荐直接安装 Docker Desktop :
Download Docker Desktop | Docker

容器构建

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
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
FROM node:lts-bookworm
# Debian 12 LTS - bookworm

# 配置工具
#RUN sed -i 's|deb.debian.org|mirrors.cloud.tencent.com|g' /etc/apt/sources.list.d/debian.sources && \
RUN sed -i 's|deb.debian.org|mirrors.cloud.tencent.com|g' /etc/apt/sources.list && \
apt-get update && \
apt-get install -y fish vim && \
# 设置默认 Shell 为 Fish
chsh -s /usr/bin/fish

# 配置 npm
RUN npm config set registry http://mirrors.cloud.tencent.com/npm/ && \
npm install hexo -g && \
npm install hexo-admin -g && \
npm install hexo-theme-butterfly -g


# 初始化 hexo 工作目录
RUN mkdir /root/blog && \
cd /root/blog && \
hexo init . && \
git init .

# 初始化工作环境
RUN mkdir ~/.ssh/ && \
echo 'Host github.com' >> ~/.ssh/config && \
echo ' HostName ssh.github.com' >> ~/.ssh/config && \
echo ' User git' >> ~/.ssh/config && \
echo ' Port 443' >> ~/.ssh/config && \
echo "set fileencodings=utf-8,ucs-bom,gb18030,gbk,gb2312,cp936" >> ~/.vimrc && \
echo "set termencoding=utf-8" >> ~/.vimrc && \
echo "set encoding=utf-8" >> ~/.vimrc && \
git clone --depth=1 https://github.com/amix/vimrc.git ~/.vim_runtime && \
sh ~/.vim_runtime/install_awesome_vimrc.sh


WORKDIR /root/blog/

# 安装主题依赖
RUN npm install hexo-renderer-pug --save && \
npm install hexo-renderer-stylus --save && \
npm uninstall hexo-renderer-marked && \
npm uninstall hexo-renderer-kramed && \
npm install hexo-renderer-markdown-it --save && \
npm install katex @renbaoshuo/markdown-it-katex --save &&\
npm install hexo-generator-search --save && \
npm install hexo-wordcount --save && \
npm install hexo-filter-nofollow --save && \
npm install hexo-generator-feed --save && \
npm install hexo-memorial-day --save && \
npm install hexo-images-watermark --save

# 配置主题
RUN rm -f _config.landscape.yml && \
git clone -b master https://github.com/jerryc127/hexo-theme-butterfly.git themes/butterfly && \
cp themes/butterfly/_config.yml _config.butterfly.yml &&\
sed -i 's|landscape|butterfly|g' _config.yml

CMD ["fish"]

然后构建镜像,这里的命令既可以在 PowerShell (Windows) 中执行,也能在 Shell (Linux) 下执行:

1
2
3
4
5
6
7
8
# 需要和 Dockerfile 目录在一个目录下
docker build -t hexo .

docker run -it \
--name=hexo \
-p 4000:4000 \
-v $(pwd)/blog/:/root/blog/
hexo

构建镜像比较久,如果网络比较差,可以去服务器上构建。

可选,这里是可选的,都用了镜像了,网速不慢都很快。

1
2
3
4
5
6
7
8
9
10
11
# 远端导出,带压缩
docker save hexo:latest | gzip > hexo_latest.tar.gz
# 远端导出,无压缩
#docker save -o hexo_latest.tar hexo:latest

# 在本地运行,复制远端文件到本地
scp root@<remote_ip>:~/hexo_latest.tar.gz ~/hexo_latest.tar.gz

# 本地导入 Shell (Linux)/PowerShell (Windows)
docker load < hexo_latest.tar.gz
#docker load --input hexo_latest.tar

服务器一般不限速,并且距离镜像站比较近(或许只有十几米)。

外网 IDC 上构建需要去掉国内镜像,构建速度会更快。

总之,构建完后,需要将将整个镜像拉到本地。

构建环境配置的步骤记录

建议跳过,是上面 Dockerfile 的解释,细节比较繁琐。
如果不喜欢使用 Docker ,或者完全不熟悉,手动安装环境,也不会态复杂:

一开始是手动配置的,习惯性边配置边记录,免得容器配置炸了,可以快速恢复。

配置镜像,准备环境:

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
# 配置镜像 Debian
sed -i 's|deb.debian.org|mirrors.ustc.edu.cn|g' /etc/apt/sources.list
# 有时候是下面这个
# sed -i 's|deb.debian.org|mirrors.ustc.edu.cn|g' /etc/apt/sources.list.d/debian.sources

apt update

apt install -y fish vim
chsh -s /usr/bin/fish
fish

# 设置加速镜像
npm config set registry http://mirrors.cloud.tencent.com/npm/
npm config get registry

# 安装 hexo
npm install hexo -g

# hexo 管理插件
npm install hexo-admin -g

# hexo 推荐的主题
npm install hexo-theme-butterfly -g

# hexo-theme-butterfly 主题的渲染器
npm install hexo-renderer-pug -g
npm install hexo-renderer-stylus -g

cd /root

# 初始化 hexo 工作目录
hexo init blog

cd blog

# 初始化 git 仓库
git init

# 如果一直提示 `kex_exchange_identification: Connection closed by remote host` 是因为代理配置有问题,需要强行到 443 口
cat > ~/.ssh/config <<EOF
Host github.com
HostName ssh.github.com
User git
Port 443
EOF

# 配置 vim 避免乱码
cat > ~/.vimrc <<EOF
set fileencodings=utf-8,ucs-bom,gb18030,gbk,gb2312,cp936
set termencoding=utf-8
set encoding=utf-8
EOF

ls
# _config.landscape.yml _config.yml node_modules package.json scaffolds source themes

# 配置网站
vi _config.yml

# 配置主题

# 删除原主题配置文件
rm _config.landscape.yml

# butterfly 主题
git clone -b master https://github.com/jerryc127/hexo-theme-butterfly.git themes/butterfly

# 应用配置文件
cp themes/butterfly/_config.yml _config.butterfly.yml

这里有个很折磨人的问题,明明 ssh-key 配置对的,可就是报错 kex_exchange_identification: Connection closed by remote host 直到查了祖传笔记,才知道是端口问题,解决方法在上面的脚本里。

配置公钥

在 容器内执行命令:

1
2
ssh-keygen
cat /root/.ssh/id_rsa.pub

或者把外面的密钥复制进去也行:

文件 位置 用途
公钥 cat ~/.ssh/id_rsa.pub 本地保存,远程验证
私钥 cat ~/.ssh/id_rsa 本地保存,不可泄露

然后提交到对应的地方:

远端 地址
Github https://github.com/settings/ssh/new
Gitee https://gitee.com/profile/sshkeys

总之,本地的公钥和私钥要配置对,远端的公钥和私钥也需要填对。

拷贝进入 Linux 需要注意密钥权限问题,比方说遇上 Permissions 0640 for 'xxx/id_rsa' are too open,就需要修正权限:

1
2
chmod 600 ~/.ssh/id_rsa
chmod 600 ~/.ssh/id_rsa.pub

推送到仓库

首先新建仓库:

远端 地址
Github https://github.com/new
Gitee https://gitee.com/projects/new

然后执行命令:

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
# 改成自己的名字,和邮箱
# Git 仓库是公开的话不建议填真实邮箱地址,因为垃圾邮件非常多
git config --global user.email "[email protected]"
git config --global user.name "hxac"

# 添加当前目录下所有文件
git add .

# 提交
git commit -m "First commit."

# 主分支 master,Github 默认为 main,只是命名上区别
git branch -M master

# 添加远端
git remote add origin [email protected]:hxac/blog.git

# 查看已添加远端
#git remote -v

# 如果以后使用,只需要 clone 下来即可,前提是公钥正确
#git clone [email protected]:hxac/blog.git

# 推送到远端
git push -u origin master

注意这里的 user.emailuser.name 需要改成自己的。

如果代理软件配置的不正确,就会出现 kex_exchange_identification: Connection closed by remote host 的错误,这个时候就需要调整 ssh 的配置:

1
2
3
4
5
6
cat >> ~/.ssh/config <<EOF
Host github.com
HostName ssh.github.com
User git
Port 443
EOF
安装 VSCode

可跳过,一般没必要

由于一开始都是那 vim 配置的,考虑到使用 vscode 确实会方便很多,虽然所可以直接用 ssh 直连容器,但这里还是贴上(水)vscode 的安装方法。

直接按照 VSCOE,适合 WSL2,以及带 GUI 环境。

1
2
3
4
5
6
7
8
9
10
11
# GPG 公钥
wget -q https://packages.microsoft.com/keys/microsoft.asc -O- | sudo apt-key add -

# 添加源
echo "deb https://packages.microsoft.com/repos/vscode stable main" >> / etc/apt/sources.list

# 更新源
sudo apt update

# 按照软件包
sudo apt install code

VScode Server,网页版的 VScode,性能稍微差,适合用在不开放 SSH 端口,并且改不了 SSH 端口的环境。

1
2
3
4
5
# 仅运行
#curl -fsSL https://code-server.dev/install.sh | sh -s -- --dry-run

# 安装
curl -fsSL https://code-server.dev/install.sh | sh
VSCode 通过 ssh 挂到远端

只要有 GUI 系统的,都可以,运行 vscode:

1
2
3
# Win + R -> cmd -> Enter
code .
# 这样子是避免打开一个远端的 vscode

左下角蓝色 >< 标识了当前 vscode 运行的地方,如果是远端,请在本地命令行执行 code .

  1. Ctrl + Shift + P 打开命令栏
  2. 输入 Remote-SSH: Add New SSH Host ... ,新建 ssh,这里需要联网下载插件
  3. 选择 C:\User\<your_name>\.ssh\config
  4. 输入远端地址 <user_name>@<ip_address>
  5. Ctrl + Shift + P 打开命令栏
  6. 输入 Remote-SSH: Connect to Host ...,打开远端 ssh
  7. 选择 Linux ,这里依然需要联网,在远端下载对应服务

vscode 不适合在无法联网的环境使用

Hexo 配置

目录结构

Hexo 的目录结构并不复杂,最好在理解 nodejs 原理下,会好理解很多:

1
2
3
4
5
6
7
8
9
10
blog/
|- _config.butterfly.yml // 主题配置(优先级比 _config.yml 高)
|- _config.yml // 网站全局配置
|- node_modules/ // npm 环境(自动)
|- package-lock.json // npm 环境配置(自动)
|- package.json // npm 环境配置(自动)
|- public/ // 生成的静态文件
|- source/ // 文章/页面
|- themes/ // 主题
|- yarn.lock // yarn 环境配置(自动)

只用记住一条: 这里的 source/ 目录下的所有文件,除了一些排除项,以及 .md.markdown 结尾的文件会被 hexo 解析为网页,其他的都会原封不动地复制到 public/ 目录下。

1
2
3
4
source/ 
|- about // 创建的 pages
|- img // 会自动复制到 public
|- _posts // post 文章

这里的 public/ 也就是静态网站的访问目录,换句话说就是需要上传的储存桶的文件。

开始配置

这里可以从 终端命令行 或者 vscode 进行编辑。

Linux

连接到容器进行配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
docker exec -it hexo /usr/bin/fish
#docker exec -it hexo /bin/sh

# 查看所有运行的容器
#docker ps -as

# 理论上可以同步刷新浏览器,但好像没用
#npm install hexo-browsersync --save

#cd /root/blog/

# 已经有仓库,重新拉取:
#cd /root/
#rm /root/blog/ -rf
#git remote add origin [email protected]:hxac/blog.git
#cd /root/blog/
GUI

挂靠到容器进行配置

1
2
code .
docker run -it hexo /usr/bin/fish

一般会提示安装 Docker 插件,直接附加到容器上去即可

配置网站

配置 _config.yml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 网站标题,配置成自己的
title: ''
# 子标题,一般不需要
subtitle: ''
# 描述,给搜索引擎看的
description: ''
# 关键词,给搜索引擎看的
keywords:
# 作者,写上大名
author: hxac
# 语言,填这个
language: zh-cn
# 时区,填这个
timezone: 'Asia/Shanghai'

# Extensions
# Plugins: https://hexo.io/plugins/
# Themes: https://hexo.io/themes/
# 需要启用主题
theme: butterfly

配置主题

配置 _config.butterfly.yml 里面的说明非常详细

具体配置可以参考官方文档:
Butterfly 安裝文檔(三) 主題配置-1 | Butterfly
Butterfly 安裝文檔(四) 主題配置-2 | Butterfly

使用 hexo s 命令就能在 http://localhost:4000/ 上实时预览博客:

1
2
3
4
5
6
7
8
9
10
11
12
13
INFO  Validating config
INFO
===================================================================
##### # # ##### ##### ###### ##### ###### # # #
# # # # # # # # # # # # #
##### # # # # ##### # # ##### # #
# # # # # # # ##### # # #
# # # # # # # # # # # #
##### #### # # ###### # # # ###### #
4.9.0
===================================================================
INFO Start processing
INFO Hexo is running at http://localhost:4000/ . Press Ctrl+C to stop.

创建页面

具体页面创建在官方文档里面有
Butterfly 安裝文檔(五) 主題問答 | Butterfly

基础操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 创建文章
hexo new 'hello world'
#hexo new post 'hello world'

# 创建页面
hexo new page about

# 构建网站,生成到 public
hexo g

# 实时预览服务器
hexo s
# 如果发现是空白页
# 请查看 public 下文件,如果都是 0 字节,请检查主题文件
# 一般是忘记 clone 主题导致的

# 清理所有构建文件(如果有难以解决的 BUG)
hexo clean

一些插件

哀悼日专用
1
npm install hexo-memorial-day --save

添加到 _config.yml

1
2
3
memorial_day:
enable: true
day: 12-13
压缩博客
1
npm install hexo-neat --save
水印
1
npm install hexo-images-watermark --save

添加到 _config.yml

1
2
3
4
5
watermark:
enable: true
textEnable: true
gravity: southeast
text: '0xac.cn'
插件汇总

插件必须在 blog 的目录下安装,同时所有插件必须带 --save 后缀,确保渲染时会被包含。

大部分报错都是因为没有加 --save 后缀。

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
# hexo 所必须的渲染器
npm install hexo-renderer-pug --save
npm install hexo-renderer-stylus --save

# KaTex 不兼容的渲染器
npm uninstall hexo-renderer-marked
npm uninstall hexo-renderer-kramed
# KaTex 需要的渲染器
npm install hexo-renderer-markdown-it --save
npm install katex @renbaoshuo/markdown-it-katex --save

# 本地搜索
npm install hexo-generator-search --save

# 字数统计
npm install hexo-wordcount --save

# SEO 优化,防止外链权重流失
#npm install hexo-filter-nofollow --save

# SSR feeds 订阅生成
npm install hexo-generator-feed --save

# 外链转内链,同 SEO 优化
npm install hexo-filter-links --save

# 水印
npm install hexo-images-watermark --save

# SEO 优化
npm install hexo-submit-urls-to-search-engine --save

换句话说,就是在 _config.yml 一个目录下,执行这些命令。

npm 自带包管理,文件夹下的包,优先服从上级文件夹的依赖。

依赖报错

依赖容易出现各种报错,只有能正常生成文件,就不用管。

落要卸载掉对应的插件,请使用 npm list 查看包名后,用 npm uninstall <package_name> 卸载对应包。

可以参考如下配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
> npm list
[email protected] /home/aero/blog
├── @renbaoshuo/[email protected]
├── [email protected]
├── [email protected]
├── [email protected]
├── [email protected]
├── [email protected]
├── [email protected]
├── [email protected]
├── [email protected]
├── [email protected]
├── [email protected]
├── [email protected]
├── [email protected]
├── [email protected]
├── [email protected]
├── [email protected]
└── [email protected]

常用命令

1
2
3
4
5
6
7
8
9
10
11
# 在全局安装
sudo npm install -g hexo-cli

# 根据本地 package.json 安装
npm install --save

# 根据本地 package.json 升级
npm update --save

# 显示所有插件
npm list
预加载

预加载提升了用户的体验,但在开发过程中,也带来了非常多奇奇怪怪的 BUG。

过程大概是:用户鼠标放到链接上,浏览器会自动预拉取数据,在用户点击后,直接渲染页面。

Hexo Butterfly 采用的是 MoOx/pjax 框架进行预加载,可以在配置文件中开启:

开启预加载:

1
2
3
4
5
6
7
8
# Pjax
# It may contain bugs and unstable, give feedback when you find the bugs.
# https://github.com/MoOx/pjax
pjax:
enable: true
exclude:
# - xxxx
# - xxxx

开启加载动画:

1
2
3
4
5
6
preloader:
enable: true
# source
# 1. fullpage-loading
# 2. pace (progress bar)
source: 2

开启预加载应当开启加载动画,否则在网络比较差的时候,容易被误认为网页卡死了。

框架注入

现有的魔改主题方案基本上是直接修改源码,这种效果自然是立竿见影的,但一旦 博客更新,或者需要应用到别的框架中,就会变得非常复杂。

一部分框架支持 inject 注入,可以实现方便地改变主题。

CCS 注入

比方说想要给导航栏和页脚,加上磨砂玻璃效果,可以通过框架的注入选项,注入对应的 jscss 流程如下:

1
2
3
4
5
6
7
8
9
10
11
cd blog

# 新建文件夹
mkdir source/js
mkdir source/css
# source/ 里面所有的文件夹,除了 .md 和 .Markdown 会被渲染,
# 其他的原封不动复制到发布目录下 public/ 。

# 新建文件
touch source/css/custom.css
touch source/js/custom.js

vi _config.butterfly.yml

1
2
3
4
5
6
7
# Inject
# 插入代码到头部 </head> 之前 和 底部 </body> 之前
inject:
head:
- <link rel="stylesheet" href="/css/custom.css">
bottom:
- <script src="/js/custom.js"></script>

注意这里的 /css/custom.css/js/custom.js 不要填错,或者拼错。

调试可以用 浏览器的 F12 开发者模式 -> 审查元素 进行编辑,编辑完成后直接加入 css 即可。

比方说这里,给导航栏和页脚增加毛玻璃效果。

vi custom.css

1
2
3
4
5
6
7
8
9
10
#footer {
/* 元素注入 */
background: unset;
/* backdrop-filter: blur(20px); */ /* 添加毛玻璃效果,模糊半径为10像素 */
}
/* 导航栏元素注入 */
#page-header.nav-fixed #nav {
background: rgba(255, 255, 255, 0.4);
/* backdrop-filter: blur(20px); */
}

这有一点需要注意的是,注入后如果不想要某种属性,直接删掉是没用的,因为你是注入,东西原本还在哪儿,只能使用 unsetnone 等标记来去掉对应元素。

比方说:

1
background: unset;

或者是:

1
display: none;
Javascript 注入

对于更复杂的效果,需要结合 Javascript 进行注入,比方说想要在不修改主题代码情况下,附加上一些功能,完全可以通过各种函数来实现:

函数 用途
document.getElementById() 元素查找
document.createElement() 元素创建
xxx.appendChild() 元素追加
xxx.remove() 元素删除
xxx.removeChild() 子元素删除

注意,这里还有两个问题:

  • 搜索引擎的站长验证不能注入,因为搜索引擎因为安全问题,都不会渲染 Javascript,采用 Javascript 进行的站长验证是无效的。
  • 若采用了 Pjax 框架实现的页面局部刷新,导致被注入的元素被修改了,但是注入只有一次,因此引入元素后注入就失效了。

对于采用了 Pjax 框架的博客,需要在注入时加 data-pjax 标签,让每个页面在刷新的时候,都重新加载注入一遍 Javascript

_config.yml

1
2
3
4
5
6
7
# Inject
# 插入代码到头部 </head> 之前 和 底部 </body> 之前
inject:
head:
- <link data-pjax rel="stylesheet" href="/css/custom.css">
bottom:
- <script data-pjax src="/js/custom.js"></script>
网页标题注入

比方有时候浏览博客时,只要一切换页面,就会弹出提示:

1
~(>_<)~ 网页崩溃了!

一切回来就变成了:

1
(≧▽≦) 骗你的,又好了。

这是通过事件监听 addEventListener 函数实现的:

1
2
3
document.addEventListener('visibilitychange', func());
window.addEventListener('blur', ()func);
window.addEventListener('focus', ()func);
事件 用途
visibilitychange 元素可见性发生变化
blur 元素失去焦点
focus 元素获得焦点

采用 blurfocuswindow 挂上 addEventListener 事件监听,理论上没问题。

当然,若是使用 visibilitychange 还得配合 document.hidden 才能准确检测当前页面状态。

本来两个事件监听的方法都没什么区别,但遇上预加载框架 Pjax 前者就会出现非常的见鬼的 BUG,参考了前人的实践,用后一个方法都会好些。

当然改变标题这段动作非常简单,也就是修改 document.title 元素,这里采用了 setTimeout() 函数,实现延迟修改:

1
2
3
4
setTimeout(() =>
{
document.title = '~(>_<)~ 网页崩溃了!';
}, 3000);

若采用 Pjax 框架,必须启用 data-pjax 标签,否则修改标题可能出现异常。

核心代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const OriginTitile = document.title; // 原标题

let backTime, leaveTime;
document.addEventListener('visibilitychange', () =>
{
if (document.hidden)
{
document.title = '~(>_<)~ 网页崩溃了!';
}
else
{
clearTimeout(leaveTime);
document.title = '(≧▽≦) 骗你的,又好了。';
backTime = setTimeout(() =>
{
document.title = OriginTitile;
}, 1500);
}
});

考虑到用户体验,加了一点点功能:

  • 反复离开进入标签页三次,就会停止触发彩蛋。
  • 除非用户主动清除浏览器缓存,否则计数器不清零,彩蛋不会重复触发。
  • 只要设置了 globalMourn = true;,在严肃场合会自动停止彩蛋。
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
var globalMourn = false;             // 不开玩笑 
const titleMaxDisplayed = 3; // 最大显示次数
var titleDisplayedCount = 0; // 显示次数计数
const OriginTitile = document.title; // 原标题

let backTime, leaveTime;
document.addEventListener('visibilitychange', () =>
{
if (globalMourn == true)
{
return ;
}

// 只会提示 3 次,
if (localStorage.getItem('titleDisplayedCount') == null ||
localStorage.getItem('titleDisplayedCount') < titleMaxDisplayed)
{
if (document.hidden)
{
clearTimeout(backTime);
leaveTime = setTimeout(() =>
{
document.title = '~(>_<)~ 网页崩溃了!';
}, 3000);

}
else
{
clearTimeout(leaveTime);
document.title = '(≧▽≦) 骗你的,又好了。';
backTime = setTimeout(() =>
{
document.title = OriginTitile;
}, 1500);
localStorage.setItem('titleDisplayedCount', titleDisplayedCount++);
}
}
});
星空背景注入

给主页加星空效果,通过给主页的 page-header 注入 canvas 然后在这个 canvas 上进行绘画,实现稍微复杂。

核心是利用 createElement("canvas") 创建 canvas 层,借助一系列函数进行绘制:

用途 函数
创建 canvas 元素 const starLightCanvas = document.createElement("canvas");
创建 2d 绘画面 const ctx = starLightCanvas.getContext("2d");
设置起始路径 ctx.beginPath();
绘画圆形路径 ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2);
填充颜色 ctx.fillStyle = rgba(211, 211, 211, ${this.opacity});
填充颜色到路径内 ctx.fill();
调用浏览器 API requestAnimationFrame(drawMain);

vi custom.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
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
/*
Title: 星光闪闪 - Star Light Spark
Author: 白芍 <0xac.cn>
Data: 20230803

星光闪闪 - Star Light Spark
让主页背景充满繁星~

This code is licensed under the GNU GPLv3 License <https://www.gnu.org/licenses/gpl-3.0.txt>.
You are free to use it under the condition of compliance with the license.

Copyright 2023 (C) hxac <0xz.co> All rights reserved.
*/
(function()
{
/* 星光闪闪,Star Light Spark */

// 插入 Canvas
const starLightCanvas = document.createElement("canvas");
const page_header = document.getElementById('page-header')
page_header.appendChild(starLightCanvas);

// 创建合适大小画布
const ctx = starLightCanvas.getContext("2d");
starLightCanvas.width = page_header.getBoundingClientRect().width;
starLightCanvas.height = page_header.getBoundingClientRect().height;

// 更新画布大小和对应的值
function handleResize()
{
starLightCanvas.width = page_header.getBoundingClientRect().width;
starLightCanvas.height = page_header.getBoundingClientRect().height;
}

// 监听窗口大小改变事件 -> 更新画布大小和对应的值
window.addEventListener('resize', handleResize);

// 星星
class starLightStar
{
constructor()
{
this.x = 0; // X 坐标
this.y = 0; // Y 坐标
this.radius = Math.random() * 1 + 1; // 半径
this.speed = Math.random() * 0.02 + 0.01; // 闪烁速度
this.opacity = 0; // 不透明度
this.opacityDirection = 1; // 不透明度改变符号
}

// 绘制星星
draw()
{
ctx.beginPath();

// 星星的形状
ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2);

// 星星的颜色
ctx.fillStyle = `rgba(211, 211, 211, ${this.opacity})`;

ctx.fill();
}

// 移动星星
move() {

// 星星画布中心向量 + 偏移量(外围快)
this.x += (this.x - starLightCanvas.width / 2) * 0.004;
this.y += (this.y - starLightCanvas.height / 2) * 0.004;

// 旋转星星,按中心旋转一点点
const radian = 0.001; // 旋转弧度

// 旋转矩阵
const RotateX = this.x * Math.cos(radian) - this.y * Math.sin(radian);
const RotateY = this.x * Math.sin(radian) + this.y * Math.cos(radian);
this.x = RotateX;
this.y = RotateY;

// 闪烁
var opacity = this.opacity + this.opacityDirection * this.speed; // 透明度闪烁
this.opacity = Math.max(0, Math.min(1, opacity)); // 限制透明度范围
this.opacityDirection = this.opacity === 1 ? -1 : 1; // 自动转向

this.draw();

// 出画布星星回中,速度位置重生
if (this.x < 0 || this.x > starLightCanvas.width ||
this.y < 0 || this.y > starLightCanvas.height)
{
this.x = Math.random() * starLightCanvas.width;
this.y = Math.random() * starLightCanvas.height;
this.radius = Math.random() * 1 + 1; // 半径
this.speed = Math.random() * 0.002 + 0.001; // 闪烁速度
}
}
}

// 对象列
const stars = [];
for (let i = 0; i < 1024; i++)
{
stars.push(new starLightStar());
}

// 主函数绘制
function drawMain()
{
// 清除画布
ctx.clearRect(0, 0, starLightCanvas.width, starLightCanvas.height);

// 更新和绘制每颗星星
for (let i = 0; i < stars.length; i++)
{
stars[i].move();
}

// 帧率限制,防止浏览器占用过高
requestAnimationFrame(() =>
{
setTimeout(drawMain, 20);
});
}

// 主函数执行
drawMain();
})();

这个由于采用纯计算实现绘图,会占用浏览器 CPU 资源。

横幅注入

同时注入 cssJavascript 实现一个消息提示的横幅。

虽然说主题已经内置消息提示框了,但缺一个横幅啊,就是那种挂在学校门口,单位楼上的那种,又红又土的横幅土就是战斗力,越土战斗力越强

vi custom.css

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
/* 横幅 */
#banner {
position: fixed;
top: 0;
left: 0;
width: 100%;
opacity: 0;
transform: translateY(-100px);
transition: opacity 0.5s ease, transform 0.5s ease;
display: flex;
align-items: center;
justify-content: center;
font-size: 18px;
transition: opacity 0.5s ease-in-out;
}

#bannerAction {
margin-left: 10px;
cursor: pointer;
font-size: 18px;
color:#d2b42c;
}
/* 下降关键帧 */
@keyframes bounce-down {
0% {
opacity: 0;
transform: translateY(-100px);
}
70% {
opacity: 1;
transform: translateY(20px);
}
100% {
opacity: 1;
transform: translateY(0);
}
}
/* 上升关键帧 */
@keyframes bounce-up {
0% {
opacity: 1;
transform: translateY(0);
}
70% {
opacity: 1;
transform: translateY(20px);
}
100% {
opacity: 0;
transform: translateY(-100px);
}
}

vi custom.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
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
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
/*
Title: 动态横幅
Author: 白芍 <0xac.cn>
Data: 20230804

主页横幅,额外支持
+ JS 动作接入
+ 一天触发一次
+ 定日期触发
+ 全页灰度

用法
var banner = new Banner("id");
document.addEventListener("DOMContentLoaded", banner.show({
text: "这是动态消息内容",
})
);

let banner = new Banner("id");
banner.show({
text: "请注意授权协议",
actionText: "授权协议",
delay: '3000',
onAction: function (element) {
window.open("/licenses/")
},
});
不可以直接将 text 交给用户输入,尤其是 UA,会被注入

| 条目 | 用途 | 默认 | true | false | 说明 |
| ---------- | ------------ | -------- | ---- | ------ | ----------------- |
| text | 显示标题消息 | - | - | - | |
| actionText | 关闭按钮 | X | X | - | |
| onAction | 关闭按钮 | true | 关闭 | - | 自定义动作 |
| background | 背景颜色 | 红 | - | - | |
| color | 文字颜色 | 白 | - | - | |
| height | 横幅高 | 40px | - | - | |
| delay | 显示时长 ms | 4000ms | - | 不关闭 | |
| date | 显示日期 | 不开启 | - | - | 仅支持 日月 09-08 |
| once | 一天一次 | 一天一次 | 一次 | 每次 | |
| mourn | 全屏灰色默哀 | 禁用 | 启用 | 关闭 | |

This code is licensed under the GNU GPLv3 License <https://www.gnu.org/licenses/gpl-3.0.txt>.
You are free to use it under the condition of compliance with the license.

Copyright 2023 (C) hxac <0xz.co> All rights reserved.
*/

class Banner {
constructor(id)
{
// 主挂靠
if (document.getElementById(id) === null)
{
return;
}
/*
<... id="id">
<div id="banner">
<span id="bannerMessage"></span>
<span id="bannerAction"></span>
</div>
</...>
*/

this.parentElement = document.getElementById(id);

this.banner = document.createElement("div");
this.banner.id = "banner";

this.bannerMessage = document.createElement("span");
this.bannerMessage.id = "bannerMessage";
this.bannerAction = document.createElement("span");
this.bannerAction.id = "bannerAction";

this.banner.appendChild(this.bannerMessage);
this.banner.appendChild(this.bannerAction);

this.parentElement.appendChild(this.banner);

}

// 设置元素
setElement()
{
//this.bannerMessage.textContent = this.actionText; // 自动转义,不会注入
// 有注入风险,不要接收用户输入,非要接受请转义,包括浏览器 User-Agent
this.bannerMessage.innerHTML = this.text;
this.bannerAction.innerHTML = this.actionText;

// 横幅颜色
this.banner.style.background = `linear-gradient(to top, ${this.background}, ${this.background})`;
// 文字颜色
this.banner.style.color = this.color;
// 横幅高度
this.banner.style.height = this.height + 'px';
}

// 显示过了?
isShown()
{
const storedData = localStorage.getItem("banner_shown");
if (storedData == null)
{
return false;
}

const parsedData = JSON.parse(storedData);
if (parsedData === undefined)
{
return false;
}

const currentTime = Date.now();
const expirationTime = parsedData.time + parsedData.expire * 1000;
if(currentTime < expirationTime)
{
if (parsedData.data == '1')
{
return true;
}
}
return false;
}

// 设置显示过了
setShown()
{ // 有效期 1天
localStorage.setItem("banner_shown",
JSON.stringify({
data: 1,
time: Date.now(),
expire: 24 * 3600,
})
);
}

// 不符合日期?
isnotDate()
{ // 日期格式 月-日(08-15)
const currentDate = new Date();
const currentMonth = currentDate.getMonth() + 1; // 月份从 0 开始 +1
const currentDay = currentDate.getDate();
const dateParts = this.date.split('-');

const targetMonth = parseInt(dateParts[0]);
const targetDay = parseInt(dateParts[1]);
if (currentMonth === targetMonth && currentDay === targetDay)
{
return false;
}
return true;
}

// 显示
displayBanner()
{
this.banner.style.animation = "bounce-down 0.5s ease forwards";

setTimeout(() => {
this.banner.style.animation = "unset;";
}, 500);
}

// 隐藏
hiddenBanner()
{
this.banner.style.animation = "bounce-up 0.5s ease forwards";

setTimeout(() => {
this.banner.style.animation = "unset;";
}, 500);
}

// 启用灰度滤镜
applyFilter()
{
const htmlElement = document.querySelector("html");
htmlElement.style.mozFilter = "grayscale(100%)";
htmlElement.style.msFilter = "grayscale(100%)";
htmlElement.style.oFilter = "grayscale(100%)";
htmlElement.style.filter = "grayscale(100%)";
}

// 关闭灰度滤镜
resetFilter()
{
const htmlElement = document.querySelector("html");
htmlElement.style.mozFilter = "grayscale(0%)";
htmlElement.style.msFilter = "grayscale(0%)";
htmlElement.style.oFilter = "grayscale(0%)";
htmlElement.style.filter = "grayscale(0%)";
}

// 显示传参
show(options)
{
this.text = options.text; // 标题消息
this.actionText = !(options.actionText === undefined) ? options.actionText : '&#x2716;'; // 关闭按钮 X: true | 文本 :''
this.onAction = !(options.onAction === undefined) ? options.onAction : false; // 隐藏横幅动作: true | 自定义动作 :''
this.background = !(options.background === undefined) ? options.background : '#c04851'; // 背景颜色
this.color = !(options.color === undefined) ? options.color : '#d8e3e7'; // 文字颜色
this.height = !(options.height === undefined) ? options.height : 50; // 文字颜色
this.delay = !(options.delay === undefined) ? options.delay : 4000 ; // 显示时长 ms
this.date = !(options.date === undefined) ? options.date : false; // 显示日期
this.once = !(options.once === undefined) ? options.once : false; // 一天一次: true | 每次都是: false
this.mourn = !(options.mourn === undefined) ? options.mourn : false; // 全屏灰色默哀

// 显示一次
if (this.once && this.isShown())
{
return;
}

// 日期判别
if (this.date && this.isnotDate())
{
return;
}

// 全屏灰色
if (this.mourn)
{
globalMourn = true; // 联动其他模块
this.applyFilter();
}

// 创建元素
this.setElement();

// 显示
this.displayBanner();

// 延时消失
if (this.delay != false)
{
setTimeout(() =>
{
this.hiddenBanner();
}, this.delay
);
}

// 按钮消失
this.bannerAction.addEventListener("click", () =>
{
if (this.onAction == false)
{
this.hiddenBanner();
}
else
{
this.onAction();
this.hiddenBanner();
}
});

// 显示一次
if (this.once)
{
this.setShown();
}

}
}

用起来非常简单:

1
2
3
4
5
var banner = new Banner("id");
document.addEventListener("DOMContentLoaded", banner.show({
text: "这是动态消息内容",
})
);

务必注意,textactionText 绝对不能交给用户,需要处理注入问题。

条目 用途 默认 true false 说明
text 显示标题消息 - - -
actionText 关闭按钮 X X -
onAction 关闭按钮 true 关闭 - 自定义动作
background 背景颜色 - -
color 文字颜色 - -
height 横幅高 40px - -
delay 显示时长 ms 4000ms - 不关闭
date 显示日期 不开启 - - 仅支持 日月 09-08
once 一天一次 一天一次 一次 每次
mourn 全屏灰色默哀 禁用 启用 关闭

二次展开

二次展开可以省略很多代码,由于这三个文件的存在,可以准确地帮助 nodejs 配置好 hexo 所需要的依赖:

文件名 用途
package-lock.json 锁定各个包的版本号
package.json 项目数据挖掘
db.json hexo 框架配置

这几个是不需要的目录,默认在 .gitignore 进行了忽略。

目录 用途
node_modules/ npm 包缓存
theme/ 主题缓存,构建时从 git 拉去
public/ 构建结果,hexo s 构建即可

快速展开环境

这里默认配置好了 git

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 私有的 blog 仓库
git clone [email protected]:hxac/blog.git

# hexo 命令行
sudo npm install hexo-cli -g

# 下载主题
git clone https://github.com/jerryc127/hexo-theme-butterfly.git themes/butterfly

# 准备依赖包
npm install

# 升级依赖包
#npm update

# 清理上此构建
hexo clean

# 生成构建
hexo generate

# 临时预览
hexo server

推送优化

SEO - Search Engine Optimization 搜索引擎优化,理论上可以提高搜索引擎的索引排名。

调整 url

1
2
3
4
5
6
7
8
# URL
# Set your site url here. For example, if you use GitHub Page, set url as 'https://username.github.io/project'
url: https://0xac.cn
permalink: :year/:month/:title/
permalink_defaults:
pretty_urls:
trailing_index: false # Set to false to remove trailing 'index.html' from permalinks
trailing_html: false # Set to false to remove trailing '.html' from permalinks

这里的 url 需要填对,避免在生成页面时产生大量点不开的死链接。url 的默认是 yoursite

添加 RSS 订阅

RSS - Really Simple Syndication(信息聚合)

1
npm install hexo-generator-feed --save

_config.yml

1
2
3
4
5
6
7
8
9
10
11
feed:
# Generate atom feed
type: atom

# Generate both atom and rss2 feeds
type:
- atom
- rss2
path:
- atom.xml
- rss2.xml

Sitemap

这个 sitemap.xml 是用来指导搜索引擎可爬取页面。

1
2
3
4
npm install hexo-generator-sitemap --save
# 生成 `sitemap.xml`
npm install hexo-generator-baidu-sitemap --save
# 生成 `baidusitemap.xml`

设置 robots.txt

这个 robots.txt 是用来指导网络爬虫工作的文件,一些搜索引擎可能不会遵循这个文件。

1
2
3
4
5
6
7
8
User-agent: *   
Allow: /
Disallow: /js/
Disallow: /css/
Disallow: /img/

Disallow: /tags/
Sitemap: https://0xac.cn/sitemap.xml

这里的 https://0xac.cn/sitemap.xml 需要填写为自己实际的网址

链接优化

一般需要在链接添加 rel="external nofollow" 标识,防止权重流失:

1
npm install hexo-filter-links --save
1
2
3
4
5
6
7
# hexo-filter-links

# 外链转内链
links:
  enable: true
  field: "site"
  exclude:

静态网站部署

域名准备

没有钱可以用 Pages 服务,不需要准备域名都行,建议直接跳过本节。

常见域名后缀

后缀 价格 RMB 注释
.cn 39 便宜
.com 89 中等
.co 169
.org 98 组织,无法备案
.me 120 只对五眼联盟 英美加澳 四国学生免费
.io 259 巨贵

域名注册商比较多,这里随便列举一部分:

免费域名

目前只有 https://www.freenom.com/ 提供的 5 个免费后缀

1
.tk, .ml, .ga, .cf, .gq

二级域名有一定风险。

证书准备

推荐采用 Let’s Encrypt 的证书 https://letsencrypt.org/ 是免费的。

这里采用 DNS 验证,简单快捷,采用服务器验证经常遇到各种奇怪的故障(比方说折腾一个下午,发现是防火墙端口没用放行,而且是云服务商和主机两道防火墙都没用放行)。

准备好域名 <you_domain>,以及一台 Linux 主机:

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
# 使用 acme.sh 便捷申请证书
git clone https://github.com/acmesh-official/acme.sh

cd acme.sh

# 设置默认证书服务器
./acme.sh --set-default-ca --server letsencrypt

# 设置请求域名(仅一个,可以是泛域名)
export RQ_DOMAIN=0xac.cn

# 设置 DNS 鉴权(这条命令不会成功,可以直接用下一条手动进行)
./acme.sh --issue --dns -d ${RQ_DOMAIN}

# 进行 DNS 鉴权请求
./acme.sh --issue --dns -d $RQ_DOMAIN--yes-I-know-dns-manual-mode-enough-go-ahead-please

# 这里要记下 DNS 的鉴权 TXT 记录
# 是一个 `_acme-challenge.<you_domain>` 形式的
# 注意 TTL 不易设置过长,避免操作失误

# 检查 DNS 解析状态,只有解析出来了,才能进行下一步
dig +nocmd txt +noall +answer _acme-challenge.$RQ_DOMAIN

# 申请证书
./acme.sh --renew -d $RQ_DOMAIN --yes-I-know-dns-manual-mode-enough-go-ahead-please

# 证书
cat "${HOME}/.acme.sh/${RQ_DOMAIN}_ecc/${RQ_DOMAIN}.cer"

# 证书链(一般用这个)
cat "${HOME}/.acme.sh/${RQ_DOMAIN}_ecc/fullchain.cer"

# 私钥(不可公开)
cat "${HOME}/.acme.sh/${RQ_DOMAIN}_ecc/${RQ_DOMAIN}.key"

# 证书订单
#cat "${HOME}/.acme.sh/${RQ_DOMAIN}_ecc/${RQ_DOMAIN}.conf"

# 续期
./acme.sh --issue -d $RQ_DOMAIN --dns --yes-I-know-dns-manual-mode-enough-go-ahead-please

# 重新签发会用一样的 DNS 认证密钥,所以不用删掉 DNS 认证钥(别个拿了也没用)

API 请求会方便一些,脚本会全自动进行一切必要验证,详细可见官方文档:
dnsapi · acmesh-official/acme.sh Wiki (github.com)

比方说采用 DNSPod 的话,可以在 DNSPod(不是腾讯云)上申请 API 密钥,然后填入执行

1
2
3
4
5
6
7
8
9
10
11
12
# 仅适合 DNSPod
export DP_Id="id"
export DP_Key="key"

# 指定 DNSPod 作为 DNS
acme.sh --issue --dns dns_dp -d <you_domain>

# 重新签发
acme.sh --issue --renew -d <you_domain> --dns

# 续期
acme.sh --issue -d <you_domain> --dns

注意这里的重新签发并不会吊销旧证书。

虽然说 DNSPod 是腾讯旗下的产品,在 腾讯云管理 中也能配置 DNS,但 API 密钥却得去 DNSPod 申请。

一般 云服务器管理 中都有界面上传密钥,可以手动操作。后面会采用 Actions 全自动申请推送密钥。

完整配置可见后面的 [Actions 合辑

请务必在本地验证成功后,再采用 Actions 配置,证书申请有次数限制。

CDN 准备

CDN 按照流量计费,一般而言 CDN 流量都有免费额度,普遍在 50GB 以下是免费的。

流量计费很贵,建议买流量包(新站流量不大,不用买

供应商 链接 价格 备注
华为云 https://www.huaweicloud.com 240
阿里云 https://www.aliyun.com/ 240+36
腾讯云 https://www.cloud.tencent.com 306 按天计费,每月 120GB
七牛云 https://www.qiniu.com/ 336
又拍云 www.upyun.com 435 CDN+对象储存+HTTPS,每月 120GB

对象储存 准备

对象储存流入是免费的,但流出需要计费,一般接在 CDN 后端,都只用流出一次给 CDN 缓存,只要网站更新得不是很频繁,消耗流量都非常低。

下面的 对象储存 的流量价格按照 10GB,10GB CDN 流出,每年计费计算。

供应商 链接 价格
华为云 https://www.huaweicloud.com 18.9
阿里云 https://www.aliyun.com/ 32.4
腾讯云 https://www.cloud.tencent.com 32.16
七牛云 https://www.qiniu.com/ 计入 CDN 流量

实际上域名可以直接解析到 对象储存上,但 对象储存 的流出流量非常贵。

Action 自动构建

如果对自己的代码能力有充分信心,可以用以下 Github Action 脚本进行自动化构建。

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
name: Deploy Blog

on:
push:
branches: [ "master" ]

# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:

env:
TZ: Asia/Shanghai

jobs:
generate-pages:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3

- name: Use Node.js 18
uses: actions/setup-node@v3
with:
node-version: 18
cache: 'npm'

- name: Cache node modules
uses: actions/cache@v3
id: cache
with:
path: node_modules
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-

- name: Prepare Hexo
run: |
npm install hexo-cli -g

- name: Prepare Themes
run: |
mkdir -p blog/themes
git clone -b master https://github.com/jerryc127/hexo-theme-butterfly.git themes/butterfly

- name: Prepare Blog
if: steps.cache.outputs.cache-hit != 'true'
run: npm install

- name: Generate Blog
run: |
hexo clean
hexo generate

- name: Upload generated public pages
uses: actions/upload-artifact@v3
with:
name: public_pages
path: ./public

当然,这里只是生成了构建文件,需要送到 对象存储筒 上,需要用到一些命令行工具。

完整配置可见后面的 [Actions 合辑](#Actions 合辑)

对象储存上传

首先需要在云服务商申请密钥,确保有对储存桶的全读写权限:

后续会有全自动证书配置,因此也建议一并申请这些权限:

权限 说明
存储桶全读写 上传静态网页到存储桶
CDN ,刷新、配置 配置证书,上传证书
DNS 的新增,删除解析 申请证书时的 DNS 验证

特别注意,对于腾讯云 DNS,不是在控制台申请,而是在 DNSPOD 上申请。

1
2
3
4
COS_SECRET_ID=yourSecretId
COS_SECRET_KEY=yourSecretKey
COS_BUCKET=yourBucket
COS_REGION=yourRegion

注意这里必须使用 Secerts 以防止密钥泄露,绝对不能用 Variables,Variables 会将变量暴露在命令行膻中,非常危险。

暴露密钥基本上等同于送免费网站给别人,特别是已备案网站,还可能被利用做一些违法的事情。

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
upload-store:
runs-on: ubuntu-latest
needs: compress-pages
steps:
- uses: actions/checkout@v3
- name: Download compressed public pages
uses: actions/download-artifact@v3
with:
name: public_pages
path: ./public

- name: Install COS Tool
run: |
wget https://github.com/tencentyun/coscli/releases/download/v0.13.0-beta/coscli-linux
mv coscli-linux coscli
chmod 755 coscli
./coscli --version
touch ~/.cos.yaml

#- name: Compressed Files
# run: |
# tar -zcvf blog.tgz -C public .

- name: Depoly blog
run: |
./coscli config add -b ${{ secrets.COS_BUCKET }} -r ${{ secrets.TENCENTCLOUD_REGION }} -a public-page
./coscli config set --secret_key ${{ secrets.TENCENTCLOUD_SECRET_KEY }} --secret_id ${{ secrets.TENCENTCLOUD_SECRET_ID }}
./coscli rm cos://public-page/ -rf
./coscli cp public/ cos://public-page/
./coscli du cos://public-page/

tar 压缩文件的解压需要服务商的支持,部分服务商不支持

服务商都有自己的对象储存上传工具,后面给出了国内主流服务商的参考命令。

这里采用的是删掉旧文件,重新上传新文件,避免旧文件不删除引发安全问题。

完整配置可见后面的 [Actions 合辑](#Actions 合辑)

腾讯云对象储存

对象存储 COSCLI - 腾讯云 (tencent.com)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 下载工具
#https://github.com/tencentyun/coscli/releases
wget https://github.com/tencentyun/coscli/releases/download/v0.13.0-beta/coscli-linux
mv coscli-linux coscli
chmod 755 coscli
./coscli --version
touch ~/.cos.yaml

# COS 操作
./coscli config add -b ${COS_BUCKET} -r ${COS_REGION} -a public-page
./coscli config set --secret_key ${COS_SECRET_KEY} --secret_id ${secrets.COS_SECRET_ID}
./coscli rm cos://public-page/ -rf
./coscli cp public/ cos://public-page/ -r
./coscli du cos://public-page/

华为云对象储存

这些命令没有验证过

对象存储服务 OBS_obsutil_华为云 (huaweicloud.com)

1
2
3
4
5
6
7
8
9
10
11
12
# 下载工具
wget https://obs-community.obs.cn-north-1.myhuaweicloud.com/obsutil/current/obsutil_linux_amd64.tar.gz
tar -xzvf obsutil_linux_amd64.tar.gz
cp obsutil_linux_amd64_*/obsutil obsutil
chmod 755 obsutil
./obsutil version

# OBS 操作
./obsutil config -i=${OBS_AK} -k=${OBS_SK} -e=${OBS_ENDPOINT}
./obsutil rm obs://${OBS_BUCKET_NAME}/ -r -f
./obsutil cp public/ obs://${OBS_BUCKET_NAME}/ -r -f -flat
./obsutil stat obs://${OBS_BUCKET_NAME}/

阿里云对象储存

这些命令也没有验证过

对象存储服务 OOS - 安装 ossutil (aliyun.com)

1
2
3
4
5
6
7
8
9
10
11
12
13
# 下载工具
#https://github.com/aliyun/ossutil/releases
wget https://github.com/aliyun/ossutil/releases/download/v1.7.16/ossutil-v1.7.16-linux-amd64.zip
unzip ossutil-*.zip
cp ossutil-*/ossutil-*/ossutil64 ossutil64
chmod 755 ossutil64
./ossutil64 update

# OOS 操作
./ossutil64 config -i=${OOS_AK} -k=${OOS_SK} -e=${OOS_ENDPOINT}
./obsutil rm oss://${OOS_BUCKET_NAME}/ -r -f
./ossutil64 cp -r public/ oss://${OOS_BUCKET_NAME}/
./ossutil64 du oss://${OOS_BUCKET_NAME}/

末端部署全自动

截至目前,只实现了网页生成和推送的全自动化。而证书生成,和 CDN 预热刷新都需要手动进行。

下面将会配合各大厂商的 cli 工具,彻底实现真正的全自动方案。

各个云服务厂商的接口不统一,特别是某些厂商,DNS 和 CDN 解析的 API 要到两个子站上申请,特别麻烦。某些厂商 API 文档写得跟捉迷藏似的,非常难找。

这里列举了一些厂商的命令行工具进行自动化操作:

厂商 地址
腾讯云 tencentcloud-cli
阿里云 aliyun-cli
华为云 huaweicloud-hcli

由于成本问题,实现全自动化的只有腾讯云,理论上其他厂商,都能实现全自动化部署崩了别打我,找文档

阿里云全自动化

未验证

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
# 设置请求域名
export RQ_DOMAIN="0xac.cn"

# 需要自行申请 api key
export Ali_Key="<key>"
export Ali_Secret="<secret>"
export REGION="ap-guangzhou "

# 下载 acme.sh 仓库
git clone https://github.com/acmesh-official/acme.sh

# 设置默认 CA 为 Let's Encrypt
./acme.sh --set-default-ca --server letsencrypt

# 申请泛域名证书
./acme.sh --issue -d "${RQ_DOMAIN}" -d "*.${RQ_DOMAIN}" --dns dns_ali
cd acme.sh

# 获取公钥
public_key="$(cat $HOME/.acme.sh/${RQ_DOMAIN}_ecc/fullchain.cer)"
private_key="$(cat $HOME/.acme.sh/${RQ_DOMAIN}_ecc/${RQ_DOMAIN}.key)"
# 随便生成不重复名字
private_name="$(cat $HOME/.acme.sh/${RQ_DOMAIN}_ecc/fullchain.cer | md5sum)"

# 安装 阿里云 cli 工具
#ref: https://github.com/aliyun/aliyun-cli/releases
wget https://github.com/aliyun/aliyun-cli/releases/download/v3.0.170/aliyun-cli-linux-3.0.170-amd64.tgz

tar xzvf aliyun-cli-linux-*-amd64.tgz

chmod 755 aliyun

# 配置
aliyun configure set --profile akProfile --region "${REGION}" --access-key-id "${Ali_Secret}" --access-key-secret "${Ali_Key}"

# 上传证书
aliyun cas UploadUserCertificate --region cn-hangzhou --Cert "${public_key}" --Key "${private_key}" --Name "${private_name}" --version 2020-04-07 --force

# 更新证书
aliyun cdn SetCdnDomainSSLCertificate --region "${REGION}" --DomainName "${RQ_DOMAIN}" --SSLProtocol on --CertName "${private_name}"

# 刷新 CDN
aliyun dcdn RefreshDcdnObjectCaches --region "${REGION}" --ObjectPath 'https://${RQ_DOMAIN}/'

需要调试 API 可以在:
阿里云OpenAPI开发者门户 (aliyun.com)

没有验证,就不给出对应的 Actions 工作流配置了。

华为云全自动化

未验证

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
#ref: https://support.huaweicloud.com/qs-hcli/hcli_02_003.html
# 设置请求域名
export RQ_DOMAIN="0xac.cn"

# 需要自行申请 api key
export HUAWEICLOUD_Username="<Your IAM Username>"
export HUAWEICLOUD_Password="<Your Password>"
export HUAWEICLOUD_DomainName="<Your DomainName>" # 是帐号名
export HW_TOKEN=<cli-x-auth-token>
#ref: [参数说明_统一身份认证服务 IAM_API参考_使用前必读_华为云 (huaweicloud.com)](https://support.huaweicloud.com/api-iam/iam_01_0006.html)
export REGION="cn-north-4"

# 下载 acme.sh 仓库
git clone https://github.com/acmesh-official/acme.sh

# 设置默认 CA 为 Let's Encrypt
./acme.sh --set-default-ca --server letsencrypt

# 申请泛域名证书
./acme.sh --issue -d "${RQ_DOMAIN}" -d "*.${RQ_DOMAIN}" --dns dns_huaweicloud
cd acme.sh

# 获取公钥
public_key="$(cat $HOME/.acme.sh/${RQ_DOMAIN}_ecc/fullchain.cer)"
private_key="$(cat $HOME/.acme.sh/${RQ_DOMAIN}_ecc/${RQ_DOMAIN}.key)"
# 随便生成不重复名字
cert_name="$(cat $HOME/.acme.sh/${RQ_DOMAIN}_ecc/fullchain.cer | md5sum)"

# 安装 阿里云 cli 工具
#ref: https://github.com/aliyun/aliyun-cli/releases
wget https://github.com/aliyun/aliyun-cli/releases/download/v3.0.170/aliyun-cli-linux-3.0.170-amd64.tgz

tar xzvf aliyun-cli-linux-*-amd64.tgz

chmod 755 aliyun

# 配置
hcloud configure set --cli-profile=hwToken --cli-mode=token --cli-region="${REGION}"--cli-x-auth-token="${HW_TOKEN}" --cli-domain-id="${HUAWEICLOUD_DomainName}"

# 上传证书
hcloud SCM ImportCertificate/v3 --cli-region="${REGION}" --name="${private_name}" --certificate="${public_key}" --private_key="${private_key}"

# 更新证书
hcloud CDN UpdateDomainMultiCertificates/v1 --cli-region="${REGION}" --https.domain_name="${RQ_DOMAIN}" --https.cert_name="${cert_name}" --https.https_switch=1 --https.force_redirect_config.switch=1

# 刷新 CDN
hcloud CDN CreatePreheatingTasks/v1 --cli-region="${REGION}" --preheating_task.urls.1="https://${RQ_DOMAIN}/"

需要调试 API 可以在:
API Explorer (huaweicloud.com)

腾讯云全自动化

这里是在本地部署的脚本,作为参考:

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
# 设置请求域名
export RQ_DOMAIN="0xac.cn"

# 需要自行申请 api key,这里是 DNSPod.cn 上的,不是腾讯云上的
export DP_Id="123456"
export DP_Key="8921sdfjo237o4274h9387xxxxx"

# # 需要自行申请 api key,这里是腾讯云上的,记得开放 COS 和 CDN 的全读写权限
export TENCENTCLOUD_SECRET_ID="xxxxxxxxxx"
export TENCENTCLOUD_SECRET_KEY="xxxxxxxxxx"
export TENCENTCLOUD_REGION="ap-xxxxxxxxx"

# 下载 acme.sh 仓库
git clone https://github.com/acmesh-official/acme.sh

# 设置默认 CA 为 Let's Encrypt
./acme.sh --set-default-ca --server letsencrypt

# 申请泛域名证书
./acme.sh --issue -d "${RQ_DOMAIN}" -d "*.${RQ_DOMAIN}" --dns dns_dp

# 获取公钥
PUBLIC_KEY="$(cat $HOME/.acme.sh/${RQ_DOMAIN}_ecc/fullchain.cer)"
PRIVATE_KEY="$(cat $HOME/.acme.sh/${RQ_DOMAIN}_ecc/${RQ_DOMAIN}.key)"

echo export PUBLIC_KEY="\"$(cat ${HOME}/.acme.sh/${RQ_DOMAIN}_ecc/${RQ_DOMAIN}.key)\"" >.env
echo export PRIVATE_KEY="\"$(cat $HOME/.acme.sh/${RQ_DOMAIN}_ecc/${RQ_DOMAIN}.key)\"" >>.env
# 安装 腾讯云 cli 工具
#docker run --rm -it tencentcom/tencentcloud-cli --version
pip install tccli

# 配置
#touch ~/.tccli/.default.configure
#touch ~/.tccli/default.credential
#tccli configure set secretId "${SECRET_ID}" --profile test
#tccli configure set secretKey "${SECRET_KEY}" --profile test
#tccli configure set region "${REGION}" output json language zh-CN --profile test

# 导入 API 到环境变量
echo export TENCENTCLOUD_SECRET_ID="\"***\"" >> .env
echo export TENCENTCLOUD_SECRET_KEY="\"***\"" >> .env
echo export TENCENTCLOUD_REGION="\"***\"" >> .env

# 上传证书
source .env
echo export CERT_ID=$(tccli ssl UploadCertificate \
--cli-unfold-argument \
--CertificatePublicKey "${PUBLIC_KEY}" \
--CertificatePrivateKey "${PRIVATE_KEY}" \
--filter CertificateId) >> .env

# 更新证书
source .env
tccli cdn UpdateDomainConfig \
--cli-unfold-argument \
--Domain "${RQ_DOMAIN}" \
--Https.Switch on \
--Https.CertInfo.CertId "${CERT_ID}"

# 刷新 CDN
tccli cdn PurgePathCache \
--cli-unfold-argument \
--Paths "https://${RQ_DOMAIN}/" \
--FlushType flush

需要调试 API 可以在:
API Explorer - 云 API - 控制台 (tencent.com)

然后在本地可以正常运行上述脚本时,转写成 Actions 工作流。这里分为两个部分,一个是刷新 CDN 预热

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
reflush-cdn:
runs-on: ubuntu-latest
needs: upload-store
steps:
- uses: actions/checkout@v3
- uses: actions/setup-python@v4
with:
python-version: '3.9'

- name: Install Cloud Service Cli
run: |
pip install tccli
sleep 20

- name: Load Tencent Cloud API
run: |
echo export TENCENTCLOUD_SECRET_ID="\"${{ secrets.TENCENTCLOUD_SECRET_ID }}\"" > .env
echo export TENCENTCLOUD_SECRET_KEY="\"${{ secrets.TENCENTCLOUD_SECRET_KEY }}\"" >> .env
echo export TENCENTCLOUD_REGION="\"${{ secrets.TENCENTCLOUD_REGION }}\"" >> .env

- name: reflush CDN
run: |
source .env
tccli cdn PurgePathCache --cli-unfold-argument --Paths "https://${{ secrets.RQ_DOMAIN }}/" --FlushType flush

另一部分是证书自动申请,和提交,配置

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
name: Generate Certificates

on:
push:
branches-ignore: [master]
schedule:
- cron: "1 1 1 * 1"
# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:
jobs:
generate-Key:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-python@v4

- name: Clone ACME.SH
run: |
curl https://get.acme.sh | sh

- name: Config ACME.sh
if: ${{ env.ACME == 1 }}
run: |
echo "DEFAULT_ACME_SERVER='https://acme-v02.api.letsencrypt.org/directory'" > /home/runner/.acme.sh/account.conf
echo "SAVED_DP_Id='${{ secrets.DP_ID }}'" >> /home/runner/.acme.sh/account.conf
echo "SAVED_DP_Key='${{ secrets.DP_KEY }}'" >> /home/runner/.acme.sh/account.conf

- name: Request Let's Encrypt for issue certificates
if: ${{ env.ACME == 1 }}
run: |
/home/runner/.acme.sh/acme.sh --issue -d "${{ secrets.RQ_DOMAIN }}" -d "*.${{ secrets.RQ_DOMAIN }}" --dns dns_dp

- name: Load certificates and key from ACME.SH
if: ${{ env.ACME == 1 }}
run: |
echo export PUBLIC_KEY="\"$(cat /home/runner/.acme.sh/${RQ_DOMAIN}_ecc/${RQ_DOMAIN}.key)\"" > .env
echo export PUBLIC_KEY="\"$(cat /home/runner/.acme.sh/${RQ_DOMAIN}_ecc/${RQ_DOMAIN}.key)\"" >> .env

- name: Load certificates and key from ENV
if: ${{ env.ACME == 0 }}
run: |
echo PUBLIC_KEY="\"${{ secrets.PUBLIC_KEY }}\"" > .env
echo PRIVATE_KEY="\"${{ secrets.PRIVATE_KEY }}\"" >> .env

- name: Install Tencent Cloud CLi
run: |
pip install tccli

- name: Load Tencent Cloud API
run: |
echo export TENCENTCLOUD_SECRET_ID="\"${{ secrets.TENCENTCLOUD_SECRET_ID }}\"" >> .env
echo export TENCENTCLOUD_SECRET_KEY="\"${{ secrets.TENCENTCLOUD_SECRET_KEY }}\"" >> .env
echo export TENCENTCLOUD_REGION="\"${{ secrets.TENCENTCLOUD_REGION }}\"" >> .env

- name: Upload public key and private key
run: |
source .env
echo export CERT_ID="\"$(tccli ssl UploadCertificate --cli-unfold-argument --CertificatePublicKey "${PUBLIC_KEY}" --CertificatePrivateKey "${PRIVATE_KEY}" --filter CertificateId)"\" >> .env

- name: Update public key and private key to CDN
run: |
source .env
tccli cdn UpdateDomainConfig --cli-unfold-argument --Domain "${{ secrets.RQ_DOMAIN }}" --Https.Switch on --Https.CertInfo.CertId "${CERT_ID}"
tccli cdn UpdateDomainConfig --cli-unfold-argument --Domain "api.${{ secrets.RQ_DOMAIN }}" --Https.Switch on --Https.CertInfo.CertId "${CERT_ID}"

完整配置可见后面的 [Actions 合辑](#Actions 合辑)

压缩网页

压缩网页放到最后,是因为用到的包 gulphexo 不互相构成依赖关系,为了避免对原来的开发环境造成破坏,gulp 压缩放到后面进行。

流程如下

1
2
3
4
5
6
7
8
9
10
11
12
13
mkdir gulp_dir && gulp_dir

# 依赖
npm install gulp gulp-clean-css gulp-html-minifier-terser gulp-htmlclean gulp-fontmin gulp-terser --save

# gulp 命令行工具
npm install gulp-cli -g

# 需要先用 hexo g 生成
cp ./blog/pubilc ./pubilc

# 压缩
gulp

依赖包的版本如下:

1
2
3
4
5
6
7
8
> npm list
gulp@ /home/aero/blog/gulp_dir
├── [email protected]
├── [email protected]
├── [email protected]
├── [email protected]
├── [email protected]
└── [email protected]

各个包的用途如下:

项目 用途
gulp-clean-css 压缩 css
gulp-html-minifier-terser 压缩 HTML
gulp-htmlclean 压缩 html
gulp-uglify 压缩 js
gulp-fontmin 压缩字体

目前没有找到更方便的 gulpfile.js 配置文件,沿用的是这里的:

1
[使用gulp压缩博客静态资源 | Akilarの糖果屋](https://akilar.top/posts/49b73b87/)

效果嘛,不能说是不尽人意,只能是没有尽到一点意义,可能是文件量太小的缘故吧。

压缩前:

1
2
> du -s public
1848 public

压缩后:

1
2
> du -s public
1864 public

但凡量纲大些都看不出来差别在哪儿。

搜索引擎优化

采用的是这个 Github 项目,可以实现自动推送链接
cjh0613/hexo-submit-urls-to-search-engine

1
npm install hexo-submit-urls-to-search-engine --save

详细可见其官方文档:
hexo-submit-urls-to-search-engine 中文文档

进行站长验证

申请好验证标记,三家都能申请

厂商 链接
Google Google Search Console
Baidu 百度搜索资源平台
Bing Bing Webmaster Tools

申请好的结果是这样的:

1
2
3
4
5
<meta name="google-site-verification" content="MXalOlJ3giNXLmtfFmg3nqhepdzSTZIx2amET1sJ90M" />

<meta name="msvalidate.01" content="MXalOlJ3giNXLmtfFmg3nqhepdzSTZIx2amET1sJ90M" />

<meta name="baidu-site-verification" content="codeva-YhIH7IUWWW" />

然后编辑主题文件 _config.butterfly.yml 分别填入对应的位置

1
2
3
4
5
6
7
site_verification:
  - name: google-site-verification
    content: MXalOlJ3giNXLmtfFmg3nqhepdzSTZIx2amET1sJ90M
  - name: baidu-site-verification
    content: codeva-YhIH7IUWWW
  - msvalidate.01
  content: MXalOlJ3giNXLmtfFmg3nqhepdzSTZIx2amET1sJ90M

构建并发布网站后,就可以一个个去点击验证了。

这里的密钥泄露没关系,网站验证密钥仅用于验证。后面的推送密钥绝对不能泄露。

设置搜索引擎推送

在主题配置站点配置 _config.yml 中添加:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
deploy:  
- type: cjh_google_url_submitter
- type: cjh_bing_url_submitter
- type: cjh_baidu_url_submitter

hexo_submit_urls_to_search_engine:
submit_condition: count #链接被提交的条件,可选值:count | period 现仅支持count
count: 10 # 提交最新的10个链接
period: 900 # 提交修改时间在 900 秒内的链接
google: 1 # 是否向Google提交,可选值:1 | 0(0:否;1:是)
bing: 1 # 是否向bing提交,可选值:1 | 0(0:否;1:是)
baidu: 1 # 是否向baidu提交,可选值:1 | 0(0:否;1:是)
txt_path: submit_urls.txt ## 文本文档名, 需要推送的链接会保存在此文本文档里
baidu_host: https://0xac.cn ## 在百度站长平台中注册的域名
baidu_token: BAIDU_TOKEN ## 请注意这是您的秘钥, 所以请不要把它直接发布在公众仓库里!
bing_host: https://0xac.cn ## 在bing站长平台中注册的域名
bing_token: BING_TOKEN ## 请注意这是您的秘钥, 所以请不要把它直接发布在公众仓库里!
google_host: https://0xac.cn ## 在google站长平台中注册的域名
google_key_file: project.json #存放google key的json文件,放于网站根目录(与hexo _config.yml文件位置相同),请不要把json文件内容直接发布在公众仓库里!

特别注意,这里的推送文件的密钥绝对不能公开,尤其是放到公共仓库中。
保不准谁拿着密钥干一些奇奇怪怪的事情

Actions 配置

如果采用的是 Actions,具体步骤可见后面的 Action 自动构建,可以这样子设置 Action:

1
2
3
4
5
6
7
8
9
- name: Prepare SEO
run: |
sed -i "s|BAIDU_TOKENS|${{ secrets.BAIDU_TOKEN }}|g" _config.yml
sed -i "s|BING_TOKENS|${{ secrets.BING_TOKEN }}|g" _config.yml
echo -E "${{ secrets.GOOGLE_JSON }}" > project.json

- name: Push to search engine
run: |
hexo deploy

然后在 Actions 环境变量里面设置:

1
2
3
4
5
6
7
8
BAIDU_TOKEN=xxxxxxxxxxx
BING_TOKEN=xxxxxxxxxxxxx
GOOGLE_JSON=
{
"type": "service_account",
xxxxxx
}

GOOGLE_JSON 的密钥有多行,上面的 echo -E 设置了避免写入时转义,避免读取出错

完整配置可见后面的 [Actions 合辑](#Actions 合辑)

优化的其他问题

Google Search Console 的 DNS 验证有些问题,本地已经查得到解析了,可依然提示验证不通过。

1
dig +nocmd google-site-verification.0xac.cn txt +noall +answer

Google Search Console 必须采样 .json 文件作为验证密钥,实际使用不容易安全导入到公开仓库。

评论区

评论区可以采用免费的 Valine 或需要自己搭建服务器的 Artalk,二者都比较简单,并且支持评论审核,可有效避免意外。

框架 网址
Valine https://valine.js.org/
Artalk https://artalk.js.org/

静网页托管

方案 特点 难度 价格 SEO
对象储存+CDN+DNS 自主可控 复杂 完整对搜索引擎优化
Pages + DNS 证书由 Pages 服务提供 稍复杂 只付域名 能做
纯 Pages 无自定义域名 简单 稍微能做

讲实话,真要追求推广,应该去写公众号

各大 Pages 服务

服务商 网址 说明
Github GitHub Pages 偶尔抽风
Gitee Gitee Pages 有审核
Netlify Netlify 差不多
Cloudflare Cloudflare Pages 勉强能用

Pages 服务放到最后进行讲解,这里只会将 Github Pages 的使用,其他的都差不多。由于 Cloudflare 经常被用于奇怪的用途,导致服务非常不稳定,建议不要用。

部署到 CDN

一般只有国内才需要这样子搞,步骤比较多:

  1. 在云服务商买一个最便宜,带 IP 地址的云服务器,因为备案必须带 IP
  2. 在云服务商里面申请备案,备案有一定限制,可以在服务商上了解
  3. 等云服务商提交信息到管理局,填短信验证 ICP/IP地址/域名信息备案管理系统 (miit.gov.cn) 如果没收到,可以申请重发
  4. 等备案号下来
  5. 创建对象存储桶,选择对应区域
  6. 创建 CDN 并配置好域名
  7. 设置 DNS 解析

虽然 DNS 可以解析到存储桶上,但这样流量费会非常贵。

每个厂商的流程都一定区别,并且某些厂商一直在变,还有直接取消已有服务的情况,不好具体说明,详细可见各厂商提供的文档。

备案域名需要更换,请直接申请新域名备案,备案下来后注销旧域名即可。不要听客服引导直接注销备案,那样子会产生空壳主体,很麻烦。

设置境内加速的 CDN 在境外是无法访问的

国内运营商的境内流量包比较便宜,境外的比较贵。

部署到网页自动生成服务

比较简单,这里推荐用 Qexo,带网页 GUI,可以实现全程白嫖,只需要申请一系列密钥,然后就能开始部署,全程不涉及命令行操作,非常简单。

服务商 权限 申请地址
Github 密钥 Repo & Workflow New Personal Access Token (Classic) (github.com)
Vercel Token - Tokens – Account – Dashboard – Vercel
reCAPTCHA - reCAPTCHA (google.com)
  • Github 是 git 存储仓库,可以有之前的博客
  • Vercel 是 pages 服务
  • reCAPTCHA 是网页端登录验证

然后点击以下链接一键部署
https://vercel.com/new/clone?repository-url=https://github.com/am-abudu/Qexo
数据库选 PostgreSQL。

不明白可以看官方文档,写的有点复杂:
Qexo | Qexo (oplog.cn)

其中 Vercel 的 Project ID 需要在这里获得:

1
https://vercel.com/<your_name>/<you_project>/settings

Actions 合辑

博客的核心是采用 GitHub Actions 进行自动化构建,整个结构如下:

链接 用途
hxac/blog (github.com) 主仓库
hxac/hxac.github.io Pages 仓库
只是笔记 (hxac.github.io) Pages 上的镜像
只是笔记 (0xac.cn) 主站

这里简要介绍下 GitHub Actions, Actions 主要由一下几个部分构成:

构成 解释 意义
workflow 工作流 一个 .yml 一个工作流
job 任务 workflow 由一个或多个 jobs 构成,任务直接互相独立
step 步骤 job 由 step 构成,是上下文衔接传递的,共享环境,工作目录固定
action 动作 执行的命令,相当于在命令行顺序执行

仓库目录下的 .github/workflows/xxxxxx.yml 就是一个工作流,在 .github/workflows 下的任何 .yml 结尾的文件,都是一个工作流,只要推送到仓库,就会自动识别 .yml 里面的配置,并构建。

只需要记住以下几点,就能避免掉很多令人困惑的情况:

  1. workflow 之间完全隔离,无法互联。
  2. job 之间互相隔离,job 之间传输文件,必需采用“工件传递”的方法。
  3. step 之间环境变量隔离,step 之间传递环境变量需要额外配置。
  4. actions 之间共享环境变量。

注意,无论是哪一种配置,都需要在本地搭建好博客,然后推送到 git 仓库中去,在云端构建。

直接在云端写入 Actions 控制文件是行不通的,容易出错

Actions 部署前准备

目前的方案是 Github 的私有仓库 + 公开仓库 pages + 腾讯云 CDN 链条,使用云端本地双备份,境内境外双路线。

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
# 私有的 blog 仓库
git clone [email protected]:hxac/blog.git

# 进入博客仓库
cd blog

# 安装 hexo 命令行
sudo npm install hexo-cli -g

# 下载主题
git clone https://github.com/jerryc127/hexo-theme-butterfly.git themes/butterfly

# 准备依赖包
npm install

# 升级依赖包
#npm update

# 清理上次构建
#hexo clean

# 生成构建
#hexo generate

# 临时预览
#hexo server

请务必在本地测试好 hexo generatehexo server ,确保可以看到非空白的网页。

这一步的核心是确保生成对应的 package.jsonpackage-lock.json 文件,以便构建的时候可以命中构建缓存,加快构建速度。

然后新建文件夹,放入 Actions 配置文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
mkdir .github

mkdir .github/workflows

# 创建每个 workflow 的工作文件

# 部署到 COS 并刷新 CDN
touch .github/workflows/onDaily.yml

# 申请证书并提交到 CDN
touch .github/workflows/onMonthly.yml

# 直接部署到 pages 页面
touch .github/workflows/onPush.yml

部署到 COS 并刷新 CDN

环境变量设置

需要准备的环境变量:

环境变量 说明 申请链接
TENCENTCLOUD_SECRET_ID 腾讯云 API ID 信息中心 - 云 API
TENCENTCLOUD_SECRET_KEY 腾讯云 API key 信息中心 - 云 API
TENCENTCLOUD_REGION 云服务区域
COS_BUCKET 存储桶所在区域 ID 存储桶列表 - 对象存储
RQ_DOMAIN CDN 加速的域名 你的域名比方说 0xac.cn
BAIDU_TOKEN 百度站长密钥 百度搜索资源平台
BING_TOKENS 必应站长密钥 必应网站管理员工具

Google Search 的密钥不好配置,建议在私有库中配置。

大部分厂商的配置大同小异,就不一一列举了

配置其他厂商,可以参考前面的代码自己写 Actions,但请确保脚本可以在本地正常运行后,再写成 Actions 文件

不会写 Actions?跟着完整的流程下来就能写了,不难就是掉亿点点头发

全部的环境变量如下,在仓库设置页面的 Settings -> Actions -> Secrets 里面逐个添加即可。

1
2
3
4
5
6
7
TENCENTCLOUD_SECRET_ID
TENCENTCLOUD_SECRET_KEY
TENCENTCLOUD_REGION
COS_BUCKET
RQ_DOMAIN
BAIDU_TOKENS
BING_TOKENS

Actions 文件

完整的 Actions 配置文件如下,一般是在仓库里面的 blog/.github/workflows/my_workflow.yml 。比方说这里的是 blog 仓库,my_workflow.yml 文件名任意取,只要是 .yml 结尾的,在 ./.github/workflows 下的都会被执行。

默认为有 push 到 Github 就会自动执行。

注意,这里的 branchesmaster ,而新版本的 Github 默认分支是 main

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
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
name: Deploy Blog to CDN

on:
push:
branches-ignore: [master]
#push:
# branches: [ "master" ]
schedule:
- cron: "3 3 * * *"
# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:

env:
TZ: Asia/Shanghai

jobs:
generate-pages:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3

- name: Use Node.js 18
uses: actions/setup-node@v3
with:
node-version: 18
cache: 'npm'

- name: Cache node modules
uses: actions/cache@v3
id: cache
with:
path: node_modules
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-

- name: Prepare Hexo
run: |
npm install hexo-cli -g

- name: Prepare Themes
run: |
mkdir -p blog/themes
git clone -b master https://github.com/jerryc127/hexo-theme-butterfly.git themes/butterfly

- name: Prepare Blog
if: steps.cache.outputs.cache-hit != 'true'
run: npm install

- name: Generate Blog
run: |
hexo clean
hexo generate

- name: Prepare SEO
run: |
sed -i "s|BAIDU_TOKENS|${{ secrets.BAIDU_TOKEN }}|g" _config.yml
sed -i "s|BING_TOKENS|${{ secrets.BING_TOKEN }}|g" _config.yml

- name: Push to search engine
run: |
hexo deploy

- name: Upload generated public pages
uses: actions/upload-artifact@v3
with:
name: public_pages
path: ./public

compress-pages:
runs-on: ubuntu-latest
needs: generate-pages
steps:
- uses: actions/checkout@v3
- name: Use Node.js 18
uses: actions/setup-node@v3
with:
node-version: 18
cache: 'npm'

- name: Download generated public pages
uses: actions/download-artifact@v3
with:
name: public_pages
path: ./public

# 这里可以新建文件夹,运行 Gulp 的两行安装命令,然后复制生成的 package.json 和 package-lock.json 文件,以便在构建自动寻找缓存,加速构建
#- name: Install Gulp
# run: |
# rm package.json
# rm package-lock.json
# mv package.json.gulp package.json
# mv package-lock.json.gulp package-lock.json
# npm install
# npm install gulp-cli -g

- name: Install Gulp
run: |
npm install gulp gulp-clean-css gulp-html-minifier-terser gulp-htmlclean gulp-terser --save
npm install gulp-cli -g

- name: Compress Pages
run: |
gulp

- name: Upload compressed public pages
uses: actions/upload-artifact@v3
with:
name: public_pages
path: ./public

upload-store:
runs-on: ubuntu-latest
needs: compress-pages
steps:
- uses: actions/checkout@v3
- name: Download compressed public pages
uses: actions/download-artifact@v3
with:
name: public_pages
path: ./public

- name: Install COS Tool
run: |
wget https://github.com/tencentyun/coscli/releases/download/v0.13.0-beta/coscli-linux
mv coscli-linux coscli
chmod 755 coscli
./coscli --version
touch ~/.cos.yaml

#- name: Compressed Files
# run: |
# tar -zcvf blog.tgz -C public .

- name: Depoly blog
run: |
./coscli config add -b ${{ secrets.COS_BUCKET }} -r ${{ secrets.TENCENTCLOUD_REGION }} -a public-page
./coscli config set --secret_key ${{ secrets.TENCENTCLOUD_SECRET_KEY }} --secret_id ${{ secrets.TENCENTCLOUD_SECRET_ID }}
./coscli rm cos://public-page/ -rf
./coscli cp public/ cos://public-page/ -r
./coscli du cos://public-page/

reflush-cdn:
runs-on: ubuntu-latest
needs: upload-store
steps:
- uses: actions/checkout@v3
- uses: actions/setup-python@v4
with:
python-version: '3.9'
# Python 也是有缓存的,也能打开
- name: Install Cloud Service Cli
run: |
pip install tccli

- name: Load Tencent Cloud API
run: |
echo export TENCENTCLOUD_SECRET_ID="\"${{ secrets.TENCENTCLOUD_SECRET_ID }}\"" > .env
echo export TENCENTCLOUD_SECRET_KEY="\"${{ secrets.TENCENTCLOUD_SECRET_KEY }}\"" >> .env
echo export TENCENTCLOUD_REGION="\"${{ secrets.TENCENTCLOUD_REGION }}\"" >> .env

- name: reflush CDN
run: |
source .env
tccli cdn PurgePathCache --cli-unfold-argument --Paths "https://${{ secrets.RQ_DOMAIN }}/" --FlushType flush

申请证书并提交到 CDN

请确保有独立的一级域名,否则可能申请不了证书

环境变量设置

需要准备的环境变量:

环境变量 说明 申请链接
TENCENTCLOUD_SECRET_ID 腾讯云 API ID 信息中心 - 云 API
TENCENTCLOUD_SECRET_KEY 腾讯云 API key 信息中心 - 云 API
TENCENTCLOUD_REGION 云服务区域
COS_BUCKET 存储桶所在区域 ID 存储桶列表 - 对象存储
RQ_DOMAIN CDN 加速的域名 你的域名 0xac.cn
DP_ID DNSPod ID API 密钥
DP_KEY DNSPod Key

这里的 RQ_DOMAIN 建议是一级域名 0xac.cn 不要是 blog.0xac.cn。因为申请证书默认申请 0xac.cn*.0xac.cn 泛域名的。

大部分厂商的配置大同小异,需要自行修改 Actions 文件配置。

配置其他厂商,可以参考前面的代码自己写 Actions,但请确保脚本可以在本地正常运行后,再写成 Actions 文件。

Let’sencrypt 证书申请有请求频率限制,确保能够在本地正常使用,再上云。

全部的环境变量如下,在 Settings -> Actions -> Secrets 里面逐个添加即可。

1
2
3
4
5
6
7
TENCENTCLOUD_SECRET_ID
TENCENTCLOUD_SECRET_KEY
TENCENTCLOUD_REGION
COS_BUCKET
RQ_DOMAIN
DP_ID
DP_KEY

和上一篇不同的是多了 DNSPod 的 API 密钥:

1
2
DP_ID
DP_KEY

定时任务设置

这里设置为定时执行 cron: "1 1 1 1/2 1"

根据文档:Events that trigger workflows - GitHub Docs

1
2
3
4
5
6
7
8
9
┌───────────── minute (0 - 59)
│ ┌───────────── hour (0 - 23)
│ │ ┌───────────── day of the month (1 - 31)
│ │ │ ┌───────────── month (1 - 12 or JAN-DEC)
│ │ │ │ ┌───────────── day of the week (0 - 6 or SUN-SAT)
│ │ │ │ │
│ │ │ │ │
│ │ │ │ │
* * * * *

也就是说 corn 的语法是由空格分割,每个位置如下:

位数 1 2 3 4 5
标号 1 1 1 1/2 1

填入数字有范围限制,也就是是:

位数 含义 范围
1 分钟 0-59
2 小时 0-23
3 1-31
4 1-12
5 0-6

只有到指定日期时间,cron 才会触发任务运行。

同时一些特殊字符表示:

符号 表示 例子
* 任何时间 15 * * * * 每小时的 xx:15 执行一次
, 多个时间 2,10 * * * * 每小时的 xx:02 和 xx:10 执行一次
- 时间范围 10 4-6 * * * 每天的 4-6 的第十分钟执行一次 ,即 04:10, 05:10: 06:10 执行一次
/ 时间周期 5/15 * * * * 每小时的 5 分钟后,每 15 分钟执行一次,即每小时的 xx:05, xx:20, xx:35, xx:50 分钟执行一次

换句话说 1 1 1 1/2 1 表示在每隔 2 月的 1 分,1小时, 1 日,第 1 周,执行一次,也就是会在以下时间执行:

1
2
3
4
5
6
1 月 1 日 - 第 1 周 - 01:01
3 月 1 日 - 第 1 周 - 01:01
5 月 1 日 - 第 1 周 - 01:01
7 月 1 日 - 第 1 周 - 01:01
9 月 1 日 - 第 1 周 - 01:01
11 月 1 日 - 第 1 周 - 01:01

Let’sencrypt 证书 有效期为三个月,为了避免提前失效,可以两个月申请一次。

Actions 文件

这里的 branches-ignore: [master] 忽略分支需要设置正确,否则一旦有 push 请求到仓库就会被执行。

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
name: Generate Certificates

on:
push:
branches-ignore: [master]
schedule:
- cron: "1 1 1 1/2 1"
# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:
jobs:
generate-Key:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-python@v4

- name: Clone ACME.SH
run: |
curl https://get.acme.sh | sh

- name: Config ACME.sh
if: ${{ env.ACME == 1 }}
run: |
echo "DEFAULT_ACME_SERVER='https://acme-v02.api.letsencrypt.org/directory'" > /home/runner/.acme.sh/account.conf
echo "SAVED_DP_Id='${{ secrets.DP_ID }}'" >> /home/runner/.acme.sh/account.conf
echo "SAVED_DP_Key='${{ secrets.DP_KEY }}'" >> /home/runner/.acme.sh/account.conf

- name: Request Let's Encrypt for issue certificates
if: ${{ env.ACME == 1 }}
run: |
/home/runner/.acme.sh/acme.sh --issue -d "${{ secrets.RQ_DOMAIN }}" -d "*.${{ secrets.RQ_DOMAIN }}" --dns dns_dp

- name: Load certificates and key from ACME.SH
if: ${{ env.ACME == 1 }}
run: |
echo export PUBLIC_KEY="\"$(cat /home/runner/.acme.sh/${RQ_DOMAIN}_ecc/${RQ_DOMAIN}.key)\"" > .env
echo export PUBLIC_KEY="\"$(cat /home/runner/.acme.sh/${RQ_DOMAIN}_ecc/${RQ_DOMAIN}.key)\"" >> .env

- name: Load certificates and key from ENV
if: ${{ env.ACME == 0 }}
run: |
echo PUBLIC_KEY="\"${{ secrets.PUBLIC_KEY }}\"" > .env
echo PRIVATE_KEY="\"${{ secrets.PRIVATE_KEY }}\"" >> .env

- name: Install Tencent Cloud CLi
run: |
pip install tccli

- name: Load Tencent Cloud API
run: |
echo export TENCENTCLOUD_SECRET_ID="\"${{ secrets.TENCENTCLOUD_SECRET_ID }}\"" >> .env
echo export TENCENTCLOUD_SECRET_KEY="\"${{ secrets.TENCENTCLOUD_SECRET_KEY }}\"" >> .env
echo export TENCENTCLOUD_REGION="\"${{ secrets.TENCENTCLOUD_REGION }}\"" >> .env

- name: Upload public key and private key
run: |
source .env
echo export CERT_ID="\"$(tccli ssl UploadCertificate --cli-unfold-argument --CertificatePublicKey "${PUBLIC_KEY}" --CertificatePrivateKey "${PRIVATE_KEY}" --filter CertificateId)"\" >> .env

- name: Update public key and private key to CDN
run: |
source .env
tccli cdn UpdateDomainConfig --cli-unfold-argument --Domain "${{ secrets.RQ_DOMAIN }}" --Https.Switch on --Https.CertInfo.CertId "${CERT_ID}"
tccli cdn UpdateDomainConfig --cli-unfold-argument --Domain "api.${{ secrets.RQ_DOMAIN }}" --Https.Switch on --Https.CertInfo.CertId "${CERT_ID}"

直接部署到 pages 页面

  1. 新建一个和用户名一样的仓库 hxac/hxac.github.io,比方说用户名是 hxac,那么仓库就是 hxac.github.io
  2. 需要在仓库里设置 Settings -> Pages -> Source,将 branch 改为 gh-pages 确保 pages 的显示页面是正确的。
  3. 需要在仓库里设置 Settings -> Deploy keys,里面添加。

这里由于已经有了域名+CDN 了,做这个新页面只是用来备份,因此有个切换博客域名的操作:

1
2
3
- name: Change Domain
run: |
sed -i 's|^url: https:\/\/0xac\.cn|url: https:\/\/hxac.github.io|g' _config.yml

环境变量

添加 ssh-key 在私有仓库的环境变量 里面,然后推送到公开仓库中去。

仓库 可见性 用途
blog 私有 私有文件,构建原文件,Actions 文件
username.github.io 公开 pages 页面展示

有备案的建议移除备案,github.io 子域名是无法备案的/

然后设置好环境变量

1
SSH_PRIVATE_KEY

Actions 文件

注意,这里的:

1
2
3
4
- name: Change Domain
run: |
sed -i 's|^url: https:\/\/0xac\.cn|url: https:\/\/hxac.github.io|g' _config.yml
sed -i '/custom_text/d' _config.butterfly.yml

需要根据自己的情况配置,注意 sed 命令,是需要转义的。

如果不采用国内 + 国外双模式,可以直接删掉这一行 actions。

双模式可以加快国内和国外的访问速度。

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
name: Deploy Blog to pages

on:
push:
branches: [ "master" ]

# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:

env:
TZ: Asia/Shanghai

jobs:
generate-pages:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3

- name: Use Node.js 18
uses: actions/setup-node@v3
with:
node-version: 18
cache: 'npm'

- name: Cache node modules
uses: actions/cache@v3
id: cache
with:
path: node_modules
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-

- name: Prepare Hexo
run: |
npm install hexo-cli -g

- name: Prepare Themes
run: |
mkdir -p blog/themes
git clone -b master https://github.com/jerryc127/hexo-theme-butterfly.git themes/butterfly

- name: Prepare Blog
if: steps.cache.outputs.cache-hit != 'true'
run: npm install

- name: Change Domain
run: |
sed -i 's|^url: https:\/\/0xac\.cn|url: https:\/\/hxac.github.io|g' _config.yml
sed -i '/custom_text/d' _config.butterfly.yml

- name: Generate Blog
run: |
hexo clean
hexo generate

- name: Upload compressed public pages
uses: actions/upload-artifact@v3
with:
name: public_pages
path: ./public

deploy-pages:
runs-on: ubuntu-latest
needs: generate-pages
steps:
- uses: actions/checkout@v3
- name: Download compressed public pages
uses: actions/download-artifact@v3
with:
name: public_pages
path: ./public/

- name: No jekyll
run: |
touch .nojekyll


- name: Deploy to gh-pages
uses: s0/git-publish-subdir-action@develop
env:
REPO: [email protected]:hxac/hxac.github.io.git
BRANCH: gh-pages
FOLDER: ./public/
SSH_PRIVATE_KEY: ${{ secrets.DEPLOY_PRIVATE_KEY }}

参考资料

【1】:actions/setup-python: Set up your GitHub Actions workflow with a specific version of Python
【2】:Platform-CUF/use-gulp: gulp资料收集 (github.com)