Hexo框架

本次版本:

  • node v22.13.1
  • git version 2.47.1.windows.2
  • hexo: 8.1.2
    hexo-cli: 4.3.2
  • npm 11.4.2
  • butterfly 5.7.0

快速安装 :

1
2
3
4
hexo init
npm install
git clone -b master https://github.com/jerryc127/hexo-theme-butterfly.git themes/butterfly
npm install hexo-renderer-pug hexo-renderer-stylus --save

安装插件:

  1. 搜索

  2. 数学公式

  3. 思维导图

  4. 加密文章

    1
    2
    3
    4
    npm install hexo-generator-search --save
    npm install hexo-filter-katex --save
    npm install hexo-markmap --save
    npm install hexo-blog-encrypt --save

Node.js和Git

安装Hexo前,电脑需要先有Node.jsGit

  • 安装 Node.js:Hexo要求Node.js版本不低于10.13,建议使用12.0及以上版本。可以在Node.js官网下载安装程序。
    • Windows用户:安装时务必勾选 Add to PATH 选项。
    • Mac/Linux用户:推荐使用nvmnvs这类版本管理工具安装,以避免权限问题。
  • 安装 Git:从Git官网下载安装即可。
    • Mac用户:可以通过Homebrew安装,或从App Store安装Xcode及命令行工具。
    • Linux用户:可以使用包管理器安装,例如Ubuntu/Debian用 sudo apt-get install git-core

出现版本信息则表示环境没问题

1
2
node -v  # 查看node版本信息 
git --version # 查看Git版本信息
1
2
3
4
5
C:\Code\KikaBlog>node -v  # 查看node版本信息
v22.13.1

C:\Code\KikaBlog>git --version # 查看Git版本信息
git version 2.47.1.windows.2
  • 不过Node.js太新好像不好——Hexo 的某些旧依赖包不兼容 Node.js 22

pnpm?

npm 依赖冲突 问题,通常由 Hexo 的旧版本依赖与新版 npm 的严格检查机制不兼容导致。

所以,所有 npm **改成 pnpm **。

npm install -g pnpm  # 使用npm安装pnpm,全局安装 pnpm
pnpm setup  # 自动初始化

这个命令会自动:

  • 创建必要的目录,会自动创建 pnpmpnpm-cachepnpm-state 等目录
  • 更新环境变量配置,会将全局 bin 目录添加到系统的用户环境变量 Path
  • 让命令行识别 pnpm 全局安装的命令

【重要】运行完 pnpm setup 后,关闭当前终端窗口,然后重新打开一个新的终端

手动删除node_modules文件夹和package-lock.json文件

清理 npm 缓存

npm cache clean --force

如果要卸载pnpm:

npm uninstall -g pnpm

卸载后检查,运行以下命令确认是否卸载成功:

where pnpm

如果提示“找不到文件”或没有输出,说明卸载成功。

全局安装 hexo-cli

pnpm install -g hexo-cli --ignore-scripts
  • npm 自动执行所有脚本
  • pnpm 让你控制,让你选择是否构建,更安全,更可控。
    • 参数 --ignore-scripts 表示完全禁用脚本,忽略脚本(更安全)

安装Hexo

准备存放博客的文件夹,后续称之为 hexo 根目录文件夹,cmd打开终端。

1
2
3
npm install -g hexo-cli
hexo init
npm install
  • 全局安装 hexo-cli

    npm install -g hexo-cli
  • 初始化,Hexo 将会新建所需要的文件

    hexo init
  • 安装依赖,会生成一个依赖包文件夹node_modules和一个依赖包相关信息文件。

    npm install

查询Hexo 与 npm 版本的命令

1
2
hexo -v
npm -v

常用命令

清除缓存

hexo clean

生成主题静态文件:

等价于hexo generate

hexo g

启动本地服务:

等价于hexo server

hexo s

部署到GitHub:

hexo d

博客根目录下cmd,执行以下命令,创建一篇新文章:

hexo new "标题"

本地测试

hexo clean && hexo generate && hexo s

部署

hexo clean && hexo generate && hexo deploy

.exe

C:\Windows\System32\cmd.exe /k "cd C:\Code\KikaBlog && hexo c && hexo g && hexo s"

查询版本

1
2
3
4
5
6
node -v  # 查看node版本信息 
git --version # 查看Git版本信息
hexo -v
npm -v
cd themes/butterfly
type package.json | findstr "version"

快速开始测试博客

1
2
3
4
5
6
7
hexo init
npm install
git clone -b master https://github.com/jerryc127/hexo-theme-butterfly.git themes/butterfly
npm install hexo-deployer-git --save
npm install hexo-renderer-pug hexo-renderer-stylus --save
npm install hexo-generator-search --save

Github pages

创建一个仓库

  • 仓库名创建规则:用户名.github.io,必须是用户名加github.io后缀,因为我们使用的是Github pages。

安装【hexo-deployer-git 插件】

然后,hexo 根目录,部署的操作都在上面进行。

npm install hexo-deployer-git --save

添加配置,在根目录的 _config.yml 文件中添加配置

1
2
3
4
deploy:
type: 'git'
repo: https://github.com/Kkkika/Kkkika.github.io.git
branch: master

Butterfly主题

安装Butterfly

在根目录下 下载主题。

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

安装 pug 和 stylus 渲染器

npm install hexo-renderer-pug hexo-renderer-stylus --save
  • npm写法:npm install hexo-renderer-pug hexo-renderer-stylus –save
  • pnpm写法:pnpm add hexo-renderer-pug hexo-renderer-stylus
    pnpm add 默认会将依赖保存到 dependencies 中(相当于 --save

修改项目根目录下的_config.yml文件(称为站点配置文件),开启主题。

theme: butterfly

把主题文件夹中的 _config.yml 重命名为 _config.butterfly.yml,复制到 Hexo 根目录 下与_config.yml同级。

image-20260806151737080

  • Hexo会自动合并主题中的_config.yml_config.butterfly.yml ,如果存在同名配置,会使用_config.butterfly.yml的配置,其优先度较高。所以像和博客网址相关联的固定资料可以设置在_config.yml中,比如博客的标题、作者信息和邮箱等等资料,而和主题样式相关的配置放在 _config.butterfly.yml 中,那么在将来你想换一个主题是很方便的。

GitHub私密 + Cloudflare Pages部署

GitHub推送

上传代码到 GitHub,Cloudflare Pages 依赖 Git 仓库

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
hexo init
npm install

git init
git add .
git commit -m "init hexo"
git remote add origin https://github.com/Kkkika/blog1-hexo.git
git remote -v
git push -u origin master

git submodule add https://github.com/jerryc127/hexo-theme-butterfly themes/butterfly

npm install hexo-renderer-pug hexo-renderer-stylus --save
npm install hexo-generator-search --save
npm install hexo-filter-katex --save
npm install hexo-markmap --save
npm install hexo-blog-encrypt --save

git pull
git add .
git commit -m "初始化博客"
git push

git pull
git add .
git commit -m "更新post Blog启动"
git push
  • 子模块更新:由于主题是通过 git submodule 引入的,后续如果主题有更新,需要在本地执行 git submodule update --remote 来拉取最新主题代码。

绑定 Git 仓库

  1. 登录 Cloudflare 控制台,左侧菜单打开 Workers & Pages
    【构建】→【计算】→【Workers & Pages】
  2. 点击 Create application → 切换到 Pages → Import a repository(导入现有仓库)
  3. 选择 GitHub,跳转授权页面:
    • 选择你的 GitHub 账号
    • 权限选择:Only select repositories,只勾选你的博客仓库(更安全)
    • 点击 Install & Authorize 授权
  4. 仓库列表选中你的my-blog仓库,点击 Begin setup(开始配置)

构建参数

配置项 填写内容
生产分支 master
框架预设
构建命令 npx hexo clean && npx hexo generate
构建输出目录 public

环境变量(Settings → Environment variables)加:

  • NODE_VERSION = 你本地 node -v 的大版本,如 22(Butterfly 新版本建议 18+)
  • 可选 NPM_VERSION 对齐本地

注意:构建命令别写 npm(Hexo 默认没这个 script,除非你自己加),正确的是 npx

image-20260816002356041

开始部署

点击 Save and deploy,Cloudflare 会自动拉取代码、构建站点。

等待 1~3 分钟,构建成功后会生成默认域名:xxx.pages.dev,点击 Visit 即可访问博客。

快速更新.bat

Blog.exe

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@echo off
chcp 65001 >nul
:: 进入博客目录
cd /d "C:\Code\blog1-hexo"

:: 用绝对路径调用 git
call "C:\Program Files\Git\cmd\git.exe" add -A
call "C:\Program Files\Git\cmd\git.exe" commit -m "update"
call "C:\Program Files\Git\cmd\git.exe" push

if %errorlevel% equ 0 (
echo.
echo ============= ✅ Deploy Success! =============
) else (
echo.
echo ============= ❌ Deploy FAILED ! =============
)

pause

常见问题

跨平台换行符警告

这并不是错误👉可忽略

默认配置(Windows 开发常用)

1
2
git config --global core.autocrlf true
git config --global core.safecrlf warn

用 warn,有风险给你提示,但不阻断提交,是比较稳妥的方案。

  1. core.safecrlf 返回空

没有全局手动设置过 safecrlf,Git 使用内置默认值:warn 也就是:遇到换行转换风险只给警告,不阻止提交。

  1. core.autocrlf = true

全局开启自动换行转换:

  • 提交到 git:文件 CRLF → LF
  • 克隆 / 拉取到本地:LF → CRLF(Windows 本地文件用 \r\n)

查看当前配置(全局)

1
2
git config --global --get core.safecrlf
git config --global --get core.autocrlf

查看当前配置(当前仓库)

1
2
git config --get core.safecrlf
git config --get core.autocrlf

Hugo框架

Hexo太慢了,最新一次备份2026-07-13 04578fc02734d4a89cee48554ce1b3cf4d5cc6e5

第二次部署博客,改用Hugo

中文文档:Hugo官方文档

主题挑选

迁移到 Hugo 后选哪个主题?博主实用推荐 - 余师洋的个人博客

Congo好像不错【待尝试】

Git

安装Hexo前,电脑需要先有Git

  • 安装 Git:从Git官网下载安装即可。

Hugo下载

Windows下载 Hugo Extended 版本

winget install Hugo.Hugo.Extended

验证版本,应显示 extended 字样,如 hugo v0.164.x+extended 或更高版本

hugo version

初始化站点

将下载后的内容放在一个空文件夹中。在当前文件夹上方地址栏输入 cmdEnter 唤起命令行。

hugo new site .

启动本地站点

hugo server

如果页面显示 “Page not found”,说明此前的所有配置都是正常无误的。(通过 Ctrl + C 可关闭这一服务,地址通常是 localhost:1313

初始化 Git 仓库

git init

常用命令

hugo server --disableFastRender --cleanDestinationDir
参数 含义 什么时候用
--disableFastRender 禁用快速渲染。Hugo 默认只重新渲染你修改过的页面,其他页面从缓存读取。这个参数强制 Hugo 每次请求都完整重新渲染整个站点 改了模板(如 toc.html)后页面没变化,怀疑是缓存导致时
--cleanDestinationDir 清理目标目录。启动前先把 public/(或你配置的输出目录)整个删掉,再重新生成。 删除了某些文件但残留还在输出目录里,或者想确保从零开始构建时

快速调试.exe

Hugo.exe

C:\Windows\System32\cmd.exe /k "cd C:\Code\blog2-hugo && hugo server --disableFastRender --cleanDestinationDir"

PaperMod主题

安装

推荐使用 Git Submodule 方式安装,便于后续更新:

git submodule add --depth=1 https://github.com/adityatelange/hugo-PaperMod.git themes/PaperMod

如果后续要更新主题,运行:git submodule update --remote --merge

hugo.toml

编辑根目录下的 hugo.toml

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
baseURL = "https://yourname.pages.dev/"
languageCode = "zh-cn"
title = "Kika-秋"
theme = "PaperMod"
paginate = 10

hasCJKLanguage = true # 中文站点必开,否则字数统计和摘要会异常

[params]
env = "production"
description = "品味是最高判断力"
author = "Kika"
defaultTheme = "auto" # 自动深色/浅色模式
ShowReadingTime = true # 显示阅读时长
ShowShareButtons = true # 显示分享按钮
ShowPostNavLinks = true # 上下篇导航
ShowBreadCrumbs = true # 面包屑导航
ShowCodeCopyButtons = true # 代码复制按钮(技术博客必开)

[params.homeInfoParams]
Title = "Hi,我是 XXX 👋"
Content = "这里记录我的技术探索与思考"

[[params.socialIcons]]
name = "github"
url = "https://github.com/yourusername"

[[params.socialIcons]]
name = "twitter"
url = "https://x.com/yourusername"

[menu]
[[menu.main]]
identifier = "posts"
name = "文章"
url = "/posts/"
weight = 10
[[menu.main]]
identifier = "tags"
name = "标签"
url = "/tags/"
weight = 20
[[menu.main]]
identifier = "about"
name = "关于"
url = "/about/"
weight = 30

[outputs]
home = ["HTML", "RSS", "JSON"]

Toc目录

覆盖 toc.html 模板

根目录下的 layouts\partials\toc.html

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
{{- $headers := findRE "<h[1-6].*?>(.|\n])+?</h[1-6]>" .Content -}}
{{- $has_headers := ge (len $headers) 1 -}}
{{- if $has_headers -}}
<aside id="toc-container" class="toc-container wide">
<div class="toc">
<details {{if (.Param "TocOpen") }} open{{ end }}>
<summary accesskey="c" title="(Alt + C)">
<span class="details">目录</span>
</summary>

<div class="inner">
{{- $largest := 6 -}}
{{- range $headers -}}
{{- $headerLevel := index (findRE "[1-6]" . 1) 0 -}}
{{- $headerLevel := len (seq $headerLevel) -}}
{{- if lt $headerLevel $largest -}}
{{- $largest = $headerLevel -}}
{{- end -}}
{{- end -}}

{{- $firstHeaderLevel := len (seq (index (findRE "[1-6]" (index $headers 0) 1) 0)) -}}

{{- $.Scratch.Set "bareul" slice -}}
<ul>
{{- range seq (sub $firstHeaderLevel $largest) -}}
<ul>
{{- $.Scratch.Add "bareul" (sub (add $largest .) 1) -}}
{{- end -}}
{{- range $i, $header := $headers -}}
{{- $headerLevel := index (findRE "[1-6]" . 1) 0 -}}
{{- $headerLevel := len (seq $headerLevel) -}}

{{/* get id="xyz" */}}
{{- $id := index (findRE "(id=\"(.*?)\")" $header 9) 0 }}

{{- /* strip id="" to leave xyz, no way to get regex capturing groups in hugo */ -}}
{{- $cleanedID := replace (replace $id "id=\"" "") "\"" "" }}
{{- $header := replaceRE "<h[1-6].*?>((.|\n])+?)</h[1-6]>" "$1" $header -}}

{{- if ne $i 0 -}}
{{- $prevHeaderLevel := index (findRE "[1-6]" (index $headers (sub $i 1)) 1) 0 -}}
{{- $prevHeaderLevel := len (seq $prevHeaderLevel) -}}
{{- if gt $headerLevel $prevHeaderLevel -}}
{{- range seq $prevHeaderLevel (sub $headerLevel 1) -}}
<ul>
{{/* the first should not be recorded */}}
{{- if ne $prevHeaderLevel . -}}
{{- $.Scratch.Add "bareul" . -}}
{{- end -}}
{{- end -}}
{{- else -}}
</li>
{{- if lt $headerLevel $prevHeaderLevel -}}
{{- range seq (sub $prevHeaderLevel 1) -1 $headerLevel -}}
{{- if in ($.Scratch.Get "bareul") . -}}
</ul>
{{/* manually do pop item */}}
{{- $tmp := $.Scratch.Get "bareul" -}}
{{- $.Scratch.Delete "bareul" -}}
{{- $.Scratch.Set "bareul" slice}}
{{- range seq (sub (len $tmp) 1) -}}
{{- $.Scratch.Add "bareul" (index $tmp (sub . 1)) -}}
{{- end -}}
{{- else -}}
</ul>
</li>
{{- end -}}
{{- end -}}
{{- end -}}
{{- end }}
<li>
<a href="#{{- $cleanedID -}}" aria-label="{{- $header | plainify -}}">{{- $header | safeHTML -}}</a>
{{- else }}
<li>
<a href="#{{- $cleanedID -}}" aria-label="{{- $header | plainify -}}">{{- $header | safeHTML -}}</a>
{{- end -}}
{{- end -}}
<!-- {{- $firstHeaderLevel := len (seq (index (findRE "[1-6]" (index $headers 0) 1) 0)) -}} -->
{{- $firstHeaderLevel := $largest }}
{{- $lastHeaderLevel := len (seq (index (findRE "[1-6]" (index $headers (sub (len $headers) 1)) 1) 0)) }}
</li>
{{- range seq (sub $lastHeaderLevel $firstHeaderLevel) -}}
{{- if in ($.Scratch.Get "bareul") (add . $firstHeaderLevel) }}
</ul>
{{- else }}
</ul>
</li>
{{- end -}}
{{- end }}
</ul>
</div>
</details>
</div>
</aside>
<script>
let activeElement;
let elements;
window.addEventListener('DOMContentLoaded', function (event) {
checkTocPosition();

elements = document.querySelectorAll('h1[id],h2[id],h3[id],h4[id],h5[id],h6[id]');
// Make the first header active
activeElement = elements[0];
const id = encodeURI(activeElement.getAttribute('id')).toLowerCase();
document.querySelector(`.inner ul li a[href="#${id}"]`).classList.add('active');
}, false);

window.addEventListener('resize', function(event) {
checkTocPosition();
}, false);

window.addEventListener('scroll', () => {
// Check if there is an object in the top half of the screen or keep the last item active
activeElement = Array.from(elements).find((element) => {
if ((getOffsetTop(element) - window.pageYOffset) > 0 &&
(getOffsetTop(element) - window.pageYOffset) < window.innerHeight/2) {
return element;
}
}) || activeElement

elements.forEach(element => {
const id = encodeURI(element.getAttribute('id')).toLowerCase();
if (element === activeElement){
document.querySelector(`.inner ul li a[href="#${id}"]`).classList.add('active');
} else {
document.querySelector(`.inner ul li a[href="#${id}"]`).classList.remove('active');
}
})
}, false);

const main = parseInt(getComputedStyle(document.body).getPropertyValue('--article-width'), 10);
const toc = parseInt(getComputedStyle(document.body).getPropertyValue('--toc-width'), 10);
const gap = parseInt(getComputedStyle(document.body).getPropertyValue('--gap'), 10);

function checkTocPosition() {
const width = document.body.scrollWidth;

if (width - main - toc - (gap * 3) > 0) {
document.getElementById("toc-container").classList.add("wide");
} else {
document.getElementById("toc-container").classList.remove("wide");
}
}

function getOffsetTop(element) {
if (!element.getClientRects().length) {
return 0;
}
let rect = element.getBoundingClientRect();
let win = element.ownerDocument.defaultView;
return rect.top + win.pageYOffset;
}
</script>
{{- end }}

添加 CSS

找到主题文件夹,新建themes\PaperMod\assets\css\extended\blank.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
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
:root {
--nav-width: 1380px;
--article-width: 720px;
--toc-width: 260px;
}

.toc {
margin: 0 2px 40px 2px;
border: 1px solid var(--border);
background: var(--entry);
border-radius: var(--radius);
padding: 0.4em;
}

.toc-container.wide {
position: absolute;
height: 100%;
border-right: 1px solid var(--border);
left: calc((var(--toc-width) ) * -1);
top: calc(var(--gap) * 2);
width: var(--toc-width);
padding-left: var(--gap);
}

.wide .toc {
position: sticky;
top: var(--gap);
border: unset;
background: unset;
border-radius: unset;
width: 100%;
margin: 0 2px 40px 2px;
}

.toc details summary {
cursor: zoom-in;
margin-inline-start: 20px;
padding: 12px 0;
}

.toc details[open] summary {
font-weight: 500;
}

.toc-container.wide .toc .inner {
margin: 0;
}

.active {
font-size: 110%;
font-weight: 600;
color: #ff8c00; /* 深橙色 */
}

.toc ul {
list-style-type: circle;
}

.toc .inner {
margin: 0 0 0 20px;
padding: 0px 15px 15px 40px;
font-size: 16px;
}

.toc li ul {
margin-inline-start: calc(var(--gap) * 0.5);
list-style-type: none;
}

.toc li {
list-style: circle;
font-size: 1.1rem;
padding-bottom: 5px;
}

.toc li a:hover {
color: #ffd700; /* 亮黄色(金色) */
}

参考:Hugo博客目录放在侧边 | PaperMod主题-腾讯云开发者社区-腾讯云

img路径

问题背景

Hugo 在构建站点后,所有资源都输出到 public/ 目录,浏览器访问时,/ 代表站点的根目录(即 public/ 文件夹)。因此,如果你在 Markdown 中引用图片时写成:

![我的图片](../images/photo.jpg)

实际生成的 HTML 会是 <img src="../images/photo.jpg">,这显然不是我们期望的位置。正确的方式应该是根相对路径 /images/photo.jpg

PaperMod 主题默认的 render-image.html 已经对页面资源(Page Resources)和全局资源(assets/)进行了智能处理,但如果图片既不是页面资源也不在 assets/ 中,而只是放在 static/ 目录下(经典用法),那么生成的 src 可能是相对路径,导致 404。

因此,我们需要在主题默认逻辑的基础上,增加一个兜底逻辑:当资源找不到时,自动将路径转换为以 / 开头的根相对路径。

覆盖主题模板

Hugo 的模板渲染遵循优先级规则:站点根目录下的 layouts/ 会覆盖主题中的同名文件。所以,我们只需在 layouts/_markup/render-image.html 中放置自定义代码,即可覆盖 PaperMod 的默认实现。

具体做法是:

  1. 在站点根目录下创建 layouts/_markup/ 文件夹(如果不存在)。
  2. 从 PaperMod 主题目录中复制 render-image.html 到该位置。
  3. 基于主题代码进行修改。

创建文件

直接复制可用
博客根目录\layouts\_markup\render-image.html

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
{{- $u := urls.Parse .Destination -}}
{{- $src := $u.String -}}
{{- if not $u.IsAbs -}}
{{- $path := strings.TrimPrefix "./" $u.Path }}
{{- with or (.PageInner.Resources.Get $path) (resources.Get $path) -}}
{{- $src = .RelPermalink -}}
{{- with $u.RawQuery -}}
{{- $src = printf "%s?%s" $src . -}}
{{- end -}}
{{- with $u.Fragment -}}
{{- $src = printf "%s#%s" $src . -}}
{{- end -}}
{{- /* ========== 资源找不到时自动补路径前缀 / ========== */ -}}
{{- else -}}
{{- /* 资源文件不存在,手动处理静态路径 */ -}}
{{- /* 判断路径是否以 / 开头,无则在最前面拼接根斜杠 */ -}}
{{- if not (hasPrefix $path "/") -}}
{{- $path = printf "/%s" $path -}}
{{- end -}}
{{- /* 将处理好带/的路径赋值给图片src */ -}}
{{- $src = $path -}}
{{- /* 还原原链接携带的查询参数 */ -}}
{{- with $u.RawQuery -}}
{{- $src = printf "%s?%s" $src . -}}
{{- end -}}
{{- /* 还原原链接携带的锚点哈希 */ -}}
{{- with $u.Fragment -}}
{{- $src = printf "%s#%s" $src . -}}
{{- end -}}
{{- end -}}
{{- end -}}
{{- $attributes := merge .Attributes (dict "alt" .Text "src" $src "title" (.Title | transform.HTMLEscape) "loading" "lazy") -}}
<img
{{- range $k, $v := $attributes -}}
{{- if $v -}}
{{- printf " %s=%q" $k $v | safeHTMLAttr -}}
{{- end -}}
{{- end -}}>
{{- /**/ -}}

数学公式

选用MathJax(公式语法兼容性更强,复杂定理 /amsmath 友好)

同样新建 layouts/partials/extend_head.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
{{ if or .Params.math .Site.Params.math }}
<script>
MathJax = {
tex: {
inlineMath: [['$','$'], ['\\(', '\\)']],
displayMath: [['$$','$$'], ['\\[','\\]']],
processEscapes: true,
processEnvironments: true,
ams: true // 支持amsmath多行公式环境
},
options: {
// 跳过代码/脚本标签,避免代码里$被误识别为公式
skipHtmlTags: ['script', 'noscript', 'style', 'textarea', 'pre', 'code']
},
svg: {
fontCache: 'global'
}
};
</script>
<script id="MathJax-script" async src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js"></script>
{{ end }}

启用数学公式(两种方式)

方式 1:单篇文章启用(推荐,按需加载 JS,提升速度)

文章头部 front-matter 添加 math: true

1
2
3
4
---
title: "微积分笔记"
math: true
---

方式 2:全站全局启用

config.toml[params] 下设置:

1
2
[params]
math = true

mermaid图表

PaperMod 本身不内置 Mermaid,需要两步模板覆盖实现:

  1. Hugo 自定义 mermaid 代码块渲染器
  2. 在页面头部按需加载 mermaid.js(只在有图表的页面加载,优化性能)

步骤 1:创建 mermaid 代码块渲染模板

在站点根目录新建文件夹,不要修改 themes 里的原文件(方便后续更新主题)

新建文件 layouts/_default/_markup/render-codeblock-mermaid.html

写入内容:

1
2
3
4
<pre class="mermaid">
{{- .Inner | htmlEscape | safeHTML -}}
</pre>
{{ .Page.Store.Set "hasMermaid" true }}

作用:识别 Markdown 的 mermaid 代码块,标记当前页面需要加载 mermaid。

步骤 2:在 extend_head.html 引入 mermaid 脚本(PaperMod 官方扩展点)

新建文件 layouts/partials/extend_head.html

写入完整代码(自动识别 PaperMod 明暗模式切换 mermaid 主题):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
{{ if .Page.Store.Get "hasMermaid" }}
<script src="https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.min.js"></script>
<script>
// 自动跟随站点亮/暗色模式
const darkMode = document.body.classList.contains("dark");
mermaid.initialize({
startOnLoad: true,
theme: darkMode ? "dark" : "default",
securityLevel: "loose",
logLevel: "error"
});

// 切换明暗后自动重绘图表
document.querySelector(".theme-toggle").addEventListener("click", () => {
setTimeout(() => location.reload(), 100);
});
</script>
{{ end }}

CDN 国内慢可替换 jsdmirror 镜像:

https://cdn.jsdmirror.com/npm/mermaid@11/dist/mermaid.min.js

封面

文章 frontmatter写上

1
2
3
4
---
cover:
image: "images/blog-cover.jpg"
---

Cloudflare Pages部署

GitHub推送

上传代码到 GitHub,Cloudflare Pages 依赖 Git 仓库

  1. GitHub 新建公开 / 私有仓库不要勾选 README
  2. 本地关联仓库并推送代码
1
2
3
4
5
6
git init
git add .
git commit -m "初始化博客"
git remote add origin https://github.com/你的用户名/仓库名.git
git remote -v
git push -u origin master

重点:使用 git submodule 安装的主题,推送后仓库会记录子模块地址,CF Pages 会自动拉取完整主题,不会出现样式丢失。

绑定 Git 仓库

  1. 登录 Cloudflare 控制台,左侧菜单打开 Workers & Pages
  2. 点击 Create application → 切换到 Pages → Import a repository(导入现有仓库)
  3. 选择 GitHub,跳转授权页面:
    • 选择你的 GitHub 账号
    • 权限选择:Only select repositories,只勾选你的博客仓库(更安全)
    • 点击 Install & Authorize 授权
  4. 仓库列表选中你的my-blog仓库,点击 Begin setup(开始配置)

参数配置

基础构建设置

配置项 填写值 说明
Production branch(生产分支) main 你推送代码的主分支
Framework preset(框架预设) Hugo 自动识别构建环境
Build command(构建命令) hugo --gc --minify --baseURL $CF_PAGES_URL --gc 清理缓存,--minify 压缩资源;$CF_PAGES_URL 自动适配 pages.dev 预览域名,无需手动改 baseURL
Build output directory(输出目录) public Hugo 默认静态文件输出文件夹
Root directory 留空 博客代码在仓库根目录
Install command 留空;若主题有 JS 依赖填npm ci 纯 Hugo 无需安装依赖

高级设置:添加环境变量(必须配置)

点击 Advanced settings → Add environment variable生产 / 预览环境全部勾选

  1. HUGO_VERSION = 你本地 hugo extended 版本(如 0.164.0

    不填会使用老旧 Hugo 版本,SCSS / 图片渲染报错

  2. HUGO_ENV = production

    开启生产模式,关闭草稿、压缩资源、启用缓存

开始部署

点击 Save and deploy,Cloudflare 会自动拉取代码、安装对应 Hugo、构建站点。

等待 1~3 分钟,构建成功后会生成默认域名:xxx.pages.dev,点击 Visit 即可访问博客。

额外部署避坑要点

  1. 主题使用 Git Submodule 必做

    若你的主题是 submodule 引入,仓库必须提交 .gitmodules 文件,否则构建时拉取不到主题文件,出现 theme not found 报错:

    1
    2
    3
    4
    git submodule update --init --recursive
    git add .gitmodules themes/你的主题文件夹名
    git commit -m "同步主题子模块"
    git push

Hextra主题

下载

  1. Windows下载 Hugo Extended 版本

    winget install Hugo.Hugo.Extended
  2. 安装 Go

    winget install GoLang.Go

    安装完成后重新打开 PowerShell/CMD(让环境变量生效),然后继续

  3. 初始化一个新的Hugo站点

    hugo new site . --format=yaml
  4. 通过模块配置Hextra主题

    • 初始化Hugo模块

      hugo mod init github.com/kika/my-site
    • 添加Hextra主题

      hugo mod get github.com/imfing/hextra
  5. 配置hugo.yaml以使用Hextra主题,添加以下内容:

    1
    2
    3
    module:
    imports:
    - path: github.com/imfing/hextra
  6. 第一篇文章:

    hugo new content/_index.md
  7. 本地预览站点

    hugo server --disableFastRender --cleanDestinationDir
    参数 含义 什么时候用
    --disableFastRender 禁用快速渲染。Hugo 默认只重新渲染你修改过的页面,其他页面从缓存读取。这个参数强制 Hugo 每次请求都完整重新渲染整个站点 改了模板(如 toc.html)后页面没变化,怀疑是缓存导致时
    --cleanDestinationDir 清理目标目录。启动前先把 public/(或你配置的输出目录)整个删掉,再重新生成。 删除了某些文件但残留还在输出目录里,或者想确保从零开始构建时

​ 新站点预览可在http://localhost:1313/查看。

更新

要更新项目中的所有Hugo模块到最新版本,运行以下命令:

hugo mod get -u

要将Hextra更新到最新发布版本,运行以下命令:

hugo mod get -u github.com/imfing/hextra

更多详情请参阅Hugo模块

Stack主题

  • 高亮、公式、mermaid、搜索好像都很齐全

demo:Hugo 主题 Stack

配置:欢迎 | Stack


前置了解

域名

域名都要收费吗

绝大多数域名是收费的,但存在少量免费域名,日常建站基本都用付费域名。

一、付费域名(主流)

.com.cn.net.com.cn.org 这些常见后缀,全部需要按年缴费。

  • 计费方式:按年续费,不能永久买断;你只是租用使用权。
  • 价格:
    • .com:一般 55‑70 元 / 年左右
    • .cn:新人首年很便宜,续费通常 30‑60 元 / 年

⚠️ 到期不续费,域名会被收回,别人可以注册走,网站就打不开。

二、免费域名(不适合正式网站)

  1. 二级免费域名 比如 xxx.tkxxx.cfxxx.ga 这类,还有很多服务商提供的二级域名 xxx.xxx.com
    ✅ 优点:零成本测试玩一玩
    ❌ 缺点:不稳定、随时被收回、搜索引擎权重差、不能过户、广告多,不建议做正式网站、企业项目
  2. 部分云服务商活动赠送
    买服务器 / 主机偶尔送 1 年域名,第二年正常收费,不是永久免费。

三、容易混淆:域名 ≠ 服务器

  • 域名:就是网址名字(如 baidu.com),用来好记;
  • 服务器 / 虚拟主机:存放网站文件,另外收费。

就算你有免费服务器,域名大多还是要花钱。

简单总结

  • 做个人博客、企业官网:老老实实买付费域名,一年几十块,踏实稳定。
  • 只是临时测试练手:可以用免费二级域名玩玩,不要放重要业务数据。

二级免费域名有哪些

可用免费二级域名(2026)

重要提醒:全部只能用于测试、练手、内网穿透演示,不能做正式 / 商业网站;不能备案(国内服务器用不了);所有权不属于你,服务商随时可以收回
⚠️ Freenom(.tk/.cf/.ga)已经不再免费,不要去找这个了。

一、海外免费二级域名(可自定义前缀,可套 Cloudflare)

  1. eu.org 👉 xxx.eu.org

    • 老牌公益,无广告,支持自定义 DNS、DNSSEC

    • 缺点:人工审核 3‑7 天甚至更久,需要写简单用途说明,不是秒过

    • 适合:个人项目、静态站点,不适合急着上线

  2. ClouDNS Free Zone 👉 xxx.abrdns.comxxx.cloud‑ip.cc 等后缀

    • 注册账号直接创建,秒生效,无需审核,支持 DDNS 动态解析

    • 适合:NAS、树莓派、内网穿透、临时测试

  3. DigitalPlat FreeDomain 👉 xxx.us.kg

    • GitHub 账号验证申请,无广告,可以改 NS 到 Cloudflare

    • 适合开发者演示项目

  4. freedns.afraid.org
    大量后缀可选,注册账号认领子域名,解析功能齐全;部分后缀国内访问不稳定。

二、平台附赠子域名(建站 / 托管平台自带,不用单独申请域名)

注册平台账号自动分配,不能转出域名:

  • GitHub Pages:xxx.github.io
  • Vercel:xxx.vercel.app
  • Cloudflare Pages:xxx.pages.dev
  • Netlify:xxx.netlify.app

优点:自带 HTTPS;缺点:域名不能拿出来给别的服务器用。

三、国内 DDNS 类免费二级域名(适合内网穿透)

  1. 花生壳:xxx.vicp.netxxx.xicp.net
  • 中文界面,适合 NAS 远程访问;免费版有流量 / 带宽限制。
  1. Nat123:平台提供若干免费二级域名,搭配内网映射使用。

免费二级域名共同缺点(一定要看)

  1. 无法 ICP 备案,不能绑定国内服务器做公开网站,只能用海外 / 香港服务器、内网穿透演示。
  2. 域名不属于你,服务商政策一变就直接回收,数据、外链全部作废。
  3. SEO 差,百度、谷歌权重低,同后缀如果有垃圾站点会被连带影响。
  4. 微信、支付宝等接口审核经常会拒绝免费域名。

简单选择建议

  • 急着测试、做内网 NAS:ClouDNS / 花生壳
  • 不着急、想要体面一点后缀:eu.org(等审核)
  • 只是写静态网页:直接用 github.io/vercel.app
  • 正式网站:直接买 .com/.cn,一年几十块最稳妥。

域名后缀怎么选

核心原则:商业 / 正式站优先主流后缀;买域名一定要看「续费价」,不要只看首年促销价

怎么选(分场景)

✅ 优先推荐(稳定、信任度高、大部分支持 ICP 备案)

  1. .com|全球通用,所有人第一选择,用户默认会输.com;外贸、企业、个人都适合,续费稳定 55‑70 元 / 年。
  2. .cn|中国国家域名,面向国内用户;企业、个人都能注册;新人首年很便宜,续费约 30‑60 元;可以备案
  3. .com.cn|国内商业备选,支持备案,适合企业。
  4. .net|老通用后缀,适合技术类项目,可备案。

个人博客、测试站:.com/.cn 为主;.xyz/.me 仅做备选,不要做主站。

⚠️ 可以玩,但不做主站(大多不能国内 ICP 备案,只能搭海外 / 香港服务器)

.io`、`.ai`、`.cc`、`.tv`、`.app`、`.dev
  • 优点:科技感强,适合开发者、海外项目
  • 缺点:国内服务器不能备案;部分续费很贵;邮件容易被当成垃圾邮件。

❌ 尽量不要做主站(灰产滥用多、信任差、收录差)

.win.pw.party.loan.icu.bid这类低价新后缀 很多被钓鱼、垃圾站大量使用,微信、小程序容易拦截,搜索引擎隐性降权;只适合临时测试玩一玩。

中文后缀 .中国:只适合品牌保护,不适合做主站,用户很难输入。

快速判断能不能备案

买之前看平台页面标注:支持 ICP 备案。不在工信部白名单,再便宜也不能绑国内服务器,备案直接驳回。

八大套路

1.「首年几块钱,续费暴涨」最大坑

广告:6 元域名! 真相:首年活动价,第二年续费直接 100‑200 元。

✅ 怎么防:下单前点开详情页看「续费价格」,不要只看注册价。长期建站,优先选续费稳定的.com/.cn。

  1. 结账页面默认勾选一堆付费服务

结算时悄悄勾选:虚拟主机、企业邮箱、SSL 证书、域名隐私保护、防劫持服务。 明明只想买域名,结账多花几十上百。

✅ 操作:提交订单前,把所有不需要的附加服务全部取消勾选

3.WHOIS 隐私保护单独收费坑

域名实名信息默认公开,会收到大量推销、诈骗邮件。 部分小平台把「信息隐藏」做成付费增值服务。

✅ 正规大厂现在域名隐私保护免费,优先选免费的服务商。

  1. 域名到期赎回天价

域名到期后不是立刻删除,有赎回期:

  • 到期→续费宽限期(正常价续费)
  • 再过期:进入赎回期,赎回费几百元,非常贵。

✅ 一定要开启自动续费,记好到期时间,不要赌 “过期重新注册”。

  1. 新注册 60 天不能转出锁死规则(ICANN 强制)

刚注册、刚转入的域名,强制锁定60 天,不能迁移到别的服务商。 如果踩了黑心小平台,前两个月你没法搬走,只能任它涨价。

不要在不知名小代理商买重要域名。

  1. 溢价域名坑(Premium 高价域名)

有些短词、好听名字是注册局设置溢价域名:注册就几百上千,每年续费也是高价,不是只买一次。很多人以为只贵第一年,年年都要高价续费。

✅ 看清页面标注「溢价域名」,确认注册价 + 续费价再下单。

  1. 二手 / 抢注域名历史污点坑

买二手域名,一定要查历史记录。 如果以前做过赌博、诈骗,就算你买来做正规站,搜索引擎已经拉黑,很难恢复收录,甚至仲裁被收回

新手尽量直接注册全新域名,少碰二手。

  1. 疯狂推销注册一堆后缀

销售劝你:把 .com .cn .net .top .xyz 全部买下来保护品牌。 普通个人 / 小公司完全没必要,只把主站后缀搞定即可,多余就是浪费钱。

三、实操购买小清单(照着做)

  1. 优先大厂域名注册商(阿里云、腾讯云等),不选不知名小网站。
  2. 搜域名,重点看:注册价、续费价、是否支持备案
  3. 不要勾选多余附加服务,隐私保护打开。
  4. 确认不是溢价域名。
  5. 开启自动续费,记好到期时间。
  6. 正式网站:优先 .com > .cn;个人测试可以用小众后缀。

简单总结

做正经网站,不要贪首年超低价。续费才是长期真实成本。后缀再花哨,不如一个 .com/.cn 省心。