Introduction
Hexo and Hugo are both excellent static site generators, but due to performance considerations or personal preference, you might want to migrate from Hexo to Hugo. This article will document the complete migration process in detail, including configuration, article conversion, multilingual support, and more.
Preparation
Before starting the migration, you need to install Hugo and understand the basic site creation process. Here are the official documentation links, which are recommended to follow:
1. Install Hugo
Hugo provides multiple installation methods, including pre-compiled binaries and package managers. Based on your operating system, please refer to the official installation documentation:
2. Create a Hugo Site
After installing Hugo, you can create a new Hugo site using the following command:
hugo new site your-site-name
cd your-site-name
For more detailed information about creating a site, please refer to:
3. Install a Theme
Hugo has a rich selection of themes to choose from. You can browse and select a suitable theme on the Hugo Themes page.
The basic steps to install a theme are as follows:
git init
git submodule add https://github.com/theme-author/theme-name.git themes/theme-name
Then set the theme in your configuration file:
echo "theme = 'theme-name'" >> hugo.toml
Site Configuration
Create a hugo.toml configuration file:
baseURL = 'https://www.mfun.ink/'
defaultContentLanguage = 'zh-cn'
title = "Mengboy's Blog"
theme = 'PaperMod'
hasCJKLanguage = true
# Google Analytics configuration
GoogleAnalytics = "G-xxxxxxx" # Please replace with your Google Analytics ID
# Add permalink configuration to make link format compatible with Hexo
[permalinks]
post = '/:year/:month/:day/:contentbasename/'
# PaperMod theme parameters
[params]
# Set environment to production, which is important for Google Analytics and AdSense loading
env = "production"
# Author information
author = "mengboy"
# Main sections
mainSections = ['post']
defaultTheme = "auto"
ShowReadingTime = true
ShowShareButtons = true
ShowPostNavLinks = true
ShowBreadCrumbs = true
ShowCodeCopyButtons = true
ShowRssButtonInSectionTermList = true
disableSpecial1stPost = false
disableScrollToTop = false
hideMeta = false
hideFooter = false
# Website favicon settings
[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 configuration, requires custom extension or theme support
googleAdsense = "ca-pub-xxxxxxxxxx" # Please replace with your Google AdSense publisher ID
googleAdsenseSlot = "xxxxxxxx" # Please replace with your Google AdSense ad unit ID
# Sidebar configuration
[params.profileMode]
enabled = false
title = "mengboy"
subtitle = "Recording Learning and Life"
imageUrl = "images/site/favicon.png"
imageWidth = 120
imageHeight = 120
# Social icons
[[params.socialIcons]]
name = "github"
url = "https://github.com/mengboy"
# Search settings
[params.fuseOpts]
isCaseSensitive = false
shouldSort = true
location = 0
distance = 1000
threshold = 0.4
minMatchCharLength = 0
keys = ["title", "permalink", "summary", "content"]
# Allow HTML
[markup.goldmark.renderer]
unsafe = true
# Ignore HTML warnings
ignoreWarnings = ['raw-html']
ignoreLogs = ['warning-goldmark-raw-html']
[languages]
[languages.zh-cn]
contentDir = 'content'
languageName = '简体中文'
weight = 10
title = "Mengboy's Blog"
# Chinese homepage information
[languages.zh-cn.params]
[languages.zh-cn.params.homeInfoParams]
Title = "记录学习与生活点滴"
# Chinese menu
[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"
# English homepage information
[languages.en.params]
[languages.en.params.homeInfoParams]
Title = "Recording Learning and Life"
# English menu
[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
Create Content Directories
mkdir -p content/post
mkdir -p content/english/post # Directory for English articles
Create Basic Pages
About Page
mkdir -p content/about
Create Chinese about page content/about/index.md:
---
title: "关于我"
date: 2023-06-01T12:00:00+08:00
---
这是关于我的页面内容...
Create English about page content/english/about/index.md:
---
title: "About Me"
date: 2023-06-01T12:00:00+08:00
---
This is about me page...
You can create other basic pages using the about page as a reference.
Write the Conversion Script
Create a Python script to convert Hexo articles to Hugo format:
#!/usr/bin/env python3
# Convert Hexo Markdown files to Hugo format
import os
import re
import datetime
import shutil
from pathlib import Path
# Setup paths
hexo_posts_dir = "hexo/source/_posts"
hugo_posts_dir = "hugo/content/post"
hexo_img_dir = "hexo/source/images"
hugo_static_dir = "hugo/static/images"
# Ensure target directories exist
os.makedirs(hugo_posts_dir, exist_ok=True)
os.makedirs(hugo_static_dir, exist_ok=True)
# Copy image folder
def copy_images():
if os.path.exists(hexo_img_dir):
print(f"Copying image files from {hexo_img_dir} to {hugo_static_dir}")
# Copy folder contents
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)
# Process YAML front-matter
def convert_front_matter(content):
# Extract 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():]
# Convert tags and categories
new_front_matter = []
categories = []
tags = []
date = None
for line in front_matter.strip().split('\n'):
line = line.strip()
# Process date
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")}')
# Add lastmod
new_front_matter.append(f'lastmod: {date.strftime("%Y-%m-%dT%H:%M:%S+08:00")}')
except ValueError:
new_front_matter.append(line)
# Process tags
elif line.startswith('tags:'):
tags_str = line[5:].strip()
if tags_str.startswith('[') and tags_str.endswith(']'):
# Format is tags: [tag1, tag2]
tags = [tag.strip(' "\'') for tag in tags_str[1:-1].split(',')]
else:
# Might be multi-line format
continue
# Process categories
elif line.startswith('categories:'):
cats_str = line[11:].strip()
if cats_str.startswith('[') and cats_str.endswith(']'):
# Format is categories: [cat1, cat2]
categories = [cat.strip(' "\'') for cat in cats_str[1:-1].split(',')]
else:
# Might be multi-line format
continue
# Copy other lines
elif not (line.startswith('- ') and ('tags:' in front_matter or 'categories:' in front_matter)):
new_front_matter.append(line)
# Add categories and tags
if categories:
new_front_matter.append(f'categories: {categories}')
if tags:
new_front_matter.append(f'tags: {tags}')
# Add slug
if 'slug:' not in front_matter:
filename = os.path.splitext(os.path.basename(content))[0]
new_front_matter.append(f'slug: "{filename}"')
# Combine new front-matter
new_content = '---\n' + '\n'.join(new_front_matter) + '\n---\n\n' + rest_content
return new_content
# Convert Markdown content
def convert_markdown_content(content):
# Replace image links
content = re.sub(r'!\[(.*?)\]\((.*?)/images/(.*?)\)', r'', content)
# Handle the "more" marker
content = re.sub(r'<!--\s*more\s*-->', '
', content)
return content
# Main conversion function
def convert_posts():
print(f"Starting to convert posts from {hexo_posts_dir} to {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"Converting: {source_file} -> {target_file}")
# Read source file
with open(source_file, 'r', encoding='utf-8') as f:
content = f.read()
# Convert content
content = convert_front_matter(content)
content = convert_markdown_content(content)
# Write to target file
with open(target_file, 'w', encoding='utf-8') as f:
f.write(content)
# Detect language and create multilingual files
def handle_multilingual():
print("Processing multilingual articles...")
for file in os.listdir(hugo_posts_dir):
if not file.endswith('.md'):
continue
filepath = os.path.join(hugo_posts_dir, file)
# Read file content
with open(filepath, 'r', encoding='utf-8') as f:
content = f.read()
# Check if there's a language marker
lang_match = re.search(r'language:\s*[\'"]?([a-z\-]+)[\'"]?', content)
if lang_match:
lang = lang_match.group(1)
if lang.startswith('en'):
# Create English version
en_file = os.path.splitext(file)[0] + '.en.md'
en_filepath = os.path.join(hugo_posts_dir, en_file)
# Remove language marker and write to new file
content = re.sub(r'language:\s*[\'"]?[a-z\-]+[\'"]?\n', '', content)
with open(en_filepath, 'w', encoding='utf-8') as f:
f.write(content)
# Delete original file
os.remove(filepath)
print(f" Created English article: {en_filepath}")
if __name__ == "__main__":
copy_images()
convert_posts()
handle_multilingual()
print("Conversion complete!")
Run the Conversion Script
cd ~/dev/book
python3 convert_hexo_to_hugo.py
Create a Start Script
Create start.sh:
#!/bin/bash
# Start Hugo server
hugo server -D
Add execution permission:
chmod +x start.sh
Create a Deployment Script
Create deploy.sh:
#!/bin/bash
echo -e "\033[0;32mDeploying blog to GitHub Pages...\033[0m"
# Generate static files
hugo
# Go to public directory
cd public
# Initialize git repository
if [ ! -d ".git" ]; then
git init
git remote add origin YOUR_GITHUB_REPOSITORY_URL # Replace with your GitHub repository URL
fi
# Add files
git add .
# Commit changes
msg="rebuilding site $(date)"
if [ $# -eq 1 ]; then
msg="$1"
fi
git commit -m "$msg"
# Push to GitHub
git push -u origin master
# Return to parent directory
cd ..
Add execution permission:
chmod +x deploy.sh
Test Run
./start.sh
Create Multilingual Article Examples
Chinese Article
hugo new post/my-first-post.md
Edit 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-->
这是摘要后的内容。
English Article
hugo new post/english/my-first-post.md
Edit 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.
Conclusion
At this point, we have completed the entire migration process from Hexo to Hugo, including article conversion, multilingual settings, and deployment preparation. Hugo’s powerful performance and flexible features will bring a new experience to your blog.