未命名
0. 前言:为什么要这么折腾?
很多朋友使用思源笔记写文章,想直接发布到 Hugo 博客。但直接使用思源的发布插件往往会遇到图片路径不对、Front Matter(文章元数据)格式不兼容等问题。
为了解决这个问题,我设计了一套“双分支”工作流:
- Raw 分支:作为“生肉”仓库,接收本地和思源笔记推送的原始文件。
- GitHub Actions:充当“中央厨房”,自动清洗数据、修复格式。
- Main 分支:作为“熟肉”仓库,存放处理好的、干净的 Hugo 源码。
- Cloudflare Pages:直接拉取 Main 分支进行构建发布。
这篇教程的核心逻辑在于理清 数据流向:
本地/思源 -> (Push) -> GitHub Raw 分支 -> (Actions 清洗) -> GitHub Main 分支 -> (Cloudflare 监听) -> 自动构建部署。
如果你只是单纯想建一个网站,也可以参考这篇文章的内容。这个教程更为简单的就实现了一个网站的搭建,如果后期你想要与思源笔记配合,可以再看看本篇文章。

1. 环境准备
-
本地环境:
- Git(版本控制工具)
- Hugo Extended 版本(用于本地预览,推荐 Extended 版以支持更多功能)
- VS Code(推荐的代码编辑器)
-
账号准备:
- GitHub 账号
- Cloudflare 账号
2. 第一步:本地部署 Hugo 与目录结构解析
2.1 初始化 Hugo 站点
在你的电脑上打开终端(CMD/PowerShell/Terminal):
|
|
2.2 详解 Hugo 目录结构
一个标准的 Hugo 目录是这样的:
-
content/:核心目录。你的文章(Markdown 文件)都放在这里。content/posts/:通常建议建个子目录存放博文。
-
static/:存放图片、CSS 等静态资源。思源发布的图片通常需要映射到这里。 -
themes/:存放主题文件。 -
hugo.toml:整个网站的配置文件。
如果你不知道目录的详细内容,你可以参考Github上hugo主题的示例网站,里面会有详细的目录。里面每一个目录都会有文件告诉你应该放什么更改什么。
3. 第二步:GitHub 仓库与双分支设置(关键!)
3.1 创建远程仓库
去 GitHub 创建一个新的 Repository(比如叫 blog-source),设为 Public 或 Private 均可。
3.2 本地分支重命名与推送
我们需要把本地默认分支设为 raw。
|
|
3.3 创建 Main 分支(用于存放清洗后的代码)
在 GitHub 页面上,基于 raw 分支新建一个 main 分支;或者在本地创建并推送到远程,确保远程有两个分支:raw 和 main。
- 注意:在 GitHub 仓库设置(Settings -> General)里,建议把
raw设为默认分支(Default Branch),方便查看源码。
4. 第三步:配置 GitHub Actions(自动化核心)
这是连接两个分支的桥梁。我们需要在项目中创建 Workflow 文件。
4.1 创建文件
在本地项目根目录创建路径: .github/workflows/process-sync.yml
4.2 编写配置(核心逻辑)
(这里重点解释逻辑,脚本部分由你自行填充)
文件内容结构如下:
YAML
|
|
5. 第四步:Cloudflare Pages 自动构建
5.1 绑定 GitHub
登录 Cloudflare Dashboard -> Pages -> Create a project -> Connect to Git -> 选择你的仓库 blog-source。
5.2 构建配置(重点)
- Production branch(生产分支) :一定要选
main(因为那是清洗后的干净代码)。 - Framework preset:选择
Hugo。 - Build command:
hugo(如果主题较新,建议加上--minify)。 - Build output directory:
public。
点击 Save and Deploy。以后只要 main 分支变动,CF 就会自动更新网站。
如果你要使用自己的域名,需要在自定义域中选择绑定自己的域名。否则就是cloudflare的默认域名(免费的)。
6. 第五步:实际使用演示
场景一:本地手写(Geek 模式)
- 在 VS Code 打开项目。
hugo new posts/hello-world.md。- 编辑文章,设置
draft: false。 git add .->git commit->git push origin raw。- 结果:Actions 自动清洗推送到 main,CF 自动构建上线。
场景二:思源笔记发布(插件模式)
- 在思源中配置发布插件。
- Git 仓库地址:填你的 GitHub 仓库。
- 分支(Branch) :填
raw。 - 填入 GitHub Access Token (PAT)
- 保存路径:对应 Hugo 的
content/posts。 - 点击发布。
- 结果:同上,所有繁琐的格式清洗都由 GitHub Actions 在云端默默完成了。
🟢 补充教程:如何获取 GitHub Access Token (PAT)
在配置思源笔记的发布插件时,你不能使用 GitHub 的登录密码,必须使用 Personal Access Token (PAT) 。以下是获取步骤:
1. 进入开发者设置
登录你的 GitHub 账号,点击右上角的头像,在下拉菜单中点击 Settings(设置)。
在设置页面最左侧的侧边栏,一直拉到底部,点击 Developer settings(开发者设置)。
2. 选择正确的 Token 类型(关键点!)
在左侧菜单中选择 Personal access tokens。

⚠️ 注意: 这里会出现两个选项,请务必选择 Tokens (classic)。
- 原因:虽然 Fine-grained 是新版,但目前很多第三方插件对新版 Token 的权限细分支持不够完美,选择 Classic(经典版)兼容性最好,最不容易报错。
点击右侧的 Generate new token -> 选择 Generate new token (classic) 。

3. 配置 Token 权限
进入创建页面后,按照以下填写:
-
Note(备注): 随便填,例如
SiYuan-Blog,方便你以后知道这个 Token 是干嘛的。 -
Expiration(过期时间):
- 如果你不想每隔几个月就重新配置一遍,建议选择 No expiration(永不过期) 。
- GitHub 会弹窗警告你这样不安全,点击确认即可(因为只用于你自己的博客仓库)。
-
Select scopes(选择权限范围): 这是最重要的一步!
- 请找到
repo这一行。 - 勾选
repo旁边的主复选框(Full control of private repositories)。 - 勾选这一个大项通常就足够了,它包含了对仓库的读写权限,足以支持插件推送代码。
- 请找到
4. 生成并复制
-
拉到页面最底部,点击绿色按钮 Generate token。
-
🚨 红色警报: 页面会刷新并显示一串以
ghp_开头的字符串。- 立刻复制这一串字符!
- 立刻保存到你的记事本里!
- 一旦你刷新或关闭这个页面,你将永远无法再次看到这个 Token(如果丢了只能重新生成)。
🔵 在思源发布插件中填入 Token
回到思源笔记,打开你的发布插件配置界面(以 siyuan-blog-github 或类似插件为例):
- Token:粘贴刚才复制的那串
ghp_xxxx...字符。 - 仓库 (Repo) :填写
用户名/仓库名(例如yourname/my-blog)。 - 分支 (Branch) :填写
raw(千万别填 mian,记得我们是推送到生肉分支)。 - 路径 (Path) :通常填
content/posts(根据你的 Hugo 目录结构定)。 - 点击“检测”或“保存”,如果提示成功,说明连接已打通!
7. 避坑指南(FAQ)
-
Q: Cloudflare 构建失败怎么办?
- A: 检查 Hugo 版本环境变量。在 CF Pages 设置里添加
HUGO_VERSION,设为0.120.0或更高。
- A: 检查 Hugo 版本环境变量。在 CF Pages 设置里添加
-
Q: 为什么思源图片不显示?
- A: 检查中间脚本是否正确将图片移动到了
static目录,或者修正了 Markdown 里的引用路径。
- A: 检查中间脚本是否正确将图片移动到了
-
Q: 用于洗版的PY脚本?
-
1. 脚本功能总结
这个脚本会自动遍历
content/post目录下的所有.md文件,并执行以下操作:-
清洗不可见字符:删除零宽空格(
\u200b),防止解析错误。 -
强制表格规范化:
- 修复断行:如果表格上方没有空行,强制插入一个空行(Markdown 标准要求)。
- 重构结构:自动删除空列,对齐表格内容。
- 强制左对齐:将所有表格的对齐方式强制改为
:---(左对齐)。
-
Front Matter(文章头信息)提取与整理:
- 封面图:如果 Front Matter 里没图,但正文第一张图是网络图片(http开头),则提取它作为封面图(
image字段),并从正文删除。 - 标题:提取正文中的 H1 (
# 标题)。逻辑是优先取第二个 H1,如果没有则取第一个。提取后存入title字段并从正文删除。 - 标签:识别正文中的
Tags: xxx或标签: xxx,提取为 Front Matter 的tags列表,并从正文删除。
- 封面图:如果 Front Matter 里没图,但正文第一张图是网络图片(http开头),则提取它作为封面图(
-
格式美化:去除多余的连续空行。
-
-
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 243import os import re # ================= 配置区域 ================= # 配置:文章所在的根目录,脚本会递归遍历该目录 POSTS_DIR = 'content/post' def fix_markdown_files(): """ 主入口函数:遍历目录并处理文件 """ print(">>> 正在执行 V10.0 标准格式版 (强制 :--- 左对齐 + 插入空行)...") # os.walk 递归遍历目录 for root, dirs, files in os.walk(POSTS_DIR): for file in files: # 只处理 .md 文件,且忽略以 _index 开头的文件(通常是 Hugo 的列表页配置) if file.endswith('.md') and not file.startswith('_index'): filepath = os.path.join(root, file) process_file(filepath) def process_file(filepath): """ 核心处理逻辑:读取单个文件 -> 清洗 -> 提取元数据 -> 重组表格 -> 保存 """ try: # 使用 utf-8 读取文件 with open(filepath, 'r', encoding='utf-8') as f: content = f.read() except Exception as e: print(f"读取失败: {filepath} - {e}") return is_modified = False # 标记文件是否被修改过 # 1. 【清洗】去除零宽空格 (Zero-width space) # 这种字符通常来自于从网页复制粘贴,会导致编辑器或解析器报错 if '\u200b' in content: content = content.replace('\u200b', '') is_modified = True # 分割 Front Matter (头部 YAML) 和正文 (Body) # split('---', 2) 会切分成 ['', 'Front Matter内容', '正文内容'] parts = content.split('---', 2) if len(parts) < 3: return # 如果不是标准的 Hugo/Hexo 格式,跳过 front_matter, body = parts[1], parts[2] # 2. 【表格处理】重构表格 + 修复表格前的空行 new_body_lines = [] body_lines = body.split('\n') in_table = False # 状态机:是否正在读取表格 table_buffer = [] # 缓存表格行 for i, line in enumerate(body_lines): stripped = line.strip() # 判断当前行是否是表格行 (以 | 开头) if stripped.startswith('|'): if not in_table: # === 发现表格开始 === # 【关键修复】检查上一行是否为空行 # Markdown 标准要求表格前必须有空行,否则可能无法渲染 if new_body_lines and new_body_lines[-1].strip() != "": new_body_lines.append("") in_table = True table_buffer = [] # 将当前行加入表格缓存 table_buffer.append(line) else: if in_table: # === 表格结束 === # 调用重构函数处理刚才缓存的表格 reconstructed = reconstruct_table(table_buffer) # 检查重构前后内容是否一致,不一致说明表格被优化了 if "".join(reconstructed) != "".join(table_buffer): print(f" [表格] 重构为标准格式: {os.path.basename(filepath)}") is_modified = True # 将重构后的表格行加入新正文 new_body_lines.extend(reconstructed) # 重置状态 in_table = False table_buffer = [] # 当前这一行不是表格,直接加入(例如表格后的文字) new_body_lines.append(line) else: # 普通文本行,直接加入 new_body_lines.append(line) # 处理文件以表格结尾的情况 (EOF) if in_table and table_buffer: reconstructed = reconstruct_table(table_buffer) if "".join(reconstructed) != "".join(table_buffer): is_modified = True new_body_lines.extend(reconstructed) # 如果在步骤2中修改了内容,更新 body 变量 if is_modified: body = '\n'.join(new_body_lines) # 3. 【封面图提取】 # 如果 Front Matter 里没有 image 字段 if not re.search(r'^image:\s*http', front_matter, re.MULTILINE): # 查找正文中的第一张网络图片 ![] (http...) match = re.search(r'!\[.*?\]\((http.*?)\)', body) if match: # 清理旧的 image 字段(如果有的话) front_matter = re.sub(r'^image:.*\{\{.*\}\}.*$', '', front_matter, flags=re.MULTILINE).strip() # 添加新的 image 字段 front_matter += f'\nimage: {match.group(1)}' # 从正文中删除这张图片的代码 body = body.replace(match.group(0), '', 1) is_modified = True # 4. 【标题提取】 # 查找所有的 H1 标题 (# Title) h1_matches = list(re.finditer(r'^\s*#\s+(.+?)\s*$', body, re.MULTILINE)) target_title = "" # 逻辑:有些博客习惯第一个 # 是站点名,第二个 # 才是文章名 if len(h1_matches) >= 2: target_title = h1_matches[1].group(1).strip() elif len(h1_matches) == 1: target_title = h1_matches[0].group(1).strip() if target_title: # 转义标题中的双引号,防止 YAML 语法错误 safe_title = target_title.replace('"', '\\"') # 如果 Front Matter 已有 title,替换它;否则追加 if re.search(r'^title:', front_matter, re.MULTILINE): front_matter = re.sub(r'^title:.*$', f'title: "{safe_title}"', front_matter, flags=re.MULTILINE) else: front_matter += f'\ntitle: "{safe_title}"' # 从正文中删除所有的 H1 标题,避免重复显示 body = re.sub(r'^\s*#\s+(.+?)\s*$', '', body, flags=re.MULTILINE) is_modified = True # 5. 【标签提取】 # 如果 Front Matter 没有 tags if not re.search(r'^tags:', front_matter, re.MULTILINE): # 在正文中查找 "Tags: a, b, c" 或 "标签: a, b, c" match = re.search(r'^(?:Tags|标签)[::]\s*(.*)$', body, re.MULTILINE | re.IGNORECASE) if match: # 按逗号、顿号、空格分割标签 items = [t.strip() for t in re.split(r'[,,、\s]+', match.group(1)) if t.strip()] if items: # 格式化为 YAML 列表格式 front_matter += "\ntags:\n" + "\n".join([f" - {t}" for t in items]) # 从正文中删除这一行 body = body.replace(match.group(0), '') is_modified = True # 6. 【保存文件】 if is_modified: # 将正文中连续的3个以上换行符替换为2个(去除多余空行) body = re.sub(r'\n{3,}', '\n\n', body.strip()) # 重新拼接文件内容 new_content = f'---{front_matter}\n---\n\n{body}' with open(filepath, 'w', encoding='utf-8') as f: f.write(new_content) def reconstruct_table(lines): """ 表格重构核心逻辑: 1. 解析 Markdown 表格为矩阵 2. 删除没有任何内容的列 3. 重新生成表格字符串,强制使用 :--- 左对齐 """ if not lines: return lines # 1. 过滤有效行:排除掉原本的分隔线行(例如 |--|--|) # 这一步会把原本的对齐方式丢弃,准备重新生成 content_lines = [] for line in lines: # 正则含义:如果行只包含 空格、|、-、:、+,则认为是分隔线,不存入 content_lines if not re.match(r'^[\s\|\-\:\–\—\+]+$', line.strip()): content_lines.append(line) if not content_lines: return lines # 2. 解析矩阵 matrix = [] for line in content_lines: # 按 | 切割单元格 cells = line.strip().split('|') # 去除首尾因为 split 产生的空元素 (比如 "| a | b |" split后首尾会有空字符) if len(cells) > 0 and cells[0].strip() == '': cells.pop(0) if len(cells) > 0 and cells[-1].strip() == '': cells.pop(-1) matrix.append(cells) # 3. 识别有效列(如果某一列在所有行里都是空的,就丢弃该列) max_cols = 0 if matrix: max_cols = max(len(row) for row in matrix) cols_to_keep = [] for c in range(max_cols): is_empty = True for row in matrix: if c < len(row) and row[c].strip(): is_empty = False break if not is_empty: cols_to_keep.append(c) # 4. 重组表格文本 new_lines = [] # A. 写入表头 (第一行) header_row = matrix[0] new_header = [] for c in cols_to_keep: cell = header_row[c].strip() if c < len(header_row) else "" new_header.append(cell) new_lines.append("| " + " | ".join(new_header) + " |") # B. 生成分隔线 (强制左对齐 :---) # 如果你想改对齐方式,改这里,例如 ":-:" 是居中 separators = [":---"] * len(cols_to_keep) new_lines.append("| " + " | ".join(separators) + " |") # C. 写入数据行 (从第二行开始) for row in matrix[1:]: new_data = [] for c in cols_to_keep: cell = row[c].strip() if c < len(row) else "" new_data.append(cell) new_lines.append("| " + " | ".join(new_data) + " |") return new_lines if __name__ == "__main__": fix_markdown_files() print(">>> V10.0 处理完成")
-
-
最后是一个标准的md文件,仅供参考
1 2 3 4 5 6 7 8 9 10 11--- date: 2024-01-21 image: http://example.com/banner.jpg title: "Python 自动化指南" tags: - Python - 脚本 - 效率 --- 这是正文内容。这里隐藏了一个零宽空格。