怎么搭建这个Blog

这是一个基于 Hexo + Markdown + GitHub Pages 的个人博客方案,部署成本低、维护简单、主题生态丰富。

在真正动手前,先来看一下整个系统是如何协作的:

  • Hexo 是一个 静态博客生成器,你只负责写 Markdown 文件,Hexo 会把它们转换成 HTML 页面。
  • GitHub Pages 是一个 免费静态网站托管服务,负责把生成好的网页放到公网。
  • 主题(如本网站使用的 Fluid)决定博客的外观和交互。
  • Git 负责把你本地生成的页面同步到 GitHub。

整个流程就是:你写 Markdown → Hexo 生成网页 → Git 推送 → GitHub Pages 自动上线

接下来正式进入部署步骤

一、创建 Hexo 博客站点

1.全局安装 Hexo

1
pnpm install -g hexo-cli

2. 初始化博客目录

选择一个文件夹作为你存放整个博客的目录,然后在这个目录下执行初始化命令:

1
hexo init my-blog(换成你喜欢的任何名字)

接下来进入目录,安装依赖

1
2
cd my-blog
pnpm install

执行完后,目录结构会变成这样:

1
2
3
4
5
6
7
8
.
├── _config.yml ← 网站总配置
├── package.json
├── scaffolds
├── source
│ ├── _drafts
│ └── _posts ← 所有博客文章
└── themes ← 博客主题

这里你需要记住三件事:

  • 文章只写在 source/_posts
  • 网站行为改 _config.yml
  • 样式和布局在 themes

3. 本地预览博客

创建第一篇文章:

1
hexo new "hello-world"

通过上面这个命令生成的 md 文件自带文件头。本方案搭建的博客,每个 md 文件都需要一个文件头才能识别,显示标题,分类等等。

如果你已经有了很多的 md 文件(跟我一样),我会在最后提供一个思路给你。

启动本地服务器:

1
hexo server

浏览器访问:

1
http://localhost:4000

如果能看到网页,说明你的本地博客环境已经完全成功。

二、GitHub Pages 部署

1. 创建 GitHub Pages 仓库

在 GitHub 新建仓库,名称必须是

1
你的用户名.github.io

这是 GitHub Pages 的硬性规则。

3. 配置 Hexo 部署信息

编辑博客根目录下的 _config.yml

1
2
3
4
deploy:
type: git
repo: git@github.com:你的用户名/你的用户名.github.io.git
branch: main

注意两点:

  • repo 必须是 git@github.com 形式
  • branch 要和 GitHub 默认分支一致(现在通常是 main

4. 安装部署插件并发布

1
pnpm install hexo-deployer-git

然后执行:

1
2
3
hexo clean
hexo generate
hexo deploy

几分钟后访问:

1
https://你的用户名.github.io

博客就正式上线了。

三、主题选择与美观优化

如果你觉得本博客的主题不错,可以参考这里的教程,如果你喜欢其它风格,也可以自己探索。

安装 Fluid 主题

1
pnpm install hexo-theme-fluid

从 Hexo 5.0 开始,官方引入了 NPM 包管理机制。当你使用 pnpm install hexo-theme-fluid 命令时:

  1. pnpm 会向上寻找你的 package.json 文件
  2. 然后,它会把 Fluid 当作一个普通的依赖包,下载到了 ...\my-blog\node_modules\hexo-theme-fluid 里面。

所以,现在的 themes 文件夹通常是空的。但 Hexo 非常聪明,如果它在 themes 目录找不到主题,会自动去 node_modules 里面找。

怎么启用主题呢?

打开你博客根目录的 _config.yml,找到 theme 字段并修改为 fluid。

为了安全地修改主题配置,你需要把 Fluid 的默认配置文件“抽离”到外层。请在你的博客根目录下,手动新建一个文件,命名为 _config.fluid.yml

然后,去 node_modules/hexo-theme-fluid/_config.yml 中,把里面的所有内容 全选复制粘贴 到你刚刚新建的 _config.fluid.yml 中。

如果你想要进行一些自定义的操作,修改这里的配置文件即可。尽情探索吧!

附:批量给文件添加头部

其实这个事情还是比较简单,只是添加基础的头部的话,只需要让 AI 写个 Python 脚本,把文件名当成 title,配合固定的格式,加入即可。但是这样是不能分类和加 tag 的。

但是既然现在 AI 都这么好用了,为何不更进一步呢?

你可以再改进一下脚本,把文件名,和文章前几百个字,一起发给 AI,让它分析,给出它认为最合适的分类和 tag。

如果你和我一样,维护着一个比较大而全,比较杂的笔记仓库,同时又维护着一个博客,想要放一些更“垂直”的文章,又不想复制粘贴,毕竟时不时就会更新,同步起来太痛苦。

我目前的做法是,维护一个“精选列表”,其实就是一个 txt 文本文件,其中每一行的内容都是“博客文章名|笔记的完整路径”,然后写个脚本,每次只需要检查两个文件的 md5 是否相同,为了方便,我选择的是直接用仓库中的文件覆盖博客的文章。

这样看起来有个问题是吧?覆盖了,原本的文件头不就丢失了?

确实是这样的,所以我(AI)对 AI 添加文件头的脚本进行了简单的小改造,加了一个缓存功能,这样就只会计算一次文件头,毕竟,对于文章内容的部分改动,基本不会影响到这篇文章的定位。若是进行了大改,也只需要去缓存文件里,删除掉这个文件的缓存即可。

附上脚本:

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
import os
import re
import time
import json
import hashlib
import threading
import openai
from datetime import datetime
import concurrent.futures
from openai import OpenAI

# ================= 配置区 =================
API_KEY = os.environ.get("DASHSCOPE_API_KEY", "sk-")
BASE_URL = "https://"
MODEL_NAME = ""
POSTS_DIR = r"source/_posts"
CACHE_FILE = "process_cache.json"

MAX_WORKERS = 7
MAX_RETRIES = 3
# ==========================================

client = OpenAI(api_key=API_KEY, base_url=BASE_URL)
YAML_PATTERN = re.compile(r'^---.*?\r?\n(.*?)\r?\n---.*?\r?\n', re.DOTALL)

print_lock = threading.Lock()
cache_lock = threading.Lock()

def safe_print(message):
with print_lock:
print(message)

def get_content_md5(content):
return hashlib.md5(content.encode('utf-8')).hexdigest()

def load_cache():
if os.path.exists(CACHE_FILE):
try:
with open(CACHE_FILE, 'r', encoding='utf-8') as f:
return json.load(f)
except Exception:
return {}
return {}

def save_cache(cache_data):
with open(CACHE_FILE, 'w', encoding='utf-8') as f:
json.dump(cache_data, f, indent=4, ensure_ascii=False)

def get_ai_metadata_with_retry(filename, content_snippet):
clean_name = os.path.splitext(filename)[0]
prompt = f"""
请根据以下 Markdown 笔记的【文件名】和【正文前几行】,推断最合适的 1 个大分类(categories)和 1-3 个标签(tags)。

文件名: {clean_name}
正文片段: {content_snippet}
请以纯 JSON 格式输出,包含 "category" (字符串) 和 "tags" (字符串数组) 两个字段。
"""

for attempt in range(MAX_RETRIES):
try:
response = client.chat.completions.create(
model=MODEL_NAME,
messages=[
{"role": "system", "content": "你是一个严谨的博客分类助手。只能输出合法的 JSON 格式。"},
{"role": "user", "content": prompt}
],
temperature=0.1,
response_format={ "type": "json_object" },
timeout=15
)

data = json.loads(response.choices[0].message.content.strip())
category = data.get('category', '未分类')
tags = data.get('tags', ['待整理'])

yaml_lines = ["categories:", f" - {category}", "tags:"]
for tag in tags:
yaml_lines.append(f" - {tag}")
return "\n".join(yaml_lines)

except openai.RateLimitError as e:
# 429 限流错误:增加退避时间(基础时间更长)
wait_time = (2 ** attempt) + 2
safe_print(f" [{filename}] 触发限流 (429),等待 {wait_time} 秒后重试...")
time.sleep(wait_time)

except openai.AuthenticationError as e:
# 401 鉴权错误:API Key 失效,直接放弃,无需重试
safe_print(f" [{filename}] 致命错误(API Key失效),放弃重试。")
break

except openai.BadRequestError as e:
# 400 请求错误:参数传错了,直接放弃
safe_print(f" [{filename}] 请求格式错误,放弃重试: {e}")
break

except (openai.APIConnectionError, openai.InternalServerError) as e:
# 5xx 服务端错误或网络抖动:标准指数退避
wait_time = 2 ** attempt
safe_print(f" [{filename}] 网络/服务端异常,等待 {wait_time} 秒: {e}")
time.sleep(wait_time)

except Exception as e:
# 其他未知错误
wait_time = 2 ** attempt
safe_print(f" [{filename}] 未知异常,等待 {wait_time} 秒: {e}")
time.sleep(wait_time)

return "categories:\n - 未分类\ntags:\n - 待整理"

def process_single_file(filename, global_cache):
filepath = os.path.join(POSTS_DIR, filename)

try:
with open(filepath, 'r', encoding='utf-8') as f:
full_content = f.read()

match = YAML_PATTERN.match(full_content)
if match:
old_yaml = match.group(1)
raw_body = full_content[match.end():].strip()
date_match = re.search(r'^date:\s*(.+)$', old_yaml, re.MULTILINE)
date_str = date_match.group(1).strip() if date_match else None
else:
raw_body = full_content.strip()
date_str = None

# 计算当前正文的 MD5
current_md5 = get_content_md5(raw_body)

# 兼容旧版本缓存或初始化新缓存结构
file_cache = global_cache.get(filename, {})
if isinstance(file_cache, str):
# 如果读到的是旧版纯 MD5 字符串,转换成字典结构
file_cache = {"md5": file_cache, "yaml": None}

cached_md5 = file_cache.get("md5")
cached_yaml = file_cache.get("yaml")

# 1. 没有任何修改,直接跳过
if cached_md5 == current_md5 and match:
return "skipped"

# 如果没有时间,取文件创建时间
if not date_str:
ctime = os.path.getctime(filepath)
date_str = datetime.fromtimestamp(ctime).strftime('%Y-%m-%d %H:%M:%S')

# 2. 核心逻辑:MD5变了,或者文件头没了,但我们有缓存的 YAML!(恢复模式)
if cached_yaml:
ai_metadata = cached_yaml
status_msg = f" [{filename}] 检测到微小修改,已从缓存恢复文件头 (免Token)"
result_code = "restored"
# 3. 彻底的全新文件,或者没有缓存,呼叫 AI
else:
content_snippet = raw_body[:500].replace('\n', ' ')
ai_metadata = get_ai_metadata_with_retry(filename, content_snippet)
status_msg = f" [{filename}] AI 处理成功并生成全新头部"
result_code = "success_ai"

title = os.path.splitext(filename)[0]
new_front_matter = f"---\ntitle: {title}\ndate: {date_str}\n{ai_metadata}\n---\n\n"

# 原子化写入
temp_filepath = filepath + ".tmp"
with open(temp_filepath, 'w', encoding='utf-8') as f:
f.write(new_front_matter + raw_body)
os.replace(temp_filepath, filepath)

# 更新缓存库(保存新 MD5 和 YAML)
with cache_lock:
global_cache[filename] = {
"md5": current_md5,
"yaml": ai_metadata
}

safe_print(status_msg)
return result_code

except Exception as e:
safe_print(f" [{filename}] 错误: {e}")
return "error"

def main():
start_time = time.time()
safe_print("=== 2. 开始检查文件头与内容更新 ===")
global_cache = load_cache()

if not os.path.exists(POSTS_DIR):
safe_print(f"错误:找不到目录 {POSTS_DIR},请检查路径。")
return

files = [f for f in os.listdir(POSTS_DIR) if f.endswith(".md")]
safe_print(f" 扫描到 {len(files)} 篇笔记,缓存库中已记录 {len(global_cache)} 条数据。")

stats = {"success_ai": 0, "restored": 0, "error": 0, "skipped": 0}

with concurrent.futures.ThreadPoolExecutor(max_workers=MAX_WORKERS) as executor:
future_to_file = {executor.submit(process_single_file, f, global_cache): f for f in files}

for future in concurrent.futures.as_completed(future_to_file):
result = future.result()
stats[result] += 1

save_cache(global_cache)

end_time = time.time()
safe_print("\n" + "="*40)
safe_print(f" 全部处理完成!耗时: {round(end_time - start_time, 2)} 秒")
safe_print(f" 统计: AI新生成 {stats['success_ai']} 篇, 缓存恢复 {stats['restored']} 篇, 跳过 {stats['skipped']} 篇, 失败 {stats['error']} 篇")

if __name__ == "__main__":
main()

怎么搭建这个Blog
https://gavinmo1.github.io/2026/03/18/怎么搭建这个Blog/
作者
Gavin
发布于
2026年3月18日
许可协议