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'![\1](/images/\3)', 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.