Hugo

Hugo 永久链接自定义配置

警告
本文最后更新于 2026-04-03,文中内容可能已过时。

Hugo 永久链接默认使用 Markdown 文件名作为 slug,文件名不易辨认(如 example.md),如果使用中(如 示例.md),slug 可能变成编码字符串,仍然不直观。通过配置 :slugorcontentbasename,可优先使用 Front Matter 中的 slug 字段,保持易读的文件名和简洁的 URL。1

下面介绍配置修改方法,以及一个无需依赖的 Python 脚本,批量为现有文章添加 slug 并重命名。

准备工具

在开始之前,准备以下工具:

  • Python(必需):用于运行批量处理脚本,安装时勾选 Add Python to PATH
  • Git(必需):用于拉取代码和版本管理。
  • VS Code(可选):用于查看和编辑脚本、配置文件和 Markdown 文件。

配置永久链接

修改主题配置文件:

TOMLconfig/default/hugo.toml
1
2
[Permalinks]
posts = ":slugorcontentbasename"
  • :slugorcontentbasename 需要 Hugo 版本 ≥ v0.144.0。
  • 如果 Front Matter 没有 slug 字段,Hugo 默认会使用文件路径生成 URL。

保存配置后,无需其他更改,新的文章可通过 slug 自定义 URL,旧文章保持不变。

创建处理脚本

⚠️ 注意:脚本默认 Front Matter 使用 --- 包裹,且格式规范。

创建以下文件:

Pythonscripts/slug-rename.py
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
#!/usr/bin/env python3
"""
批量处理 Markdown 文件:
1. 为没有 slug 字段的文件添加 slug: "原文件名"(插入在 title 行之后,保持原格式)
2. 将文件重命名为 Front Matter 中的 title(自动处理非法字符)
用法:
    python scripts/slug-rename.py                    # 处理当前目录
    python scripts/slug-rename.py dir1 dir2 ...      # 处理多个指定目录
"""
import re
import sys
from pathlib import Path

def sanitize_filename(name):
    """移除 Windows/Linux 文件名中的非法字符"""
    return re.sub(r'[\\/*?:"<>|]', '_', name)

def add_slug_to_frontmatter(content, slug_value):
    """
    在 Front Matter 的 title 行之后插入 slug: "value"
    保持原有格式(缩进、引号、数组写法等)
    """
    pattern = r'(---\n)(.*?\n)(---\n)'
    match = re.match(pattern, content, re.DOTALL)
    if not match:
        return content
    
    frontmatter = match.group(2)
    rest = match.group(3) + content[match.end():]
    
    title_line_match = re.search(r'^(title\s*:\s*.+)$', frontmatter, re.MULTILINE)
    if not title_line_match:
        return content
    
    title_line = title_line_match.group(0)
    indent = re.match(r'^(\s*)', title_line).group(1)
    slug_line = f'{indent}slug: "{slug_value}"'
    
    new_frontmatter = frontmatter.replace(title_line, title_line + '\n' + slug_line, 1)
    return f'---\n{new_frontmatter}{rest}'

def process_md_file(filepath):
    print(f"正在处理: {filepath}")
    with open(filepath, 'r', encoding='utf-8') as f:
        content = f.read()
    
    if not content.startswith('---'):
        print(f"  跳过(无 Front Matter)")
        return
    
    fm_match = re.match(r'---\n(.*?)\n---', content, re.DOTALL)
    if not fm_match:
        print(f"  跳过(Front Matter 格式异常)")
        return
    
    frontmatter = fm_match.group(1)
    if re.search(r'^slug\s*:', frontmatter, re.MULTILINE):
        print(f"  跳过(slug 已存在)")
        return
    
    title_match = re.search(r'^title\s*:\s*(.+)$', frontmatter, re.MULTILINE)
    if not title_match:
        print(f"  跳过(无 title 字段)")
        return
    
    title = title_match.group(1).strip()
    if (title.startswith('"') and title.endswith('"')) or (title.startswith("'") and title.endswith("'")):
        title = title[1:-1]
    
    slug_from_filename = filepath.stem
    
    new_content = add_slug_to_frontmatter(content, slug_from_filename)
    
    with open(filepath, 'w', encoding='utf-8') as f:
        f.write(new_content)
    
    new_filename = sanitize_filename(title) + filepath.suffix
    new_filepath = filepath.parent / new_filename
    if filepath.name != new_filename:
        if new_filepath.exists():
            print(f"  警告:目标文件已存在,跳过重命名 {filepath.name} -> {new_filename}")
        else:
            filepath.rename(new_filepath)
            print(f"  ✅ 处理完成:重命名为 {new_filename}")
    else:
        print(f"  ✅ 处理完成:slug 已添加,文件名不变")

def main():
    # 支持多个目录参数
    if len(sys.argv) > 1:
        target_dirs = [Path(p) for p in sys.argv[1:]]
    else:
        target_dirs = [Path.cwd()]
    
    all_files = []
    for target_dir in target_dirs:
        if not target_dir.is_dir():
            print(f"警告:'{target_dir}' 不是有效目录,跳过")
            continue
        # 使用 set 避免重复(虽然不同目录不太可能重复,但安全起见)
        for md_file in target_dir.rglob('*.md'):
            if md_file not in all_files:
                all_files.append(md_file)
    
    print(f"找到 {len(all_files)} 个 Markdown 文件")
    for md_file in all_files:
        process_md_file(md_file)
    print("全部处理完成")

if __name__ == '__main__':
    main()

脚本运行逻辑说明

  1. 遍历目录:接收参数(默认为当前目录),递归查找所有 .md 文件。
  2. 检查 Front Matter:如果无 Front Matter 或已有 slug 字段则跳过。
  3. 提取 title:从 Front Matter 中获取 title 值(去除引号)。
  4. 插入 slug:在 title 行后插入 slug: "原文件名"(保持缩进格式)。
  5. 重命名文件:将文件重命名为 title 内容(非法字符替换为 _),如果目标已存在则跳过。
  6. 输出处理结果:显示每个文件的处理状态。

执行处理脚本

⚠️ 注意:使用前先备份,或先测试后批量处理,操作不可逆。

运行以下命令:

Bash
1
python scripts/slug-rename.py content/posts

脚本会递归处理该目录下的所有 .md 文件,完成后运行 hugo server 启动本地预览,确认文章 URL 保持原来的短标识(如 /posts/hugo-build/),同时检查文件名已更新为可读标题。

故障回退(如发现问题)

  • 如果没有提交更改,可以使用 Git 恢复:git checkout -- content/posts/
  • 如果已经提交但未推送,可以回退到上一个提交:git reset --hard HEAD~1

后续新建文章

创建文章时,可自由指定文件名,并在 Front Matter 中添加 slug 控制 URL。例如:

Bash
1
hugo new posts/tech/这是标题.md

打开 Markdown 文件,在 Front Matter 中添加 slug 参数即可:

Markdown
1
2
3
---
slug: "my-custom-url"
---
留言交流