前言

Hexo和Hugo都是优秀的静态网站生成器,但由于性能考虑或个人喜好,你可能想从Hexo迁移到Hugo。本文将详细记录完整迁移过程,包括配置、文章转换、多语言支持等方面。

准备工作

在开始迁移前,你需要先安装Hugo并了解基本的站点创建流程。以下是官方文档链接,建议按照官方文档进行操作:

1. 安装Hugo

Hugo提供了多种安装方式,包括预编译二进制文件和包管理器。请根据你的操作系统,参考官方安装文档:

2. 创建Hugo站点

安装完Hugo后,你可以使用以下命令创建一个新的Hugo站点:

hugo new site your-site-name
cd your-site-name

更多关于创建站点的详细信息,请参考:

3. 安装主题

Hugo有丰富的主题可供选择,你可以在Hugo主题页面浏览并选择适合的主题。

安装主题的基本步骤如下:

git init
git submodule add https://github.com/theme-author/theme-name.git themes/theme-name

然后在配置文件中设置主题:

echo "theme = 'theme-name'" >> hugo.toml

站点配置

创建hugo.toml配置文件:

baseURL = 'https://www.mfun.ink/'
defaultContentLanguage = 'zh-cn'
title = "Mengboy's Blog"
theme = 'PaperMod'
hasCJKLanguage = true

# Google Analytics配置
GoogleAnalytics = "G-xxxxxxx" # 请替换为您的Google Analytics ID

# 添加permalink配置,使得链接格式与Hexo兼容
[permalinks]
  post = '/:year/:month/:day/:contentbasename/'

# PaperMod主题参数
[params]
  # 设置环境为生产环境,这对于Google Analytics和AdSense加载非常重要
  env = "production"
  
  # 作者信息
  author = "mengboy"
  
  # 主要部分
  mainSections = ['post']
  defaultTheme = "auto"
  ShowReadingTime = true
  ShowShareButtons = true
  ShowPostNavLinks = true
  ShowBreadCrumbs = true
  ShowCodeCopyButtons = true
  ShowRssButtonInSectionTermList = true
  disableSpecial1stPost = false
  disableScrollToTop = false
  hideMeta = false
  hideFooter = false
  
  # 网站favicon设置
  [params.assets]
    favicon = "/images/site/favicon.png"
    favicon16x16 = "/images/site/favicon.png"
    favicon32x32 = "/images/site/favicon.png"
    apple_touch_icon = "/images/site/favicon.png"
  
  # Google AdSense配置 需要自定义扩展,或者主题支持
  googleAdsense = "ca-pub-xxxxxxxxxx" # 请替换为您的Google AdSense发布商ID
  googleAdsenseSlot = "xxxxxxxx" # 请替换为您的Google AdSense广告单元ID
  
  # 侧边栏配置
  [params.profileMode]
    enabled = false
    title = "mengboy"
    subtitle = "记录学习与生活点滴"
    imageUrl = "images/site/favicon.png"
    imageWidth = 120
    imageHeight = 120
    
  # 社交图标
  [[params.socialIcons]]
    name = "github"
    url = "https://github.com/mengboy"
  
  # 搜索设置
  [params.fuseOpts]
    isCaseSensitive = false
    shouldSort = true
    location = 0
    distance = 1000
    threshold = 0.4
    minMatchCharLength = 0
    keys = ["title", "permalink", "summary", "content"]

# 允许HTML
[markup.goldmark.renderer]
  unsafe = true

# 忽略HTML警告
ignoreWarnings = ['raw-html']
ignoreLogs = ['warning-goldmark-raw-html']

[languages]
    [languages.zh-cn]
        contentDir = 'content'
        languageName = '简体中文'
        weight = 10
        title = "Mengboy's Blog"
        
        # 中文主页信息
        [languages.zh-cn.params]
          [languages.zh-cn.params.homeInfoParams]
            Title = "记录学习与生活点滴"
        
        # 中文菜单
        [languages.zh-cn.menu]
          [[languages.zh-cn.menu.main]]
            identifier = "home"
            name = "首页"
            url = "/"
            weight = 10
          [[languages.zh-cn.menu.main]]
            identifier = "categories"
            name = "分类"
            url = "/categories/"
            weight = 20
          [[languages.zh-cn.menu.main]]
            identifier = "tags"
            name = "标签"
            url = "/tags/"
            weight = 30
          [[languages.zh-cn.menu.main]]
            identifier = "archives"
            name = "归档"
            url = "/archives/"
            weight = 40
          [[languages.zh-cn.menu.main]]
            identifier = "about"
            name = "关于我"
            url = "/about/"
            weight = 50
            
    [languages.en]    
        contentDir = 'content/english'
        languageName = 'English'
        weight = 20
        title = "Mengboy's Blog"
        
        # 英文主页信息
        [languages.en.params]
          [languages.en.params.homeInfoParams]
            Title = "Recording Learning and Life"
        
        # 英文菜单
        [languages.en.menu]
          [[languages.en.menu.main]]
            identifier = "home"
            name = "Home"
            url = "/"
            weight = 10
          [[languages.en.menu.main]]
            identifier = "categories"
            name = "Categories"
            url = "/en/categories/"
            weight = 20
          [[languages.en.menu.main]]
            identifier = "tags"
            name = "Tags"
            url = "/en/tags/"
            weight = 30
          [[languages.en.menu.main]]
            identifier = "archives"
            name = "Archives"
            url = "/en/archives/"
            weight = 40
          [[languages.en.menu.main]]
            identifier = "about"
            name = "About Me"
            url = "/en/about/"
            weight = 50

创建内容目录

mkdir -p content/post
mkdir -p content/post/english  # 英文文章目录

创建基础页面

关于页面

mkdir -p content/about

创建中文关于页面 content/about/index.md

---
title: "关于我"
date: 2023-06-01T12:00:00+08:00
---

这是关于我的页面内容...

创建英文关于页面 content/english/about/index.md

---
title: "About Me"
date: 2023-06-01T12:00:00+08:00
---

This is about me page...

其他基础页面参考about页面。

编写转换脚本

创建一个Python脚本来转换Hexo文章到Hugo格式:

#!/usr/bin/env python3
# 将Hexo的Markdown文件转换为Hugo格式

import os
import re
import datetime
import shutil
from pathlib import Path

# 设置路径
hexo_posts_dir = "hexo/source/_posts"
hugo_posts_dir = "hugo/content/post"
hexo_img_dir = "hexo/source/images"
hugo_static_dir = "hugo/static/images"

# 确保目标目录存在
os.makedirs(hugo_posts_dir, exist_ok=True)
os.makedirs(hugo_static_dir, exist_ok=True)

# 复制图片文件夹
def copy_images():
    if os.path.exists(hexo_img_dir):
        print(f"正在复制图片文件从 {hexo_img_dir}{hugo_static_dir}")
        # 复制文件夹内容
        for item in os.listdir(hexo_img_dir):
            s = os.path.join(hexo_img_dir, item)
            d = os.path.join(hugo_static_dir, item)
            if os.path.isdir(s):
                shutil.copytree(s, d, dirs_exist_ok=True)
            else:
                shutil.copy2(s, d)

# 处理YAML front-matter
def convert_front_matter(content):
    # 提取front-matter
    match = re.match(r'^---\s+(.*?)\s+---\s*', content, re.DOTALL)
    if not match:
        return content
    
    front_matter = match.group(1)
    rest_content = content[match.end():]
    
    # 转换标签和分类
    new_front_matter = []
    categories = []
    tags = []
    date = None
    
    for line in front_matter.strip().split('\n'):
        line = line.strip()
        
        # 处理日期
        if line.startswith('date:'):
            date_str = line[5:].strip()
            try:
                date = datetime.datetime.strptime(date_str, '%Y-%m-%d %H:%M:%S')
                new_front_matter.append(f'date: {date.strftime("%Y-%m-%dT%H:%M:%S+08:00")}')
                # 添加lastmod
                new_front_matter.append(f'lastmod: {date.strftime("%Y-%m-%dT%H:%M:%S+08:00")}')
            except ValueError:
                new_front_matter.append(line)
        
        # 处理标签
        elif line.startswith('tags:'):
            tags_str = line[5:].strip()
            if tags_str.startswith('[') and tags_str.endswith(']'):
                # 格式为 tags: [tag1, tag2]
                tags = [tag.strip(' "\'') for tag in tags_str[1:-1].split(',')]
            else:
                # 可能是多行格式
                continue
        
        # 处理分类
        elif line.startswith('categories:'):
            cats_str = line[11:].strip()
            if cats_str.startswith('[') and cats_str.endswith(']'):
                # 格式为 categories: [cat1, cat2]
                categories = [cat.strip(' "\'') for cat in cats_str[1:-1].split(',')]
            else:
                # 可能是多行格式
                continue
        
        # 复制其他行
        elif not (line.startswith('- ') and ('tags:' in front_matter or 'categories:' in front_matter)):
            new_front_matter.append(line)
    
    # 添加分类和标签
    if categories:
        new_front_matter.append(f'categories: {categories}')
    
    if tags:
        new_front_matter.append(f'tags: {tags}')
    
    # 添加slug
    if 'slug:' not in front_matter:
        filename = os.path.splitext(os.path.basename(content))[0]
        new_front_matter.append(f'slug: "{filename}"')
    
    # 组合新的front-matter
    new_content = '---\n' + '\n'.join(new_front_matter) + '\n---\n\n' + rest_content
    return new_content

# 转换Markdown内容
def convert_markdown_content(content):
    # 替换图片链接
    content = re.sub(r'!\[(.*?)\]\((.*?)/images/(.*?)\)', r'![\1](/images/\3)', content)
    
    # 处理更多标记
    content = re.sub(r'<!--\s*more\s*-->', '


', content)
    
    return content

# 主转换函数
def convert_posts():
    print(f"开始转换文章从 {hexo_posts_dir}{hugo_posts_dir}")
    
    files = os.listdir(hexo_posts_dir)
    for file in files:
        if not file.endswith('.md'):
            continue
        
        source_file = os.path.join(hexo_posts_dir, file)
        target_file = os.path.join(hugo_posts_dir, file)
        
        print(f"转换: {source_file} -> {target_file}")
        
        # 读取源文件
        with open(source_file, 'r', encoding='utf-8') as f:
            content = f.read()
        
        # 转换内容
        content = convert_front_matter(content)
        content = convert_markdown_content(content)
        
        # 写入目标文件
        with open(target_file, 'w', encoding='utf-8') as f:
            f.write(content)

# 检测语言并创建多语言文件
def handle_multilingual():
    print("处理多语言文章...")
    
    for file in os.listdir(hugo_posts_dir):
        if not file.endswith('.md'):
            continue
        
        filepath = os.path.join(hugo_posts_dir, file)
        
        # 读取文件内容
        with open(filepath, 'r', encoding='utf-8') as f:
            content = f.read()
        
        # 检查是否有language标记
        lang_match = re.search(r'language:\s*[\'"]?([a-z\-]+)[\'"]?', content)
        if lang_match:
            lang = lang_match.group(1)
            if lang.startswith('en'):
                # 创建英文版本
                en_file = os.path.splitext(file)[0] + '.en.md'
                en_filepath = os.path.join(hugo_posts_dir, en_file)
                
                # 移除语言标记并写入新文件
                content = re.sub(r'language:\s*[\'"]?[a-z\-]+[\'"]?\n', '', content)
                
                with open(en_filepath, 'w', encoding='utf-8') as f:
                    f.write(content)
                
                # 删除原文件
                os.remove(filepath)
                print(f"  创建英文文章: {en_filepath}")

if __name__ == "__main__":
    copy_images()
    convert_posts()
    handle_multilingual()
    print("转换完成!")

运行转换脚本

cd ~/dev/book
python3 convert_hexo_to_hugo.py

创建启动脚本

创建start.sh

#!/bin/bash

# 启动Hugo服务器
hugo server -D

添加执行权限:

chmod +x start.sh

创建部署脚本

创建deploy.sh

#!/bin/bash

echo -e "\033[0;32m部署博客到GitHub Pages...\033[0m"

# 生成静态文件
hugo

# 进入public目录
cd public

# 初始化git仓库
if [ ! -d ".git" ]; then
  git init
  git remote add origin YOUR_GITHUB_REPOSITORY_URL  # 替换为您的GitHub仓库URL
fi

# 添加文件
git add .

# 提交更改
msg="rebuilding site $(date)"
if [ $# -eq 1 ]; then
  msg="$1"
fi
git commit -m "$msg"

# 推送到GitHub
git push -u origin master

# 返回上层目录
cd ..

添加执行权限:

chmod +x deploy.sh

测试运行

./start.sh

创建多语言文章示例

中文文章

hugo new post/my-first-post.md

编辑content/post/my-first-post.md

---
title: "我的第一篇博客"
date: 2023-06-10T15:30:00+08:00
lastmod: 2023-06-10T15:30:00+08:00
categories: ["博客"]
tags: ["Hugo", "入门"]
slug: "my-first-post"
description: "这是我使用Hugo创建的第一篇博客文章"
---

## 介绍

这是我的第一篇Hugo博客文章。

<!--more-->

这是摘要后的内容。

英文文章

hugo new post/english/my-first-post.md

编辑content/english/post/my-first-post.md

---
title: "My First Blog Post"
date: 2023-06-10T15:30:00+08:00
lastmod: 2023-06-10T15:30:00+08:00
categories: ["Blog"]
tags: ["Hugo", "Getting Started"]
slug: "my-first-post"
description: "This is my first blog post created with Hugo"
---

## Introduction

This is my first Hugo blog post.

<!--more-->

This is the content after the summary.

结语

至此,我们已经完成了从Hexo到Hugo的迁移全过程,包括文章转换、多语言设置和部署准备。Hugo强大的性能和灵活的功能将为你的博客带来新的体验。