How to Completely Migrate Gridea to Hugo (Including Pitfalls and Fixes)
I recently migrated my blog from Gridea to Hugo. It might seem like a simple “static blog framework switch”, but there are quite a few pitfalls.
This post documents the entire process in full, especially those issues you only discover after migration.
1. Migration Goals
From this source directory:
~/GRIDEA_SITES/margrop.gridea
To Hugo project:
~/margrop-blog
Standards to achieve:
- Preserve all posts, images, tags, and slugs.
- Keep historical URLs unchanged as much as possible (or set up compatible redirects).
- Ensure continuous operation of comments (Gitalk) and analytics (GA / 51.la).
- Automated deployment process that works through proxy networks.
2. Content Migration (Posts and Front Matter)
Initially used a script to batch convert Gridea’s posts/*.md to Hugo posts (the approach was correct, fields just needed to be filled in later):
python3 /root/convert_posts.py
Core actions:
- Read Gridea front matter:
title/date/tags/published/feature. - Skip unpublished posts (
published: false). - Write Hugo front matter.
- Normalize
<!-- more -->to Hugo’s common format<!--more-->.
Recommended additions (lessons from practice):
- Always explicitly write
slugto avoid URL drift. - Pay attention to timezone and future dates for
date, otherwise posts won’t be published normally by Hugo. - Keep original tag values, but establish slug mappings to maintain compatibility with historical links.
3. Hugo Routing and Base Configuration
Lock down routing rules early during migration, otherwise fixing them later is costly.
hugo.toml key items:
[permalinks]
post = '/post/:slug/'
tags = '/en/tag/:slug/'
[taxonomies]
tag = "tags"
category = "categories"
This step determines the final URLs for posts and tags.
4. Static Assets and Historical Link Compatibility
These two types of assets are most easily lost after migration:
post-images(inline images)- Historical tag short ID links
Handling approach:
- Completely migrate image static directory to
static/. - Keep tag mapping files (e.g.
static/tag-slug-map.js,static/slug-to-name-map.js). - Generate redirect pages for old short links:
/tag/<old-id>/ -> /tag/<new-slug>/.
This ensures historical external links, search engine index, and old WeChat links all remain accessible.
5. Comment System Migration (Gitalk)
This is the part most likely to “look like it has a comment box, but actually nothing works”.
1) Configuration Migration
Move Gitalk parameters from Gridea to Hugo data files, for example:
{
"enabled": true,
"clientID": "...",
"clientSecret": "...",
"repo": "margropcomment.margrop.io",
"owner": "margrop",
"admin": ["margrop"]
}
2) Two Key Pitfalls
Pitfall one: admin gets rendered as a string instead of an array.
Pitfall two: id generation rules changed, causing historical issues to not match.
Old site rule was:
id: (location.pathname).substring(0, 49)
If the new site changes to hash (like sha1(RelPermalink)), historical comments will “disappear”.
Final fix strategy:
adminmust output as a real JS array.idmust remain consistent with the old site (path truncated to 49).- In mainland China network environment, Gitalk resources can prioritize
jsdelivr.
6. Analytics Script Migration (GA + 51.la)
After posts are migrated and pages are working, the most commonly missed part is the “initialization segment” of analytics scripts.
This time I actually added three segments:
js.users.51.la/...LA_COLLECT + LA.init(...)LingQue.Monitor().init(...)
Only migrating the first script tag isn’t enough - missing initialization will result in no data in the dashboard.
7. Unified Page Header (Navigation Consistency)
After migration, homepage and post page headers are often inconsistent, usually because multiple templates each have their own navigation implementation.
Fix approach:
- Extract common partials, for example
partials/header-nav.html. - Homepage, list page, post page, and tag page all reference the same partial.
- Unify styles to avoid future divergence.
8. Deployment and Proxy Networks
Deployment script uses:
bash scripts/deploy_github_pages.sh
Configuration from:
config/github-pages.json
And reads from gridea/setting.json for token / email (compatible with old workflow).
Practical pitfall: If network access to GitHub must go through a proxy, the deployment script needs to automatically read proxy configuration and export http_proxy/https_proxy, otherwise git clone / git push will hang.
9. Post-Migration Verification Checklist (Strongly Recommend Executing Each Item)
# 1) 本地构建
hugo --cleanDestinationDir
# 2) 线上可用性
curl -I https://blog.margrop.net
curl -I https://blog.margrop.net/en/post/hello-world/
curl -I https://blog.margrop.net/en/tag/github/
# 3) 页面源码核对(Gitalk/统计)
curl -sL https://blog.margrop.net/en/post/hello-world/ | rg "gitalk|LA_COLLECT|LingQue|gtag"
Supplementary notes:
- GitHub Pages / CDN have caching - seeing old pages immediately after deployment is common.
?v=timestampcan help verify if the new version has been served.- If a post URL returns 404, first check if
dateis written as a future date.
10. Conclusion
Gridea -> Hugo “content migration” isn’t difficult. What’s truly hard is “behavior migration”:
- URL behavior (slug, tag, old short links)
- Comment behavior (Gitalk id rules)
- Analytics behavior (scripts + initialization)
- Deployment behavior (proxy, automation, cache verification)
Only when these four areas are completed can you call it a true “complete migration”.