前言 #
Hugo 是一个强大的静态网站生成器,使用 Go 语言编写,它以速度快、灵活性强、易于扩展而著称,非常适合用来搭建个人博客。相较于传统的动态博客系统(如 WordPress、halo、Typecho),Hugo 无需数据库,生成静态页面即可上线,加载迅速、维护简单。
可是 Hugo 的上手过程对初学者来说往往有些门槛:安装环境、配置主题、内容组织、评论系统、自动化部署……环环相扣,若缺乏系统化指引,很容易在查看教程文档的时候如堕烟海、罔知所措!
因此,在我从零入手研究 Hugo 的同时也编撰整理了这篇文章;阅读本文将完整走一遍 Hugo 搭建个人博客的全过程。
本文优势:
- 零基础友好:从环境准备到第一篇博文上线,详细说明,循序渐进
- 结构清晰:以路线图形式展开,涵盖 安装 → 配置 → 内容管理 → 评论 → 自动部署 → CMS 后台 的完整流程。
- 可直接复用:文中提供了大量示例配置文件、命令行代码和脚本工具,拷贝即用,大幅节省配置时间。
- 进阶扩展:相较其他的 Hugo 教程,本文丰富详细,介绍关键所需!涵盖主题定制、评论系统、自动化 CI/CD、无头 CMS 后台内容管理等,让博客不止能“跑起来”,还能“跑得更漂亮”。
- 实用工具推荐:不仅介绍 Hugo,还配合介绍高效编辑器、AI 助手、批量管理工具,提升写作与维护体验。
前置条件 #
环境 #
git:用于版本管理和后续上传GitHub。git -v:检测安装
Go:使用 Hugo 模块功能所必须。go version :检测安装
Hugo:官方安装文档对不同系统提供了不同的安装方法可以看一下;其中最简单是直接下载 GitHub 提供的最新预编译二进制包,解压文件夹中有可执行文件,移动至目标目录并把该目录添加到 PATH 环境变量。hugo version:检测安装
AI:(强烈推荐),用于 vibe coding ,对于 Hugo 刚开始使用的新手小白很有用,不仅能解释 Hugo 相关的概念和设置,还能快速帮你设置和修改。推荐使用 Claude code / Gemini CLI (命令行形式)、或 augment / cursor (IDE插件形式)。以及搭配 Context7 MCP 工具查询 hugo 文档。
编辑器 #
Typora:markdown 编辑器,特点:随用随开,简洁高效、专注与 markdown 写作
Obsidian:markdown 编辑器,特点:批量高效处理 md文档,适合结构化、层次化的项目
VSCode:项目文件管理&编辑器,宇宙第一编辑器的含金量不用多说了吧~
开始安装 #
首先是要选择一个主题,这里我使用 Congo 为例。其他主题可以看官方主题列表和 GitHub ( 这里我也总结和罗列了一些 Hugo 主题,你可以挑选一下)。使用其他主题开始也是没问题的,安装看对于的主题文档,主要概念大差不差。
初始化 #
安装 Hugo 并初始化博客 #
# 检查hugo 是否正常安装
hugo version
# 创建目录`myblog`作为一个新的hugo站点
hugo new site myblog
cd myblog
初始化 Hugo 模块并添加主题 #
根据 Congo 的安装文档,使用 GO 所支持的 Hugo 模块是最方便的安装方式
# 本地初始化模块
hugo mod init myblog
通过创建一个新文件 config/_default/module.toml 并添加以下内容,将主题添加到配置中:
[[imports]]
path = "github.com/jpanther/congo/v2"
使用 hugo server 启动hugo服务器,自动下载主题,并且打开 http://localhost:1313/ 就能看到博客网页了
主题配置文件 #
config/_default/ 这作为 hugo 主题配置的文件夹,所有的设置配置都在这里。
首先下载 Congo 主题配置示例文件作为参考 下载副本
其次删除根目录的配置文件hugo.toml或者config.toml,使用config/_default/hugo.toml配置网站基础配置。复制示例文件到config/_default/这个目录里(不要覆盖module.toml!),最终如下:
config/_default/
├─ hugo.toml # 站点基础配置
├─ languages.en.toml # 语言特定配置
├─ markup.toml # 标记语言设置
├─ menus.toml # 菜单配置
├─ module.toml # 模块导入配置
└─ params.toml # 主题参数
接下来就可以开始入门
后续更新 #
通过 hugo 模块安装的主题想要更新很简单,直接在项目路径下执行以下命令:
hugo mod get -u
入门 #
使用 Hugo 搭建博客,我们需要了解研究的总体流程路线图:
基础配置»内容组织»Front Matter»评论系统»自动构建和部署»无头CMS(后台内容管理)»进阶内容
先来个树状图展示和了解一下 hugo 项目的目录结构:
myblog/ # 项目根目录
├── archetypes/ # 内容模板目录
├── assets/ # 资源管道目录(编译时处理)
├── config/ # 配置文件目录
├── content/ # 内容目录(网站内容)
├── data/ # 数据文件目录
├── i18n/ # 国际化文件目录
├── layouts/ # 布局模板目录
├── static/ # 静态文件目录
├── themes/ # 主题目录(为空代表使用模块导入)
├── public/ # 构建输出目录
├── resources/ # 资源缓存目录(构建时生成)
└── go.mod # Go模块文件
我们一开始重点关注config/和content/这两个目录,前者是配置目录后者是内容目录
配置目录的结构大致如此:
config/_default/ # 默认环境配置
├── hugo.toml # 站点基础配置
├── languages.*.toml # 语言特定配置
├── menus.*.toml # 菜单配置
├── params.toml # 主题参数
├── markup.toml # 标记语言设置
└── module.toml # 模块导入配置
基础配置 #
站点配置 #
config/_default/hugo.toml:站点配置文件,设置整个站点的基础配置
baseURL设置博客网站,/结尾,然后填入语言代码设置语言和默认语言 Congo 语言代码参考
baseURL = "https://你的域名/"
languageCode = "zh-Hans"
defaultContentLanguage = "zh-Hans"
defaultContentLanguageInSubdir = false
timeZone = "Asia/Shanghai"
完整可参考配置文件:
baseURL = "https://blog.huan666.de/"
languageCode = "zh-Hans"
defaultContentLanguage = "zh-Hans"
defaultContentLanguageInSubdir = false
timeZone = "Asia/Shanghai"
enableRobotsTXT = true
summaryLength = 0
[pagination]
pagerSize = 10
[frontmatter]
format = "yaml"
# 自动日期填充配置
# date: 文章发布时间 - 优先使用front matter中的date,否则使用文件修改时间
date = ["date", ":fileModTime"]
# lastmod: 最后修改时间 - 优先使用front matter设置,否则使用文件修改时间
lastmod = ["lastmod", ":fileModTime"]
[[cascade]]
isCJKLanguage = true
_target.lang = "zh-Hans"
[outputs]
home = ["HTML", "RSS", "JSON", "articles"]
[privacy]
[privacy.vimeo]
enableDNT = true
[privacy.x]
enableDNT = true
[privacy.youTube]
privacyEnhanced = true
[services]
[services.x]
disableInlineCSS = true
多语言配置 #
接下来就是配置对应语言设置
languages.*.toml:是对应语言的配置,如果站点有简体中文和英文就需要两个文件languages.zh-Hans.toml和languages.en.toml,即**languages.[语言代码].toml**的格式命名文件。
languages.zh-Hans.toml示例配置,Congo 更多配置选项参考
languageCode = "zh-Hans"
languageName = "简体中文"
weight = 1
contentDir = "content"
title = "焕昭君的博客"
[params.author]
name = "焕昭君"
image = "img/author.webp"
headline = "知行合一,日拱一卒"
bio = "知行合一,日拱一卒"
links = [
{ email = "mailto:[email protected]" },
{ github = "https://github.com/Huan-zhaojun" },
{ linuxdo = "https://linux.do/u/huan/summary" }
]
头像设置 #
image = "img/author.webp":image设置作者的头像,路径使用assets/下的相对路径。在你的博客项目找到这个文件夹📁,把你的头像图片复制拖拽进去。
图片文件你最好压缩成 WebP 减少体积
菜单配置 #
menus.*.toml:菜单导航配置,*替换为对应的语言代码如menus.en.toml和menus.zh-Hans.toml。
两种菜单导航可以配置,main在顶栏,footer在页脚。
配置参数:
name:菜单导航名weight:权重,决定排序pageRef:菜单路径,用于引用内容页面或分类法。Warning⚠️引用的页面要实际存在才能生效!下文内容组织章节将会完整讲解到这一切~
url:直接链接URLparams:额外参数设置,可以添加icon图标、showName、target等
参考配置文件:
[[main]]
name = "文章"
pageRef = "posts"
weight = 10
[[main]]
name = "分类"
pageRef = "categories"
weight = 20
[[main]]
name = "标签"
pageRef = "tags"
weight = 30
[[main]]
name = "友链"
pageRef = "friends"
weight = 97
[[main]]
name = "关于"
pageRef = "about"
weight = 98
[[main]]
identifier = "search"
weight = 99
[main.params]
action = "search"
[[main]]
identifier = "locale"
weight = 100
[main.params]
action = "locale"
# -- Footer Menu --
# The footer menu is displayed at the bottom of the page, just before
# the copyright notice. Configure as per the main menu above.
[[footer]]
name = "随便逛逛"
pageRef = "random"
weight = 10
[footer.params]
icon = "lightbulb"
[[footer]]
name = "RSS订阅"
url = "/index.xml"
weight = 20
[footer.params]
icon = "rss"
[[footer]]
name = "站点地图"
url = "/sitemap.xml"
weight = 30
[footer.params]
icon = "globe"
主题参数配置 #
config/_default/params.toml:Hugo 主题参数配置。不同的主题提供了大量的配置选项可供设置,具体根据对应主题的文档查看不同配置选项的作用。本文以 Congo主题配置参数 为例,着重一些需要修改的选项。
| 名称 | 推荐值 | 说明 |
|---|---|---|
colorScheme | 设置主题配色方案,参阅配色方案 | |
autoSwitchAppearance | true | 根据访客系统自动切换主题外观 |
enableSearch | true | 站点搜索功能 |
enableCodeCopy | true | 代码块复制 |
header.layout | hybrid | 顶部栏&菜单的布局样式 设置成混合让移动设备宽度不足自动折叠顶栏 |
footer.showAppearanceSwitcher | true | 网站页脚显示外观切换器 |
homepage.layout | 主页的布局模式,参阅主页布局 | |
homepage.showRecent | true | 是否在首页显示近期文章列表。 |
homepage.recentLimit | 显示近期文章的最大数量 | |
article.showDateUpdated | true | 是否显示文章的更新日期 |
article.showAuthor | 是否在文章页脚显示作者信息框 | |
article.showEdit | 是否显示编辑文章内容的链接 | |
article.showTableOfContents | true | 是否在文章中显示目录markup.toml修改tableOfContents-目录层级显示 |
article.showTaxonomies | 是否显示与本文相关的分类法(即分类和标签) | |
article.showWordCount | true | 是否显示文章字数统计 |
article.showComments | true | 是否在文章页脚后包含评论部分 |
list.showTaxonomies | 是否在列表页显示与本文相关的分类标签 |
更多请参阅 Congo主题配置参数 文档
内容组织 #
Hugo 把网站内容看作一个树状结构,就像文件系统一样。每个文件夹都可以成为一个"部分(Section)",每个 Markdown 文件都可以成为一个"页面(Page)"。
基本结构和概念图解 #
content/
├── _index.md # 网站首页内容
├── about.md # 单独页面 (Page)
├── posts/ # Section: 博客文章区
│ ├── _index.md # posts 列表页
│ ├── my-first-post.md
│ └── hugo-guide.md
├── projects/ # Section: 项目展示区
│ ├── _index.md # projects 列表页
│ ├── project-1.md
│ └── awesome-app/ # Page Bundle (页面捆绑)
│ ├── index.md # 页面内容
│ ├── screenshot.png # 页面资源
│ ├── cover.jpg # 封面图
│ ├── featured.png # 特色图
│ └── thumbnail.jpg # 缩略图
└── docs/ # Section: 文档区
├── _index.md # docs 首页
├── getting-started.md
└── advanced/ # 子Section
├── _index.md # advanced 列表页
└── performance.md
接下来将区分Page (单独页面)、Section (内容分区)、List Page (列表页)、Page Bundle (页面捆绑)这四大概念的不同,在正式写博客文章之前还是非常需要了解这些概念的。
Page (单独页面) #
最基本的内容单元,就是一个独立的 .md 文件成为一个独立页面。
例子:content/about.md,URL路径就是/about/。创建了这个文件了之后可以作为菜单导航,如上文配置一个菜单导航所说的,pageRef = "about"引用这个文档作为关于页面的菜单导航
Section (内容分区) #
content/ 目录下,一个文件夹就是一个 Section,用来组织相关的内容。
例子: content/posts/ 文件夹
- URL 前缀:
/posts/ - 可包含多个不同的独立页面
- 可以有自己的列表页 (
_index.md)
List Page (列表页) #
用 _index.md 文件定义,可以展示Section下所有内容列表。
例如:
网站根目录的
_index.mdcontent/ └── _index.md # 控制网站首页Section 根目录的
_index.md
content/
└── posts/
└── _index.md # 控制 /posts/ 列表页
- 子目录的
_index.md
子分区,形成多级内容结构
content/
└── docs/
└── advanced/
└── _index.md # 控制 /docs/advanced/ 列表页
不好理解?https://blog.huan666.de/posts/- 这是我的博客的文章页面,你可以理解成一个List Page (列表页),展示了posts这个Section下所有内容的列表
Page Bundle (页面捆绑) #
hugo的内容组织形式,可以将页面资源组织管理在一个文件夹中
Branch Bundle(分支捆绑) #
使用 _index.md(注意下划线)来组织,可含其他页面作为子页面,常用于分类页、标签页、章节首页等。
具体效果我们可以看到一个页面展示了很多文章的列表。
content/
├── posts/ # Branch Bundle目录
│ ├── _index.md # 关键:使用 _index.md
│ ├── post-1.md
│ ├── post-2.md
│ └── images/
│ └── hero.jpg
└── about/ # 另一个Branch Bundle
├── _index.md
├── team.jpg
└── company.pdf
不好理解?https://blog.huan666.de/posts/ – 你也可以理解成一个Branch Bundle(分支捆绑),展示了posts下所有文档的列表。
Leaf Bundle(页捆绑) #
使用 index.md(不带下划线)来组织,代表这个文件夹是一个完整的页面,最终呈现是单个页面。通常用于将博客文章和图片绑定在一起,封面图、特色图、缩略图等。
content/
└── posts/
└── my-awesome-post/ # 文件夹
├── index.md # 注意:是 index.md,不是 _index.md!
├── featured.png # 特色图
├── cover.jpg # 封面图
├── thumbnail.jpg # 缩略图
└── images/ # 图片目录
├── gallery-1.jpg
└── gallery-2.jpg
在当前 Congo 主题使用这种方式定义一个文章的封面图、缩略图、特色图的。文章文件夹📁下,文档主体命名为index.md文件,图片命名为thumb或thumbnail则作为文章缩略图在文章列表展示,而命名为cover则在该文章页面的内容顶部展示,而featured其存在时同时替代thumb和cover的作用。
| 特性 | Branch Bundle | Leaf Bundle |
|---|---|---|
| 索引文件 | _index.md | index.md |
| 子页面 | ✅ 可以有 | ❌ 不能有 |
| 资源文件 | ✅ 支持 | ✅ 支持 |
| 典型用途 | 分类页、章节页 | 单篇文章 |
添加内容 #
这行命令创建文章内容
hugo new content/_index.md #编辑博客首页内容
hugo new content/posts/_index.md #编辑文章列表内容
hugo new content content/posts/my-first-post.md #编辑第一篇文章内容
运行hugo server,并且点击访问http://localhost:1313/发现博客首页有内容了,/posts/路径显示了包括第一篇文章在内的文章列表,/posts/my-first-post.md路径对应了刚刚创建的第一篇文章。这体现了前面说的 hugo 内容组织结构如同树状,文件夹就是路径。
Front Matter #
回过头来,刚刚创建文档内容的时候,发现文档头部位置多出了一个这样的玩意:
+++
title = 'My First Post'
date = 2024-01-14T07:07:07+01:00
draft = true
+++
这其实就是 Front Matter ── 是放在文章开头的元数据区块,由特定分隔符包围(如 --- 或 +++),支持 YAML / TOML / JSON 等格式。用于定义和存储这篇文章的标题、日期、标签等属性。在 Hugo 中,Front Matter 决定了页面如何被构建和显示。
接下来我将以 YAML 格式为 Front Matter 示例。
属性字段 #
archetypes/default.md:这是 Hugo 中的原型模板文件,用于定义新建内容时的默认模板。当你使用 hugo new 命令创建新内容时,Hugo 会使用这个模板作为基础。
下面我提供一个供参考的 Front Matter 模板示例,我认为值得关注的 Front Matter 属性设置:
---
title: "{{ replace .File.ContentBaseName "-" " " | title }}"
#slug: ""
#description: "Meta标签 页面描述"
#summary: "文章预览 内容摘要"
date: {{ .Date }}
#lastmod: {{ .Date }}
draft: false
categories: []
tags: []
---
| 字段 | 说明 |
|---|---|
title | 标题,文章的显示标题 |
slug | URL 别名,自定义文章的 URL 路径,如不设置则使用文件名 |
description | 页面描述,用于 HTML meta 标签,影响 SEO 搜索结果显示 |
summary | 内容摘要,可作为文章列表的预览内容,如不设置 Hugo 会自动截取文章开头部分 |
date | 创建时间,文章发布日期,可自动生成,影响文章排序 |
lastmod | 修改时间,文章最后修改时间,可通过配置自动根据文件修改时间更新 |
draft | 草稿状态,设为 true 时文章不会在生产环境中显示,默认为 false |
isCJKLanguage | 中日韩语言标识,设为true可优化中文等 CJK 语言的摘要截取和字数统计 |
categories&tags | 分类和标签,Hugo 默认的 Taxonomies(分类法),用于内容组织和导航 |
更多 Front Matter 字段请参阅: hugo 和 Congo主题 的文档
额外的补充内容
slug:作用:自定义URL路径,避免使用文件名作为URL。
当文档名是中文时,强烈建议设置英文slug。
唯一性:根据permalink结构决定是否需要全局唯一# 情况1:需要全局唯一slug,去掉其他嵌套目录路径 [permalinks] posts = "/:slug/" # 情况2:可以重复slug(有日期区分) [permalinks] posts = "/:year/:month/:slug/" # 情况3:可以重复slug(有分类、嵌套目录区分) [permalinks] posts = "/:section/:slug/"自动时间跟踪
在hugo.toml中配置,实现对文档修改时间的自动跟踪:[frontmatter] format = "yaml" # 自动日期填充配置 # date: 文章发布时间 - 优先使用front matter中的date,否则使用文件修改时间 date = ["date", ":fileModTime"] # lastmod: 最后修改时间 - 优先使用front matter设置,否则使用文件修改时间 lastmod = ["lastmod", ":fileModTime"]自动中日韩语言标识
isCJKLanguage
在hugo.toml配置使用**cascade**功能为特定语言自动添加配置,即实现中文文章自动添加isCJKLanguage: true的 Front Matter 字段[[cascade]] isCJKLanguage = true _target.lang = "zh-Hans"
自动添加 Front Matter #
看完上文之后我猜你跃跃欲试,准备把大量已有的笔记、文档等复制迁移到博客文件夹里,不过还没完~
大量的文档一开始是没有 Front Matter 的,当然肯定不能傻傻地手动一个个在文档开头添加。所以我专门写了一个脚本能够批量为文档添加 Front Matter,具体使用跳转到本文最后的 其他资源 章节查看。
如果你不想丢失文件原始创建时间元信息,那最好在你的文档复制到博客📂之前自动添加 Front Matter,不然你得一个个手动添加创建时间了…
便捷修改 #
不过还是要对文档的元信息做具体的修改,比如概括描述、slug、分类、标签等。这里我推荐 Obsidian 这个前面最开始提到过的 markdown 编辑器,它对文档的 Front Matter 的编辑功能做到很不错,谁用谁知道!最让我夸赞的一点就是────编辑 tag 标签时候,它会提供目前所有文档用过的标签作为候选选择!!
批量管理文档 #
最后再推荐一个强力的 VSCode 插件────Front Matter CMS,GitHub VSCode 插件
基于 Front Matter 的文档内容管理系统,根据时间、分类标签等批量管理所有文档。
网站图标(Favicon) #
在 Congo 主题把图标资源按照以下的方式放在 static/ 文件夹内即可配置博客网站图标。好在,favicon.io这个网站提供了一键生成网站图标的工具。
static/
├─ android-chrome-192x192.png
├─ android-chrome-512x512.png
├─ apple-touch-icon.png
├─ favicon-16x16.png
├─ favicon-32x32.png
├─ favicon.ico
└─ site.webmanifest
数据分析(Analytics) #
Google Analytics #
访问官网(analytics.google.com/):次使用点击“开始衡量”注册账号,若有账号就登录
继续下步骤把信息填完…
开始收集数据:选择平台“网站”,填入信息点击确认
复制衡量 ID,类似于一串
G-XXXXXXXXX的字符串。配置文件:
config/_default/hugo.toml在这个文件里填入:[services] [services.googleAnalytics] id = 'G-XXXXXXXXX'然后就会自动添加上谷歌的脚本。这适用于Congo 主题,如果你是其他主题可能也支持该操作,具体得查看对应的文档。实在没有这么简单的操作还可以选择手动添加谷歌脚本到
layouts/partials/analytics.html文件里
Umami Analytics #
评论系统 #
一个优秀的博客当然不能缺少评论系统集成。对于评论系统首要考虑的是时效通知和托管部署的这两大问题。
- 即时通知是最重要的,如果评论不能通知到对方,那整个评论系统将没有任何意义!一般常规操作是使用邮件来通知。大部分的评论系统要么需要自建邮局和设置SMTP,要么邮件代发服务;前者自建邮局搞起来挺麻烦的,但最棘手让人头疼问题──垃圾邮件──大部分自建邮局的归途,发的邮件全都被丢垃圾箱了;后者邮件代发服务,需要付费,免费额度每天只有一点,但对于个人博客的使用场景完全够用了。
- 托管与部署:评论系统可以选择自托管,完全掌控全部包括评论数据,那就需要数据库、服务器了,部署起来是有点小麻烦,考虑的事情也比较多。
综上所述,Giscus ── 首选适合的评论系统,快速上手,简单部署。
Giscus #
一个由 GitHub Discussions 驱动的评论系统。让访客通过 GitHub 在你的博客留下评论。
优势:
- 快速上手,简单部署:直接使用 GitHub来托管评论数据,集成在博客只需要通过生成器的一段js脚本。
- 开源免费
- 自带登录认证
劣势:
- 依赖GitHub,必须登录GitHub账号,评论所在仓库必须公开
- 缺乏丰富的自定义功能
- 评论不能先审后发

从官方配置生成器开始使用吧! #
- 创建一个公开的 GitHub 仓库,可以直接选择博客项目仓库,亦可专门创建一个仓库如
myblog-comment作为评论托管 - 安装 Giscus app:安装这个GitHub APP,并为刚刚的仓库启用。
- 启用 Discussions 功能:在仓库设置里启用。
继续回到官方配置生成器:
- 仓库选择:填入仓库名如
YourGitHubUsername/my-blog-comments,Giscus 会自动检查仓库是否有效以及 App 是否已安装。 - 页面 ↔️ discussion 映射关系:
这是最关键的一步,它决定了如何将你的博客文章与 GitHub Discussion 讨论帖对应起来。对于大多数 Hugo 博客,推荐使用以下两种稳定且不易出错的方案:pathname:使用文章的相对 URL 路径作为唯一标识。这是最推荐的选项。这与前面说过的hugo于文章的slug关联起来了URL:使用文章的完整 URL 作为唯一标识。
- Discussion 分类 (Category):选择一个讨论分类用于存放评论。推荐使用公告(announcements)类型的分类并删除其他分类,以确保新 discussion 只能由仓库维护者和 giscus 创建。或者新建一个专用分类如
Comments然后设置权限也是一样的。 - 功能 (Features):
- 启用主帖子上的反应(reaction):建议勾选,允许访客对你的文章点赞或送上其他表情。
- 懒加载评论(Lazy Loading):强烈建议勾选!这会使评论区只在用户滚动到页面底部时才加载,能显著提升你博客的初始加载速度。
- 主题 (Theme):默认选择 用户偏好的色彩方案
Preferred Color Scheme - 生成脚本:完成以上所有配置后,页面底部会自动生成一段
<script>代码。点击 “Copy” 按钮复制这段代码。
- 高级用法:查阅指南,还可以添加额外设置如
giscus.json设置仅允许特定来源的请求等
对于使用 hugo 的 Congo 主题的评论功能:只需要创建一个layouts/_partials/comments.html文件,把刚刚复制的代码粘贴到以下示例文件的指定位置去。如果前面article.showComments = true已经启用了评论功能,那在文章底部就能看到评论显示了。
{{/* giscus 评论系统组件 */}}
<div class="giscus-comments">
粘贴的 <script> 块
</div>
或者使用我自定义的layouts/_partials/comments.html文件:
- 多语言自适应:评论界面跟随语言切换。文章不同语言版本映射到同一个评论区。
- 更好的深浅模式适配
{{/* giscus 评论系统组件 - 增强版 */}}
{{/* 多语言自适应配置 */}}
{{- $giscusLang := "zh-CN" -}}
{{- if eq .Language.Lang "en" -}}
{{- $giscusLang = "en" -}}
{{- end -}}
{{/* 路径标准化逻辑 - 统一多语言评论映射 */}}
{{- $commentTerm := .RelPermalink -}}
{{/* 统一去掉开头的斜杠 */}}
{{- $commentTerm = strings.Replace .RelPermalink "/" "" 1 -}}
{{- if eq .Language.Lang "en" -}}
{{/* 对于英文页面,去掉 en/ 前缀来统一映射 */}}
{{- $commentTerm = strings.Replace $commentTerm "en/" "" 1 -}}
{{- end -}}
<div class="giscus-comments">
{{/* giscus评论组件 */}}
<script src="https://giscus.app/client.js"
data-repo="替换仓库名"
data-repo-id="替换仓库ID"
data-category="替换分类名"
data-category-id="替换分类ID"
data-mapping="specific"
data-term="{{ $commentTerm }}"
data-strict="1"
data-reactions-enabled="1"
data-emit-metadata="0"
data-input-position="top"
data-theme="preferred_color_scheme"
data-lang="{{ $giscusLang }}"
data-loading="lazy"
crossorigin="anonymous"
async>
</script>
{{/* giscus主题同步脚本 */}}
<script>
(function() {
function sync() {
const iframe = document.querySelector('iframe.giscus-frame');
if (iframe) {
iframe.contentWindow.postMessage({
giscus: { setConfig: { theme: document.documentElement.classList.contains('dark') ? 'dark' : 'light' } }
}, 'https://giscus.app');
}
}
new MutationObserver(() => sync()).observe(document.documentElement, {
attributes: true,
attributeFilter: ['class']
});
window.addEventListener('message', (e) => {
if (e.origin === 'https://giscus.app' && e.data?.giscus?.discussion) sync();
});
})();
</script>
</div>
评论及时邮件通知 #
使用 Giscus 作为评论系统还有一点重要设置──确保别人发评论我们能及时收到邮件通知。
在 Giscus 评论所在 GitHub 仓库点击 Watch,这样当有文章底下有新的评论(即新的 Discussions 发布),我们就能第一时间收到邮件通知了。
选择All Activity或Custom->Discussions都行。最终只要确保有新的 Discussions 能及时邮件通知。
你可以让你的朋友(或者我)帮你评论测试一下是否能及时邮件通知。
.webp)
Settings->Notifications->Subscriptions:确保全部选项开启邮件通知(一般全部是默认开启了,无需额外修改)
.webp)
Waline #
Twikoo #
Remark42 #
Artalk #
构建与部署 #
终于来到激动人心的时刻────博客要部署成网站了!你不需要手动构建 Hugo 博客和部署,这一切都能 自动化 CI/CD ,解放双手🤲,接下来只需要一步步跟着指引,很简单就完成配置。
上传 GitHub #
首先是要把博客项目上传 GitHub 仓库,根据自己的选择私有或者公开仓库。
值得注意的是,不要把public/目录也添加到 git 里了,这里有一份示例参考**.gitignore**文件:
# Hugo 构建输出目录
/public/
/resources/_gen/
.hugo_build.lock
# Hugo 可执行文件
hugo.exe
hugo.linux
hugo.darwin
# Obsidian 配置 - 选择性包含重要配置
.obsidian/*
!.obsidian/app.json
!.obsidian/appearance.json
!.obsidian/core-plugins.json
!.obsidian/community-plugins.json
!.obsidian/hotkeys.json
!.obsidian/graph.json
## 排除缓存和临时文件
.obsidian/workspace.json
.obsidian/cache/
.obsidian-git-data
.trash/
# 操作系统相关文件
.DS_Store
Thumbs.db
# 编辑器和IDE文件
.vscode/
.idea/
*.swp
*.swo
*~
# 日志文件
*.log
# 临时文件
tmp/
*.tmp
# Node.js 依赖 (如果使用 npm 或 yarn)
node_modules/
npm-debug.log
yarn-error.log
# 环境变量文件
.env
.env.local
.env.*.local
# 构建工具缓存
.cache/
.parcel-cache/
# 备份文件
*.bak
*.bak*
*.backup
*.backup*
# Front Matter 工具备份
tools/add_frontmatter_backups/
# 压缩文件
#*.tar.gz
#*.zip
# 自定义缓存和临时目录
.hugo_cache/
.publish/
/_vendor/
# 主题文件 (如果使用 git submodule 管理主题)
themes/* (如果不想版本控制主题文件可以取消注释)
# 部署相关文件
.deploy_git/
.netlify/
vercel.json
# 本地开发配置
local.toml
development.toml
# 数据库文件
*.db
*.sqlite
# 证书文件
*.pem
*.key
*.crt
# 系统生成的文件
*.orig
.history/
/.obsidian/workspace.json
/hugo_stats.json
Netlify #
Netlify 是一个专注于现代web开发的云平台,提供静态网站托管、持续部署、无服务器函数等服务。它让开发者可以直接从Git仓库部署网站,并提供全球CDN加速、自动HTTPS等功能。
对于我们来说可以免费使用它来自动构建和部署 hugo 静态博客,非常简单和方便。免费计划每月:300 构建分钟&100GB流量,对于小博客站点来说完全够用了。然后 Netlify 还有一个优势就是国内直连访问速度快。
首先在项目根目录添加
netlify.toml文件:[build] publish = "public" command = "hugo --gc --minify -b $URL" [build.environment] HUGO_VERSION = "0.148.1" HUGO_ENV = "production" TZ = "Asia/Shanghai" [context.production.environment] HUGO_ENV = "production"HUGO_VERSION:替换为成实际的版本号。使用hugo version查看当前使用的版本或查看 GitHub的 hugo 最新版本通过 git 提交并推送到远程仓库。Netlify 会识别到配置文件并应用的。
进入官网、登录 GitHub 账号,
Add new project添加新项目,导入 Git 远程仓库,GitHub 授权 读取仓库的权限可选全部仓库/指定仓库,建议最小权限只给导入的仓库权限。后续随时可在 Applications设置 里修改 **Netlify 配置 **- 仓库权限Repository access设置项目名(Project name),这将决定了部署的网址
https://xxx.netlify.app/,其他配置不管(因为前面netlify.toml文件已经一键配置好了),点击部署,接下来请坐和放宽~ 不到半分钟就构建部署完成访问
https://项目名.netlify.app/就能看到博客网站了!自定义域名:在项目的 域名管理(Domain management) 里添加自己的域名,并在域名托管商添加一个 CNAME 类型的 DNS 记录指向刚刚的 Netlify 部署的地址。如果遇到了 CORS跨域问题,记得点击重新构建(点击任意一个 Production Deploys 详情里找到 Options 按钮)。

排错:
- 如果你使用的是 Cloudflare 域名托管商并且开启了 Rocket Loader™ 优化功能(域名设置:速度(Speed)–优化(Optimization)里),请关闭掉它。该功能旨在加速含 JavaScript 网页的渲染,但对于 Hugo 博客可能会有意想不到的问题!
GitHub Actions #
除了Netlify,我们也可以使用 GitHub Actions 来自动构建博客。GitHub Actions 对于公开仓库是免费的,而私有仓库有2000分钟免费时间。
我们既可以将 GitHub Actions 自动构建产物放在同仓库下的构建分支,也可以放在另外的仓库;构建产物可以使用 GitHub Page 来部署网页(要求 GitHub 仓库是公开的才能免费使用),或者也可以用 Netlify 来部署网页,还可以使用 rsync 将构建产物复制同步到自己的 VPS 服务器上然后使用 Nginx 部署网站。
(未完待续,敬请期待!)
参考文献:
Github Action 自动发布 - Robin
无头CMS(后台内容管理) #
CMS 是 Content Management System(内容管理系统)的缩写,是一种用于创建、管理和发布数字内容的软件平台。传统CMS将内容管理和前端展示紧密耦合。
而无头CMS(Headless CMS)则将前端展示层分离,专注于内容的创建和管理。
在本文的 hugo 中,将结合基于 Git-based Headless CMS(如Sveltia CMS、Pages CMS、Decap CMS、、Tina CMS 等)与其使用。既有静态站点优势,又能提供一个友好的可视化后端管理界面。
这里我推荐使用 Sveltia CMS 或 Pages CMS 作为我们博客后台的管理系统。你可以二选一,或者两个都试试,不会冲突。两者各有特色,Sveltia CMS 是嵌入博客网站的后台管理系统,Pages CMS 最大特点是有在线平台直接使用如果你想更简单、快速上手就使用它吧。两者配置都有配置说明,跳转查看~
Pages CMS #
Pages CMS 最大优势在于──在线 CMS 后台内容管理,直接于你的 GitHub 博客项目对接管理!

- 配置文件:在项目根目录创建
.pages.yml文件。完整配置示例如下:根据自己所需修改配置文件,详细参考 Pages CMS 文档# =========================================== # PagesCMS 配置文件 - Hugo 博客 # =========================================== # 媒体文件配置 media: input: static/images/ # 上传文件存储路径(相对于项目根目录) output: /images # 网站发布后的公共访问路径 #extensions: [jpg, jpeg, png, gif, svg, webp] # 可显示的文件扩展名的文件 #categories: [jpg, jpeg, png, gif, svg, webp] # 允许特定类型的文件 # 内容配置 content: # =========================================== # 博客文章集合 # =========================================== - name: posts label: 📝 博客文章 type: collection path: content/posts #filename: '{fields.title}.md' subfolders: true #exclude: [ _index.md ] # 忽略文件 format: yaml-frontmatter view: fields: [ title, date, categories, tags ] primary: title sort: [ date, title ] default: #search: '' sort: date order: desc layout: tree # `list`平铺或`tree`树状列表 node: filename: _index.md hideDirs: nodes fields: # 基础字段 - name: title label: 标题 type: string required: true description: 文章标题,建议简洁明了 options: minlength: 2 maxlength: 100 - name: slug label: URL 别名 type: string required: false pattern: { message: '只能包含小写字母、数字和连字符', regex: '^[a-z0-9]+(?:-[a-z0-9]+)*$' } description: 用于生成 URL 的别名,留空则自动生成 # 描述和摘要 - name: description label: 文章描述 type: text required: false description: 用于 SEO 的页面描述,建议 120-160 字符 - name: summary label: 文章摘要 type: text required: false description: 显示在文章列表的预览摘要 # 时间字段 - name: date label: 发布日期 type: date required: true description: 文章发布日期 options: format: yyyy-MM-dd'T'HH:mm:ssXXX time: true step: true - name: lastmod label: 最后修改时间 type: date required: false description: 留空则自动使用文件修改时间 options: format: yyyy-MM-dd'T'HH:mm:ssXXX time: true step: true # 状态字段 - name: draft label: 草稿状态 type: boolean default: false description: 勾选后文章为草稿状态,不会在网站显示 # 分类和标签 - name: categories label: 分类 type: string list: true required: false description: 文章分类,可以添加多个 - name: tags label: 标签 type: string list: true required: false description: 文章标签,用于细分主题 # 缩略图 - name: thumbnail label: 文章缩略图 type: image required: false description: 文章封面图片 # 正文内容 - name: body label: 正文内容 type: rich-text required: true description: 文章主要内容,支持 Markdown 语法的富文本编辑器 # =========================================== # 关于页面 # =========================================== - name: about label: 📄 关于页面 type: file path: content/about.md format: yaml-frontmatter fields: - name: body label: 页面内容 type: rich-text required: true description: 关于页面的主要内容 # =========================================== # 友链页面 # =========================================== - name: friends label: 🔗 友链页面 type: file path: content/friends.md format: yaml-frontmatter fields: - name: body label: 页面内容 type: rich-text required: true description: 友链页面内容 - 提交和推送到 GitHub 远程仓库
- 在线 CMS:点击 app.pagescms.org 访问 Pages CMS 在线版本,省去部署麻烦。登录 GitHub 账号,授权 GitHub OAuth App 对于博客仓库的权限。登录成功之后选择博客项目,如果一切正常就能顺利看到文章列表。
Sveltia CMS #
Sveltia CMS 是基于 Decap CMS 的现代化替代方案,不仅保持配置兼容,还在此基础上升级和扩展,具有以下优势:
- 现代化界面、适配移动端、自动深色模式
- 高性能、加载快、页面流畅
- 更好可视化编辑器,自动保存、多媒体管理
- 等等更多…

配置 #
在
static/目录下创建admin文件夹📂
在static/admin/文件夹内,你将创建两个文件:admin ├ index.html └ config.yml第一个文件
admin/index.html是 Sveltia CMS 管理界面入口,访问yoursite.com/admin/进入。它是一个基础的HTML起始页面,负责加载 Sveltia CMS 的 JavaScript 文件。第二个文件,
admin/config.yml,是 Sveltia CMS 的核心配置文件,相对复杂一些。Configuration 详细介绍了相关内容。admin/index.html文件复制填入:<html> <head> <meta charset="utf-8"/> <meta name="viewport" content="width=device-width, initial-scale=1.0"/> <meta name="robots" content="noindex"/> <title>Content Manager</title> </head> <body> <script src="https://unpkg.com/@sveltia/cms@^0.50.0/dist/sveltia-cms.js"></script> </body> </html>admin/config.yml配置文件示例,根据自己所需修改:# =========================================== # Sveltia CMS 配置文件 - Hugo博客优化版本 # =========================================== # 后端配置 (Backend) # 配置Git仓库连接和认证方式 backend: name: github repo: Huan-zhaojun/myblog # GitHub仓库地址 branch: main # 主分支 automatic_deployments: true # 自动部署,参阅https://github.com/sveltia/sveltia-cms/blob/main/README.md#disabling-automatic-deployments commit_messages: create: '新增博客文章: {{slug}}' update: '更新博客文章: {{slug}}' delete: '删除博客文章: {{slug}}' uploadMedia: '上传媒体文件: {{path}}' deleteMedia: '删除媒体文件: {{path}}' # 媒体文件配置 (Media) # 优化图片管理,兼容Hugo静态文件结构 media_folder: "static/images/uploads" # 上传文件存储路径(项目根目录相对路径) public_folder: "/images/uploads" # 网站发布后的公共访问路径 # 全局配置 site_url: 'https://blog.huan666.de/' # 网站地址,用于预览功能 display_url: 'https://blog.huan666.de/' # 显示的网站地址 #logo_url: '' # 自定义CMS后台logo的远程URL # 发布模式 # 使用简单模式,直接提交到主分支 publish_mode: simple # 内容集合配置 (Collections) # 定义不同类型的内容管理规则 collections: # =========================================== # 主博客文章集合 # =========================================== - name: "posts" label: "📝 博客文章" label_singular: "博客文章" folder: "content/posts" create: true delete: true #extension: "md" format: "frontmatter" slug: "{{slug}}" #identifier_field: "title" #preview_path: "posts/{{slug}}" summary: "{{title}} - {{date}}" sortable_fields: fields: [ title, date ] default: field: date direction: descending # default: ascending view_filters: - label: "草稿" field: draft pattern: true - label: "已发布" field: draft pattern: false - label: "全部" field: draft pattern: "" view_groups: - label: "年份" field: date pattern: '\d{4}' - label: "月份" field: date pattern: '\d{4}-\d{2}' fields: # 基础字段 - label: "标题" name: "title" widget: "string" required: true hint: "文章标题,建议简洁明了" pattern: ['.{2,100}', "标题长度应在2-100字符之间"] - label: "slug" name: "slug" widget: "string" required: false hint: "用于生成URL的别名,留空则自动生成" pattern: ['^[a-z0-9]+(?:-[a-z0-9]+)*$', "只能包含小写字母、数字和连字符"] # 描述摘要 - label: "文章描述" name: "description" widget: "text" required: false hint: "用于SEO的页面描述,建议120-160字符" pattern: ['.{0,200}', "描述不能超过200字符"] - label: "文章摘要" name: "summary" widget: "text" required: false hint: "显示在文章列表的预览摘要" # 时间字段 - label: "发布日期" name: "date" widget: "datetime" default: "{{now}}" date_format: "YYYY-MM-DD" time_format: "HH:mm:ss" format: "YYYY-MM-DDTHH:mm:ssZZ" required: false - label: "最后修改时间" name: "lastmod" widget: "datetime" default: "" date_format: "YYYY-MM-DD" time_format: "HH:mm:ss" format: "YYYY-MM-DDTHH:mm:ssZZ" required: false hint: "留空则自动使用文件修改时间" # 状态字段 - label: "草稿状态" name: "draft" widget: "boolean" default: false hint: "勾选后文章为草稿状态,不会在网站显示" # 分类标签 - label: "分类" name: "categories" widget: "list" allow_add: true required: false hint: "文章分类,可以添加多个" default: [] - label: "标签" name: "tags" widget: "list" allow_add: true required: false hint: "文章标签,用于细分主题" default: [] # 正文内容 - label: "正文内容" name: "body" widget: "markdown" required: true hint: "文章主要内容,支持Markdown语法" - divider: true # =========================================== # 静态页面集合 (File Collection) # =========================================== - name: "pages" label: "📄 页面管理" files: # 关于页面 - label: "关于页面" name: "about" file: "content/about.md" fields: - label: "页面内容" name: "body" widget: "markdown" required: true # 友链页面 - label: "友链页面" name: "friends" file: "content/friends.md" fields: - label: "页面内容" name: "body" widget: "markdown" required: true hint: "友链页面内容,可以使用友链短代码"Sveltia CMS 对博客内容管理通过一个个集合(collection)来管理不同的文件夹📂和单个文档。如果需要添加更多的页面内容管理,只需要在
- name: "pages"的files字段复制和添加新的label,修改file为对应的路径。若如果是除了content/posts文件外的其他文件夹需要管理,只需要复制整一个- name: "posts",然后修改name的值和folder改为对应的文件夹路径配置后端:无头 CMS 需要校验登录才能进入后台管理,前面我们选择 GitHub 作为了博客项目的远程存储仓库所以直接选择配置 GitHub 作为后端登录。
因为 GitHub 硬性要求身份验证需要一个后端【1】【2】,所以需要配置 OAuth 客户端。
如果前面使用 Netlify 来构建和部署博客网站,那就简化省略一些步骤,只需要创建 GitHub OAuth Apps 而不需要部署额外的后端程序。
接下来继续跳转阅读下一步吧~
GitHub OAuth Provider #
- 创建一个OAuth Apps,
Settings -> Developer Settings -> OAuth Apps Authorization callback URL:回调URL填入https://api.netlify.com/auth/done。其他字段根据自己的喜好随意填写。Client ID:记下客户端IDClient secrets:生成一个客户端密钥并牢牢记下,之后就不能再次查看了!- 把客户端ID和客户端密钥添加到 Netlify 的部署站点:前往对应项目的
Project configuration > Access & security > OAuth中,在Authentication providers下点击Install Provider,选择**GitHub**,填入Client ID和Client secrets,保存就完成了。现在我们可以在博客后台使用 GitHub 登录进去了!Warning⚠️注意:如果你不是通过 Netlify 部署的站点,那还得进行额外操作──部署一个 GitHub OAuth 客户端
其他 GitHub OAuth 客户端 #
当我们没有使用 Netlify 部署站点,就需要自行搭建一个 GitHub OAuth 客户端。
Sveltia CMS Auth:这里 Sveltia CMS 为我们提供了一个 OAuth Client 选择,可以使用 Cloudflare Workers 来搭建,为 GitHub / GitLab 提供身份验证。
或者使用其他外部OAuth客户端
Decap CMS #
如果你还想用回 Decap CMS,这里有配置文件参考。
admin/index.html文件参考:<html> <head> <meta charset="utf-8"/> <meta name="viewport" content="width=device-width, initial-scale=1.0"/> <meta name="robots" content="noindex"/> <title>Content Manager</title> </head> <body> <script src="https://unpkg.com/decap-cms@^3.0.0/dist/decap-cms.js"></script> </body> </html>admin/config.yml配置文件示例:# =========================================== # Decap CMS 配置文件 - Hugo博客优化版本 # =========================================== # 后端配置 (Backend) # 配置Git仓库连接和认证方式 backend: name: github repo: 你的GitHub用户名/仓库名 # GitHub仓库地址 branch: main # 主分支 commit_messages: create: '新增博客文章: {{slug}}' update: '更新博客文章: {{slug}}' delete: '删除博客文章: {{slug}}' uploadMedia: '上传媒体文件: {{path}}' deleteMedia: '删除媒体文件: {{path}}' # 媒体文件配置 (Media) # 优化图片管理,兼容Hugo静态文件结构 media_folder: "static/images/uploads" # 上传文件存储路径(项目根目录相对路径) public_folder: "/images/uploads" # 网站发布后的公共访问路径 # 全局配置 locale: 'zh-Hans' site_url: 'https://你的域名/' # 网站地址,用于预览功能 display_url: 'https://你的域名/' # 显示的网站地址 #logo_url: '' # 自定义CMS后台logo的远程URL # 发布模式 # 使用简单模式,直接提交到主分支 publish_mode: simple # 内容集合配置 (Collections) # 定义不同类型的内容管理规则 collections: # =========================================== # 主博客文章集合 # =========================================== - name: "posts" label: "📝 博客文章" label_singular: "博客文章" folder: "content/posts" create: true delete: true #extension: "md" format: "frontmatter" slug: "{{slug}}" #identifier_field: "title" #preview_path: "posts/{{slug}}" summary: "{{title}} - {{date}}" sortable_fields: ['date', 'title'] view_filters: - label: "草稿" field: draft pattern: true - label: "已发布" field: draft pattern: false - label: "全部" field: draft pattern: "" view_groups: - label: "年份" field: date pattern: '\d{4}' - label: "分类" field: categories fields: # 基础字段 - label: "标题" name: "title" widget: "string" required: true hint: "文章标题,建议简洁明了" pattern: ['.{2,100}', "标题长度应在2-100字符之间"] - label: "slug" name: "slug" widget: "string" required: false hint: "用于生成URL的别名,留空则自动生成" pattern: ['^[a-z0-9]+(?:-[a-z0-9]+)*$', "只能包含小写字母、数字和连字符"] - label: "文章描述" name: "description" widget: "text" required: false hint: "用于SEO的页面描述,建议120-160字符" pattern: ['.{0,200}', "描述不能超过200字符"] - label: "文章摘要" name: "summary" widget: "text" required: false hint: "显示在文章列表的预览摘要" # 时间字段 - label: "发布日期" name: "date" widget: "datetime" default: "{{now}}" date_format: "YYYY-MM-DD" time_format: "HH:mm:ss" format: "YYYY-MM-DDTHH:mm:ssZZ" required: false - label: "最后修改时间" name: "lastmod" widget: "datetime" default: "" date_format: "YYYY-MM-DD" time_format: "HH:mm:ss" format: "YYYY-MM-DDTHH:mm:ssZZ" required: false hint: "留空则自动使用文件修改时间" # 状态字段 - label: "草稿状态" name: "draft" widget: "boolean" default: false hint: "勾选后文章为草稿状态,不会在网站显示" # 分类标签 - label: "分类" name: "categories" widget: "list" allow_add: true required: false hint: "文章分类,可以添加多个" default: [] - label: "标签" name: "tags" widget: "list" allow_add: true required: false hint: "文章标签,用于细分主题" default: [] # 正文内容 - label: "正文内容" name: "body" widget: "markdown" required: true hint: "文章主要内容,支持Markdown语法" # =========================================== # 静态页面集合 (File Collection) # =========================================== - name: "pages" label: "📄 页面管理" files: # 关于页面 - label: "关于页面" name: "about" file: "content/about.md" fields: - label: "页面内容" name: "body" widget: "markdown" required: true # 友链页面 - label: "友链页面" name: "friends" file: "content/friends.md" fields: - label: "页面内容" name: "body" widget: "markdown" required: true hint: "友链页面内容,可以使用友链短代码"
进阶内容 #
你是否想对 Hugo 博客实现更多自定义功能吗?接下来将深入 Hugo 的核心,修改代码以实现随心所欲的自定义功能~请准备好 AI - 如 Claude Code 之类的 Vibe Coding 的工具将节省你的时间和精力。(如果你懒于亲自阅读用例和写代码的话)
短代码(Shortcodes) #
模板(Templates) #
其他资源 #
Front Matter 自动添加工具 #
Python脚本,可以自动为Markdown文件添加Front Matter。脚本会动态读取项目的 archetypes/default.md 模板文件,自动解析Hugo模板语法并转换为可用格式。
复制以下代码命令为add_frontmatter.py,放在博客项目根目录或者tools/路径下,运行python add_frontmatter.py --help获取脚本帮助。
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# Windows控制台编码处理
import sys
if sys.platform == 'win32':
import os
os.system('chcp 65001 > nul') # 设置控制台为UTF-8编码
__version__ = "1.6.0"
"""
Hugo博客Front Matter自动添加工具
功能:
- 自动为Markdown文件添加Front Matter
- 支持多文件、相对路径、绝对路径处理
- 智能备份和恢复机制
- 基于archetypes/default.md模板生成
使用方法:
python add_frontmatter.py file1.md file2.md
python add_frontmatter.py path/to/dir/
python add_frontmatter.py /absolute/path/to/file.md
"""
import os
import sys
import re
import shutil
import argparse
import time
from datetime import datetime
from pathlib import Path
from enum import Enum
# import chardet # 移除依赖,使用内置方法
class LogLevel(Enum):
"""日志级别"""
QUIET = 0 # 静默模式
DEFAULT = 1 # 默认模式
VERBOSE = 2 # 详细模式
class FrontMatterProcessor:
def __init__(self, log_level=LogLevel.DEFAULT, preview_mode=False):
self.log_level = log_level
self.preview_mode = preview_mode
self.script_dir = Path(__file__).parent
self.backup_dir = self.script_dir / "add_frontmatter_backups"
self.processed_count = 0
self.skipped_count = 0
self.error_count = 0
self.created_backups = []
# title生成模式:'as_is', 'formatted', 'fallback'
self.title_generation_mode = 'fallback'
# 结果收集
self.processed_files = [] # 成功处理的文件
self.skipped_files = [] # 跳过的文件
self.error_files = [] # 错误文件
self.preview_files = [] # 预览文件(预览模式专用)
# 计时
self.start_time = time.time()
# 预览模式下不创建备份目录
if not self.preview_mode:
# 确保备份目录存在
self.backup_dir.mkdir(exist_ok=True)
# 智能查找项目结构
self.project_root, self.archetype_path, self.content_path = self.find_project_structure()
# 动态加载Front Matter模板
self.template = self.load_template()
def log(self, message, level=LogLevel.DEFAULT):
"""根据日志级别输出信息"""
if self.log_level.value >= level.value:
print(message)
def log_verbose(self, message):
"""详细模式日志"""
self.log(message, LogLevel.VERBOSE)
def log_default(self, message):
"""默认模式日志"""
self.log(message, LogLevel.DEFAULT)
def find_project_structure(self):
"""智能查找项目根目录、archetypes和content路径"""
self.log_verbose(f"[查找] 开始查找项目结构...")
self.log_verbose(f"[查找] 程序位置: {self.script_dir}")
self.log_verbose(f"[查找] 当前工作目录: {Path.cwd()}")
def check_hugo_project(base_path):
"""检查指定路径是否为Hugo项目根目录"""
archetype_path = base_path / "archetypes" / "default.md"
content_path = base_path / "content"
# 检查关键文件/目录是否存在
has_archetype = archetype_path.exists()
has_content = content_path.exists() and content_path.is_dir()
has_config = any([
(base_path / "config.toml").exists(),
(base_path / "config.yaml").exists(),
(base_path / "config.yml").exists(),
(base_path / "hugo.toml").exists(),
(base_path / "config" / "_default").exists()
])
self.log_verbose(f" [检查] {base_path}")
self.log_verbose(f" - archetypes/default.md: {'OK' if has_archetype else 'NO'}")
self.log_verbose(f" - content/ 目录: {'OK' if has_content else 'NO'}")
self.log_verbose(f" - Hugo 配置文件: {'OK' if has_config else 'NO'}")
# 至少需要有content目录,archetype可以使用默认模板
if has_content and (has_archetype or has_config):
return True, archetype_path, content_path
return False, None, None
# 查找策略:
# 1. 从当前工作目录开始查找
search_paths = [Path.cwd()]
# 2. 从程序所在目录查找
if self.script_dir != Path.cwd():
search_paths.append(self.script_dir)
# 3. 从程序所在目录向上层目录递归查找(最多5层)
current_path = self.script_dir
for i in range(5): # 最多向上查找5层
parent_path = current_path.parent
if parent_path == current_path: # 已经到达根目录
break
if parent_path not in search_paths:
search_paths.append(parent_path)
current_path = parent_path
# 依次检查每个路径
for search_path in search_paths:
self.log_verbose(f"\n[搜索] 检查路径: {search_path}")
is_hugo_project, archetype_path, content_path = check_hugo_project(search_path)
if is_hugo_project:
self.log_verbose(f"[成功] 找到Hugo项目根目录: {search_path}")
return search_path, archetype_path, content_path
# 如果都没找到,使用默认路径(向后兼容)
self.log_verbose(f"\n[警告] 未找到标准Hugo项目结构")
self.log_verbose(f"[警告] 使用默认路径配置")
# 默认使用脚本目录的上级作为项目根目录
default_root = self.script_dir.parent
default_archetype = default_root / "archetypes" / "default.md"
default_content = default_root / "content"
self.log_verbose(f"[默认] 项目根目录: {default_root}")
self.log_verbose(f"[默认] archetype路径: {default_archetype}")
self.log_verbose(f"[默认] content路径: {default_content}")
return default_root, default_archetype, default_content
def group_files_by_directory(self, file_list):
"""按目录分组文件"""
groups = {}
for file_path in file_list:
# 获取相对于项目根目录的路径
try:
relative_path = file_path.relative_to(self.project_root)
dir_path = str(relative_path.parent)
if dir_path == '.':
dir_path = ''
else:
dir_path += '/'
except ValueError:
# 如果文件不在项目根目录下,使用绝对路径的父目录
dir_path = str(file_path.parent) + '/'
filename = file_path.name
if dir_path not in groups:
groups[dir_path] = []
groups[dir_path].append(filename)
return groups
def format_grouped_files(self, groups, max_line_length=100):
"""格式化分组文件显示 - 完整显示所有文件"""
if not groups:
return []
lines = []
for dir_path, files in groups.items():
# 格式化目录显示
if dir_path:
dir_prefix = f"[处理] {dir_path} → "
else:
dir_prefix = "[处理] "
# 当前行,从目录前缀开始
current_line = dir_prefix
# 添加所有文件名
for i, filename in enumerate(files):
if i > 0:
current_line += ", "
# 检查添加当前文件名后是否会超出行长度
test_line = current_line + filename
# 如果超出长度且不是第一个文件,则换行
if len(test_line) > max_line_length and i > 0:
lines.append(current_line.rstrip(", "))
# 新行缩进对齐,使用空格对齐到箭头后
indent = " " * (len(dir_prefix))
current_line = indent + filename
else:
current_line += filename
# 添加当前目录的最后一行
if current_line.strip():
lines.append(current_line)
return lines
def preview_file(self, file_path):
"""预览模式:显示将要添加的Front Matter但不修改文件"""
try:
file_path = Path(file_path)
if not file_path.exists():
error_msg = f"文件不存在"
self.log_verbose(f"[错误] 文件不存在: {file_path}")
self.error_files.append((file_path, error_msg))
self.error_count += 1
return False
if file_path.suffix.lower() != '.md':
self.log_verbose(f"[警告] 跳过非Markdown文件: {file_path}")
self.skipped_count += 1
return False
# 检测文件编码
encoding = self.detect_encoding(file_path)
# 读取文件内容
with open(file_path, 'r', encoding=encoding) as f:
content = f.read()
# 检查是否已有Front Matter
if self.has_frontmatter(content):
self.log_verbose(f"[跳过] {file_path.name} - 已有Front Matter")
self.skipped_files.append(file_path)
self.skipped_count += 1
return True
# 生成Front Matter(预览)
title = self.generate_title_from_filename(file_path)
date = self.get_file_creation_time(file_path)
front_matter = self.template.format(
title=title,
date=date
)
# 显示预览信息
print(f"\n[预览] {file_path}")
print("--- 将要添加的Front Matter ---")
print(front_matter.rstrip())
print("---------------------------")
# 收集预览结果
self.preview_files.append(file_path)
self.processed_count += 1
return True
except Exception as e:
error_msg = f"预览出错: {str(e)}"
self.log_verbose(f"[错误] 预览文件时出错: {e}")
self.error_files.append((file_path, error_msg))
self.error_count += 1
return False
def parse_title_template(self, front_matter_content):
"""解析title模板类型,确定title生成模式"""
try:
# 查找title行
lines = front_matter_content.split('\n')
title_line = None
for line in lines:
if line.strip().lower().startswith('title:'):
title_line = line.strip()
break
if not title_line:
self.log_verbose(f"[模板分析] 未找到title字段,使用默认模式")
return 'fallback'
# 提取引号内的内容
if '"' in title_line:
# 查找第一个和最后一个引号之间的内容
first_quote = title_line.find('"')
last_quote = title_line.rfind('"')
if first_quote != -1 and last_quote != -1 and first_quote != last_quote:
title_template = title_line[first_quote+1:last_quote]
else:
self.log_verbose(f"[模板分析] 无法解析引号内容,使用默认模式")
return 'fallback'
else:
self.log_verbose(f"[模板分析] 未找到引号,使用默认模式")
return 'fallback'
self.log_verbose(f"[模板分析] 原始title模板: {title_template}")
# 判断模板类型
if '{{ .File.ContentBaseName }}' in title_template:
# 原样使用文件名
self.log_verbose(f"[模板分析] 检测到原样模式: 使用文件名原样作为title")
return 'as_is'
elif 'replace .File.ContentBaseName' in title_template and 'title' in title_template:
# 格式化模式:替换连字符并标题化
self.log_verbose(f"[模板分析] 检测到格式化模式: 替换连字符并标题化")
return 'formatted'
else:
# 其他情况使用回退模式
self.log_verbose(f"[模板分析] 未识别的模板格式,使用回退模式: {title_template}")
self.log_verbose(f"[TODO] 未来可扩展支持该模板格式")
# TODO 未来可扩展支持该模板格式
return 'fallback'
except Exception as e:
self.log_verbose(f"[模板分析] 解析title模板时出错: {e}")
return 'fallback'
def load_template(self):
"""从archetypes/default.md加载Front Matter模板"""
try:
if not self.archetype_path.exists():
raise FileNotFoundError(f"模板文件不存在: {self.archetype_path}")
# 检测编码并读取文件
encoding = self.detect_encoding_simple(self.archetype_path)
with open(self.archetype_path, 'r', encoding=encoding) as f:
content = f.read()
# 提取Front Matter部分
if not content.strip().startswith('---'):
raise ValueError("模板文件格式错误:必须以 --- 开头")
# 查找两个 --- 之间的内容
parts = content.split('---', 2)
if len(parts) < 3:
raise ValueError("模板文件格式错误:缺少结束的 ---")
front_matter = parts[1].strip()
# 解析title模板类型并设置生成模式
self.title_generation_mode = self.parse_title_template(front_matter)
self.log_verbose(f"[模板] title生成模式: {self.title_generation_mode}")
# 处理Hugo模板语法,转换为Python格式化字符串
# 替换 {{ replace .File.ContentBaseName "-" " " | title }} 为 {title}
front_matter = re.sub(r'title:\s*".*?{{\s*.*?\s*}}"', 'title: "{title}"', front_matter)
# 替换 {{ .Date }} 为 {date}
front_matter = re.sub(r'{{\s*\.Date\s*}}', '{date}', front_matter)
# 构建完整模板
template = f"---\n{front_matter}\n---\n\n"
self.log_verbose(f"[模板] 成功加载模板: {self.archetype_path}")
return template
except Exception as e:
self.log_verbose(f"[错误] 加载模板失败: {e}")
self.log_verbose(f"[错误] 模板路径: {self.archetype_path}")
# 返回默认模板作为回退
default_template = """---
title: "{title}"
#slug: ""
description: "Meta标签 页面描述"
#summary: "文章预览 内容摘要"
date: {date}
#lastmod: {date}
draft: false
categories: []
tags: []
---
"""
# 设置内置模板的title生成模式为fallback(文件名原样)
self.title_generation_mode = 'fallback'
self.log_verbose(f"[模板] 内置模板title生成模式: {self.title_generation_mode}")
self.log_verbose("[警告] 使用内置默认模板")
return default_template
def detect_encoding_simple(self, file_path):
"""简单的编码检测方法(用于模板加载)"""
# 尝试常见编码格式
encodings = ['utf-8', 'gbk', 'gb2312', 'utf-16']
for encoding in encodings:
try:
with open(file_path, 'r', encoding=encoding) as f:
f.read()
return encoding
except (UnicodeDecodeError, UnicodeError):
continue
# 如果都失败了,使用utf-8
return 'utf-8'
def detect_encoding(self, file_path):
"""检测文件编码"""
# 尝试常见编码格式
encodings = ['utf-8', 'gbk', 'gb2312', 'utf-16']
for encoding in encodings:
try:
with open(file_path, 'r', encoding=encoding) as f:
f.read()
return encoding
except (UnicodeDecodeError, UnicodeError):
continue
# 如果都失败了,使用utf-8并忽略错误
return 'utf-8'
def has_frontmatter(self, content):
"""检查内容是否已有Front Matter"""
# 检查是否以---开头
return content.strip().startswith('---')
def generate_title_from_filename(self, file_path):
"""根据模板模式从文件名生成标题"""
filename = Path(file_path).stem
if self.title_generation_mode == 'as_is':
# 原样模式:直接使用文件名
title = filename
self.log_verbose(f" [title生成] 原样模式: {title}")
elif self.title_generation_mode == 'formatted':
# 格式化模式:替换连字符和下划线为空格,并进行标题化
title = filename.replace('-', ' ').replace('_', ' ').title()
self.log_verbose(f" [title生成] 格式化模式: {filename} -> {title}")
else:
# 回退模式:使用文件名原样(安全选择)
title = filename
self.log_verbose(f" [title生成] 回退模式: {title}")
return title
def get_file_creation_time(self, file_path):
"""获取文件创建时间"""
try:
# Windows系统使用st_ctime,Unix系统使用st_birthtime(如果支持)
stat = os.stat(file_path)
if hasattr(stat, 'st_birthtime'):
# macOS和一些Unix系统
creation_time = stat.st_birthtime
else:
# Windows和其他系统,使用修改时间作为创建时间的近似
creation_time = stat.st_mtime
# 转换为ISO格式,包含时区信息
dt = datetime.fromtimestamp(creation_time)
return dt.strftime('%Y-%m-%dT%H:%M:%S+08:00')
except:
# 如果获取失败,使用当前时间
return datetime.now().strftime('%Y-%m-%dT%H:%M:%S+08:00')
def create_backup(self, file_path):
"""创建文件备份"""
try:
file_path = Path(file_path)
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
backup_name = f"{file_path.stem}_{timestamp}.md.backup"
backup_path = self.backup_dir / backup_name
shutil.copy2(file_path, backup_path)
self.created_backups.append((file_path, backup_path))
self.log_verbose(f" [备份] 备份创建: {backup_path}")
return backup_path
except Exception as e:
self.log_verbose(f" [错误] 备份失败: {e}")
return None
def restore_from_backup(self, original_path, backup_path):
"""从备份恢复文件"""
try:
shutil.copy2(backup_path, original_path)
self.log_verbose(f" [恢复] 已从备份恢复: {original_path}")
return True
except Exception as e:
self.log_verbose(f" [错误] 恢复失败: {e}")
return False
def process_file(self, file_path):
"""处理单个文件"""
# 预览模式下调用专门的预览方法
if self.preview_mode:
return self.preview_file(file_path)
try:
file_path = Path(file_path)
if not file_path.exists():
error_msg = f"文件不存在"
self.log_verbose(f"[错误] 文件不存在: {file_path}")
self.error_files.append((file_path, error_msg))
self.error_count += 1
return False
if file_path.suffix.lower() != '.md':
self.log_verbose(f"[警告] 跳过非Markdown文件: {file_path}")
self.skipped_count += 1
return False
self.log_verbose(f"\n[处理] 处理文件: {file_path}")
# 检测文件编码
encoding = self.detect_encoding(file_path)
# 读取文件内容
with open(file_path, 'r', encoding=encoding) as f:
content = f.read()
# 检查是否已有Front Matter
if self.has_frontmatter(content):
self.log_verbose(f" [跳过] 已有Front Matter,跳过")
self.skipped_files.append(file_path)
self.skipped_count += 1
return True
# 创建备份
backup_path = self.create_backup(file_path)
if not backup_path:
error_msg = f"无法创建备份"
self.log_verbose(f" [错误] 无法创建备份,跳过处理")
self.error_files.append((file_path, error_msg))
self.error_count += 1
return False
# 生成Front Matter
title = self.generate_title_from_filename(file_path)
date = self.get_file_creation_time(file_path)
front_matter = self.template.format(
title=title,
date=date
)
# 添加Front Matter到内容开头
new_content = front_matter + content
# 写入文件
try:
with open(file_path, 'w', encoding='utf-8') as f:
f.write(new_content)
self.log_verbose(f" [成功] Front Matter已添加")
self.log_verbose(f" 标题: {title}")
self.log_verbose(f" 日期: {date}")
self.processed_files.append(file_path)
self.processed_count += 1
return True
except Exception as e:
error_msg = f"写入失败: {str(e)}"
self.log_verbose(f" [错误] 写入文件失败: {e}")
# 尝试从备份恢复
self.restore_from_backup(file_path, backup_path)
self.error_files.append((file_path, error_msg))
self.error_count += 1
return False
except Exception as e:
error_msg = f"处理出错: {str(e)}"
self.log_verbose(f"[错误] 处理文件时出错: {e}")
self.error_files.append((file_path, error_msg))
self.error_count += 1
return False
def is_absolute_path(self, path):
"""判断是否为绝对路径"""
path_str = str(path)
# Windows: C:\ 或 \\ 开头
if os.name == 'nt':
return (len(path_str) >= 3 and path_str[1] == ':') or path_str.startswith('\\\\')
# Unix/Linux: / 开头
else:
return path_str.startswith('/')
def resolve_path(self, path_arg):
"""解析路径参数"""
if self.is_absolute_path(path_arg):
return Path(path_arg).resolve()
else:
# 相对路径,相对于当前工作目录
return Path.cwd() / path_arg
def collect_markdown_files(self, path):
"""收集指定路径下的所有Markdown文件"""
path = Path(path)
markdown_files = []
if path.is_file():
if path.suffix.lower() == '.md':
markdown_files.append(path)
elif path.is_dir():
# 递归搜索目录中的所有.md文件
markdown_files = list(path.rglob('*.md'))
return markdown_files
def process_paths(self, paths):
"""处理多个路径参数"""
all_files = []
for path_arg in paths:
resolved_path = self.resolve_path(path_arg)
if not resolved_path.exists():
self.log_verbose(f"[错误] 路径不存在: {resolved_path}")
self.error_count += 1
continue
files = self.collect_markdown_files(resolved_path)
all_files.extend(files)
# 去重并排序
unique_files = sorted(set(all_files))
if not unique_files:
self.log_default("[错误] 没有找到Markdown文件")
return False
self.log_verbose(f"[搜索] 找到 {len(unique_files)} 个Markdown文件")
# 处理所有文件
for file_path in unique_files:
self.process_file(file_path)
return True
def print_summary(self):
"""打印处理摘要"""
elapsed_time = time.time() - self.start_time
if self.log_level == LogLevel.QUIET:
# 静默模式:只显示统计结果
if self.preview_mode:
print(f"预览完成: WILL{self.processed_count} SKIP{self.skipped_count} ERR{self.error_count}")
else:
print(f"处理完成: OK{self.processed_count} SKIP{self.skipped_count} ERR{self.error_count}")
return
if self.log_level == LogLevel.DEFAULT:
# 默认模式:紧凑结构化输出
total_files = self.processed_count + self.skipped_count + self.error_count
# 显示处理结果
files_to_show = self.preview_files if self.preview_mode else self.processed_files
if files_to_show:
# 按目录分组显示处理的文件
processed_groups = self.group_files_by_directory(files_to_show)
lines = self.format_grouped_files(processed_groups)
for line in lines:
# 预览模式使用不同的标签
if self.preview_mode:
print(line.replace("[处理]", "[预览]"))
else:
print(line)
# 显示跳过和错误信息
info_parts = []
if self.skipped_count > 0:
if self.skipped_count <= 5:
# 如果跳过文件不多,可以简单提及
info_parts.append(f"跳过 {self.skipped_count} 个已有Front Matter的文件")
else:
info_parts.append(f"跳过 {self.skipped_count} 个文件")
if self.error_files:
error_names = ", ".join([f.name for f, _ in self.error_files[:2]])
if len(self.error_files) > 2:
error_names += f"... ({len(self.error_files)}个)"
info_parts.append(f"错误: {error_names}")
if info_parts:
print(f"[跳过] {' | '.join(info_parts)}")
elif self.skipped_count > 0 and self.processed_count == 0:
print(f"[结果] 所有 {total_files} 个文件已有Front Matter,无需处理")
# 最终统计
status_parts = []
if self.processed_count > 0:
if self.preview_mode:
status_parts.append(f"WILL处理 {self.processed_count}")
else:
status_parts.append(f"OK处理 {self.processed_count}")
if self.skipped_count > 0:
status_parts.append(f"SKIP跳过 {self.skipped_count}")
if self.error_count > 0:
status_parts.append(f"ERR错误 {self.error_count}")
backup_info = f"备份: {len(self.created_backups)}个" if self.created_backups else ""
time_info = f"用时: {elapsed_time:.1f}s"
final_parts = [" ".join(status_parts)]
if backup_info and not self.preview_mode: # 预览模式不显示备份信息
final_parts.append(backup_info)
final_parts.append(time_info)
completion_label = "[预览完成]" if self.preview_mode else "[完成]"
print(f"{completion_label} {' | '.join(final_parts)}")
# 预览模式的提示
if self.preview_mode and self.processed_count > 0:
print("[提示] 使用不带 --preview/-p 参数来实际执行修改")
elif self.log_level == LogLevel.VERBOSE:
# 详细模式:显示完整信息
print(f"\n" + "="*50)
print(f"[摘要] 处理完成摘要:")
print(f" [成功] 成功处理: {self.processed_count} 个文件")
print(f" [跳过] 跳过文件: {self.skipped_count} 个文件")
print(f" [错误] 错误文件: {self.error_count} 个文件")
print(f" [备份] 创建备份: {len(self.created_backups)} 个文件")
print(f" [耗时] 总用时: {elapsed_time:.2f}s")
if self.created_backups:
print(f"\n[备份] 备份文件位置: {self.backup_dir}")
if self.error_files:
print(f"\n[错误详情]")
for file_path, error_msg in self.error_files:
print(f" - {file_path.name}: {error_msg}")
def main():
parser = argparse.ArgumentParser(
description='Hugo博客Front Matter自动添加工具',
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
使用示例:
python add_frontmatter.py # 默认处理 content/ 目录
uv run add_frontmatter.py # 默认处理 content/ 目录
python add_frontmatter.py article1.md article2.md
python add_frontmatter.py posts/
python add_frontmatter.py /absolute/path/to/article.md
python add_frontmatter.py ../drafts/ current.md
python add_frontmatter.py -p posts/ # 预览模式
python add_frontmatter.py -d article.md # 调试模式
python add_frontmatter.py -v # 显示版本
"""
)
parser.add_argument(
'paths',
nargs='*', # 改为 '*' 允许0个或多个参数
help='要处理的文件或目录路径(支持多个)。不指定时默认处理 content/ 目录'
)
parser.add_argument(
'--preview', '-p',
action='store_true',
help='预览模式,显示将要添加的Front Matter但不实际修改文件'
)
parser.add_argument(
'--debug', '-d',
action='store_true',
help='调试输出模式,显示完整的处理过程和调试信息'
)
parser.add_argument(
'--quiet', '-q',
action='store_true',
help='静默模式,只显示最终统计结果'
)
parser.add_argument(
'--version', '-v',
action='version',
version=f'%(prog)s {__version__}',
help='显示版本信息'
)
args = parser.parse_args()
# 确定日志级别
if args.quiet:
log_level = LogLevel.QUIET
elif args.debug:
log_level = LogLevel.VERBOSE
else:
log_level = LogLevel.DEFAULT
# 参数冲突检查
if args.quiet and args.debug:
print("[错误] --quiet 和 --debug 不能同时使用")
return 1
# 初始化处理器(会进行路径查找)
processor = FrontMatterProcessor(log_level=log_level, preview_mode=args.preview)
# 根据日志级别显示启动信息
if log_level == LogLevel.QUIET:
pass # 静默模式不显示启动信息
elif log_level == LogLevel.DEFAULT:
# 默认模式:简洁启动信息
project_name = processor.project_root.name
total_files = len(list(processor.content_path.rglob('*.md'))) if processor.content_path.exists() else 0
scan_path = processor.content_path.name if processor.content_path else "未知"
print(f"[启动] Hugo Front Matter 工具 | 项目: {project_name} | 扫描: {scan_path} | 找到 {total_files} 个文件")
else: # VERBOSE
# 详细模式:完整启动信息
print("[启动] Hugo Front Matter 自动添加工具")
print(f"[信息] 当前工作目录: {Path.cwd()}")
print(f"[信息] 项目根目录: {processor.project_root}")
print(f"[信息] 备份目录: {processor.backup_dir}")
# 如果没有指定路径参数,使用默认的 content/ 目录
if not args.paths:
if processor.content_path and processor.content_path.exists():
processor.log_verbose(f"[默认] 使用默认 content 目录: {processor.content_path}")
args.paths = [str(processor.content_path)]
else:
processor.log_default(f"[错误] 未找到 content 目录: {processor.content_path}")
processor.log_default(f"[提示] 请指定要处理的文件或目录路径")
if log_level != LogLevel.QUIET:
parser.print_help()
return 1
# 预览模式的提示在处理器初始化时已处理
try:
processor.process_paths(args.paths)
processor.print_summary()
return 0
except KeyboardInterrupt:
print(f"\n[中断] 用户中断操作")
return 1
except Exception as e:
print(f"\n[错误] 程序出错: {e}")
return 1
if __name__ == '__main__':
sys.exit(main())
AI翻译文档 #
(还没做,敬请期待…)