Skip to main content

Hugo搭建博客入坑

··5157 words·25 mins

前言 #

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.tomllanguages.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/下的相对路径。在你的博客项目找到这个文件夹📁,把你的头像图片复制拖拽进去。

💡 Tip

图片文件你最好压缩成 WebP 减少体积

菜单配置 #

menus.*.toml:菜单导航配置,*替换为对应的语言代码如menus.en.tomlmenus.zh-Hans.toml
两种菜单导航可以配置,main在顶栏,footer在页脚。

配置参数:

  • name:菜单导航名
  • weight:权重,决定排序
  • pageRef:菜单路径,用于引用内容页面分类法
    ⚠️ Warning

    ⚠️引用的页面要实际存在才能生效!下文内容组织章节将会完整讲解到这一切~

  • url:直接链接URL
  • params:额外参数设置,可以添加icon图标、showNametarget

参考配置文件:

[[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设置主题配色方案,参阅配色方案
autoSwitchAppearancetrue根据访客系统自动切换主题外观
enableSearchtrue站点搜索功能
enableCodeCopytrue代码块复制
header.layouthybrid顶部栏&菜单的布局样式
设置成混合让移动设备宽度不足自动折叠顶栏
footer.showAppearanceSwitchertrue网站页脚显示外观切换器
homepage.layout主页的布局模式,参阅主页布局
homepage.showRecenttrue是否在首页显示近期文章列表
homepage.recentLimit显示近期文章的最大数量
article.showDateUpdatedtrue是否显示文章的更新日期
article.showAuthor是否在文章页脚显示作者信息框
article.showEdit是否显示编辑文章内容的链接
article.showTableOfContentstrue是否在文章中显示目录
markup.toml修改tableOfContents-目录层级显示
article.showTaxonomies是否显示与本文相关的分类法(即分类和标签)
article.showWordCounttrue是否显示文章字数统计
article.showCommentstrue是否在文章页脚后包含评论部分
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下所有内容列表。

例如:

  1. 网站根目录的 _index.md

    content/
    └── _index.md			# 控制网站首页
    
  2. Section 根目录的 _index.md

content/
└── posts/
    └── _index.md		# 控制 /posts/ 列表页
  1. 子目录的 _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文件,图片命名为thumbthumbnail则作为文章缩略图文章列表展示,而命名为cover则在该文章页面的内容顶部展示,而featured其存在时同时替代thumbcover的作用。

特性Branch BundleLeaf Bundle
索引文件_index.mdindex.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标题,文章的显示标题
slugURL 别名,自定义文章的 URL 路径,如不设置则使用文件名
description页面描述,用于 HTML meta 标签,影响 SEO 搜索结果显示
summary内容摘要,可作为文章列表的预览内容,如不设置 Hugo 会自动截取文章开头部分
date创建时间,文章发布日期,可自动生成,影响文章排序
lastmod修改时间,文章最后修改时间,可通过配置自动根据文件修改时间更新
draft草稿状态,设为 true 时文章不会在生产环境中显示,默认为 false
isCJKLanguage中日韩语言标识,设为true可优化中文等 CJK 语言的摘要截取和字数统计
categories&tags分类和标签,Hugo 默认的 Taxonomies(分类法),用于内容组织和导航

更多 Front Matter 字段请参阅: hugoCongo主题 的文档

额外的补充内容

  • 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,具体使用跳转到本文最后的 其他资源 章节查看。

Important

如果你不想丢失文件原始创建时间元信息,那最好在你的文档复制到博客📂之前自动添加 Front Matter,不然你得一个个手动添加创建时间了…

便捷修改 #

不过还是要对文档的元信息做具体的修改,比如概括描述、slug、分类、标签等。这里我推荐 Obsidian 这个前面最开始提到过的 markdown 编辑器,它对文档的 Front Matter 的编辑功能做到很不错,谁用谁知道!最让我夸赞的一点就是────编辑 tag 标签时候,它会提供目前所有文档用过的标签作为候选选择!!
obsidian-frontmatter

批量管理文档 #

最后再推荐一个强力的 VSCode 插件────Front Matter CMSGitHub VSCode 插件
基于 Front Matter 的文档内容管理系统,根据时间、分类标签等批量管理所有文档。
Front-Matter-CMS

网站图标(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 #

  1. 访问官网(analytics.google.com/):次使用点击“开始衡量”注册账号,若有账号就登录

  2. 继续下步骤把信息填完…

  3. 开始收集数据:选择平台“网站”,填入信息点击确认

  4. 复制衡量 ID,类似于一串G-XXXXXXXXX的字符串。

  5. 配置文件:config/_default/hugo.toml在这个文件里填入:

    [services]
      [services.googleAnalytics]
        id = 'G-XXXXXXXXX'
    

    然后就会自动添加上谷歌的脚本。这适用于Congo 主题,如果你是其他主题可能也支持该操作,具体得查看对应的文档。实在没有这么简单的操作还可以选择手动添加谷歌脚本到layouts/partials/analytics.html文件里

Umami Analytics #

文档

评论系统 #

一个优秀的博客当然不能缺少评论系统集成。对于评论系统首要考虑的是时效通知托管部署的这两大问题。

  1. 即时通知是最重要的,如果评论不能通知到对方,那整个评论系统将没有任何意义!一般常规操作是使用邮件来通知。大部分的评论系统要么需要自建邮局和设置SMTP,要么邮件代发服务;前者自建邮局搞起来挺麻烦的,但最棘手让人头疼问题──垃圾邮件──大部分自建邮局的归途,发的邮件全都被丢垃圾箱了;后者邮件代发服务,需要付费,免费额度每天只有一点,但对于个人博客的使用场景完全够用了。
  2. 托管与部署:评论系统可以选择自托管,完全掌控全部包括评论数据,那就需要数据库、服务器了,部署起来是有点小麻烦,考虑的事情也比较多。

综上所述,Giscus ── 首选适合的评论系统,快速上手,简单部署。

Giscus #

一个由 GitHub Discussions 驱动的评论系统。让访客通过 GitHub 在你的博客留下评论。

优势:

  • 快速上手,简单部署:直接使用 GitHub来托管评论数据,集成在博客只需要通过生成器的一段js脚本。
  • 开源免费
  • 自带登录认证

劣势:

  • 依赖GitHub,必须登录GitHub账号,评论所在仓库必须公开
  • 缺乏丰富的自定义功能
  • 评论不能先审后发
Giscus

官方配置生成器开始使用吧! #

  1. 创建一个公开的 GitHub 仓库,可以直接选择博客项目仓库,亦可专门创建一个仓库如myblog-comment作为评论托管
  2. 安装 Giscus app:安装这个GitHub APP,并为刚刚的仓库启用。
  3. 启用 Discussions 功能:在仓库设置里启用。

继续回到官方配置生成器

  1. 仓库选择:填入仓库名如YourGitHubUsername/my-blog-comments,Giscus 会自动检查仓库是否有效以及 App 是否已安装。
  2. 页面 ↔️ discussion 映射关系
    这是最关键的一步,它决定了如何将你的博客文章与 GitHub Discussion 讨论帖对应起来。对于大多数 Hugo 博客,推荐使用以下两种稳定且不易出错的方案:
    • pathname:使用文章的相对 URL 路径作为唯一标识。这是最推荐的选项。这与前面说过的hugo于文章的slug关联起来了
    • URL:使用文章的完整 URL 作为唯一标识。
  3. Discussion 分类 (Category):选择一个讨论分类用于存放评论。推荐使用公告(announcements)类型的分类并删除其他分类,以确保新 discussion 只能由仓库维护者和 giscus 创建。或者新建一个专用分类如Comments然后设置权限也是一样的。
  4. 功能 (Features):
    • 启用主帖子上的反应(reaction):建议勾选,允许访客对你的文章点赞或送上其他表情。
    • 懒加载评论(Lazy Loading):强烈建议勾选!这会使评论区只在用户滚动到页面底部时才加载,能显著提升你博客的初始加载速度。
  5. 主题 (Theme):默认选择 用户偏好的色彩方案Preferred Color Scheme
  6. 生成脚本:完成以上所有配置后,页面底部会自动生成一段 <script> 代码。点击 “Copy” 按钮复制这段代码

对于使用 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>

评论及时邮件通知 #

Important

使用 Giscus 作为评论系统还有一点重要设置──确保别人发评论我们能及时收到邮件通知

在 Giscus 评论所在 GitHub 仓库点击 Watch,这样当有文章底下有新的评论(即新的 Discussions 发布),我们就能第一时间收到邮件通知了。
选择All ActivityCustom->Discussions都行。最终只要确保有新的 Discussions 能及时邮件通知。
你可以让你的朋友(或者我)帮你评论测试一下是否能及时邮件通知。

GitHub-watch(Giscus-Email) GitHub-Watch-Custom(Giscus-Email)

Settings->Notifications->Subscriptions:确保全部选项开启邮件通知(一般全部是默认开启了,无需额外修改)

GitHub-Settings-Notifications-Subscriptions(Giscus-Email)

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 还有一个优势就是国内直连访问速度快。

  1. 首先在项目根目录添加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 会识别到配置文件并应用的。

  2. 进入官网、登录 GitHub 账号,Add new project添加新项目,导入 Git 远程仓库,GitHub 授权 读取仓库的权限可选全部仓库/指定仓库,建议最小权限只给导入的仓库权限。后续随时可在 Applications设置 里修改 **Netlify 配置 **- 仓库权限Repository access

  3. 设置项目名(Project name),这将决定了部署的网址https://xxx.netlify.app/,其他配置不管(因为前面netlify.toml文件已经一键配置好了),点击部署,接下来请坐和放宽~ 不到半分钟就构建部署完成

  4. 访问https://项目名.netlify.app/就能看到博客网站了!

  5. 自定义域名:在项目的 域名管理(Domain management) 里添加自己的域名,并在域名托管商添加一个 CNAME 类型的 DNS 记录指向刚刚的 Netlify 部署的地址。如果遇到了 CORS跨域问题,记得点击重新构建(点击任意一个 Production Deploys 详情里找到 Options 按钮)。
    Netlify-retry

排错:

  • 如果你使用的是 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 CMSPages CMSDecap CMS、、Tina CMS 等)与其使用。既有静态站点优势,又能提供一个友好的可视化后端管理界面。

这里我推荐使用 Sveltia CMSPages CMS 作为我们博客后台的管理系统。你可以二选一,或者两个都试试,不会冲突。两者各有特色,Sveltia CMS 是嵌入博客网站的后台管理系统,Pages CMS 最大特点是有在线平台直接使用如果你想更简单、快速上手就使用它吧。两者配置都有配置说明,跳转查看~

Pages CMS #

Pages CMS 最大优势在于──在线 CMS 后台内容管理,直接于你的 GitHub 博客项目对接管理!

Pages-CMS
  1. 配置文件:在项目根目录创建.pages.yml文件。完整配置示例如下:
    # ===========================================
    # 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: 友链页面内容
    
    根据自己所需修改配置文件,详细参考 Pages CMS 文档
  2. 提交和推送到 GitHub 远程仓库
  3. 在线 CMS:点击 app.pagescms.org 访问 Pages CMS 在线版本,省去部署麻烦。登录 GitHub 账号,授权 GitHub OAuth App 对于博客仓库的权限。登录成功之后选择博客项目,如果一切正常就能顺利看到文章列表。

Sveltia CMS #

Sveltia CMS 是基于 Decap CMS 的现代化替代方案,不仅保持配置兼容,还在此基础上升级和扩展,具有以下优势:

  • 现代化界面、适配移动端、自动深色模式
  • 高性能、加载快、页面流畅
  • 更好可视化编辑器,自动保存、多媒体管理
  • 等等更多…
Sveltia-CMS_1 Sveltia-CMS_2 Sveltia-CMS_3

配置 #

  1. 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 详细介绍了相关内容。

  2. 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>
    
  3. 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改为对应的文件夹路径

  4. 配置后端:无头 CMS 需要校验登录才能进入后台管理,前面我们选择 GitHub 作为了博客项目的远程存储仓库所以直接选择配置 GitHub 作为后端登录。
    因为 GitHub 硬性要求身份验证需要一个后端【1】【2】,所以需要配置 OAuth 客户端
    如果前面使用 Netlify 来构建和部署博客网站,那就简化省略一些步骤,只需要创建 GitHub OAuth Apps 而不需要部署额外的后端程序
    接下来继续跳转阅读下一步吧~

GitHub OAuth Provider #

  1. 创建一个OAuth AppsSettings -> Developer Settings -> OAuth Apps
  2. Authorization callback URL:回调URL填入https://api.netlify.com/auth/done。其他字段根据自己的喜好随意填写。
  3. Client ID:记下客户端ID
  4. Client secrets:生成一个客户端密钥并牢牢记下,之后就不能再次查看了!
  5. 客户端ID客户端密钥添加到 Netlify 的部署站点:前往对应项目的Project configuration > Access & security > OAuth中,在Authentication providers下点击Install Provider,选择**GitHub**,填入Client IDClient 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翻译文档 #

(还没做,敬请期待…)