A couple of days ago, I migrated my blog posts from Typecho to Hugo. Just setting up the Front Matter parameters and reconfiguring image links took considerable effort.
The value of a blog lies first in its articles, followed closely by its comments. Comments are proof that the blog has made an impact in both the digital and real worlds, carrying the interactions between people. More personally, comments from all corners of the globe are precious memories and a part of what makes “me”.
Thus, it’s essential to copy the original comments to the corresponding articles on the new site.
Configuring Waline
Unlike dynamic blogs like WordPress or Typecho, static blogs can only rely on external comment systems. There are many options, each with its pros and cons. After referring to this article and checking the official websites of various comment systems, I ultimately chose Waline.
Waline’s Chinese documentation is detailed and comprehensive. After setting up the LeanCloud database and Vercel server, you can access the comment management dashboard at https://<your-server-domain>/ui/. Register as an administrator for the first time, where you can manage comments and users.
Exporting Typecho Comments
Typecho is quite old, with a smaller user base compared to more active communities like Hexo or WordPress. There’s also very little documentation available online.
The only solution I found was a plugin called Export2Valine (also mentioned in Waline’s documentation) by Yi Hong Yuan Luo, which exports Typecho comments to Valine.
However, it hasn’t been updated in three years, and testing showed it only imports the first comment. Looking at the exported jsonl file, it’s clear that all comment data was fully exported.
First, install the plugin in Typecho (make sure to rename the plugin folder to “Export2Valine”!).
Referencing this article about migrating from Typecho to Hexo, the plugin is outdated and requires some modifications.
Locate Action.php in the plugin folder and modify lines 42 onwards as follows (to track parent comments):
$arr = array(
"objectId" => $comment["coid"],
"QQAvatar" => "",
"comment" => $comment["text"],
"insertedAt" => array(
"__type" => "Date",
"iso" => $time
),
"createdAt" => $time,
"updatedAt" => $time,
"ip" => $comment["ip"],
"link" => $comment["url"],
"mail" => $comment["mail"],
"nick" => $comment["author"],
"ua" => $comment["agent"],
"url" => "/{$slug}.html"
);
if($comment["parent"]) {
$arr["pid"] = $comment["parent"];
$arr["rid"] = $this->getRootId($comment["coid"]);
}
No other changes are needed.
Next, go to Typecho’s admin panel → Console → Export Comments. Open the downloaded jsonl file and delete the header line:#filetype:JSON-streaming {"type":"Class","class":"Comment"}\n\n.
Save the file, close it, and change the file extension to .json.
Fixing the JSON Format
The exported jsonl file contains escaped Chinese characters and is a single line, making it hard to read.
To convert it into a more readable, editable, and importable json format, use your editor’s Find and Replace feature to replace }\n{ with:
},
{
In Xcode, you can insert line breaks by clicking the small magnifying glass icon on the left.
Now, each line represents one comment object.
Similarly, to separate the fields within each comment object, replace "," with:
",
"
Now, each comment object contains multiple data fields, structured like this:
{"objectId":"3",
"QQAvatar":"",
"comment":"\u6d4b\u8bd5\u4e00\u4e0b",
"insertedAt":{"__type":"Date",
"iso":"2023-06-27T09:37:07.000Z"},"createdAt":"2023-06-27T09:37:07.000Z",
"updatedAt":"2023-06-27T09:37:07.000Z",
"ip":"223.104.150.16",
"link":null,"mail":"2868301418@qq.com",
"nick":"2868301418",
"ua":"Mozilla\/5.0 (Linux; Android 13; V2171A Build\/TP1A.220624.014; wv) AppleWebKit\/537.36 (KHTML, like Gecko) Version\/4.0 Chrome\/109.0.5414.86 MQQBrowser\/6.2 TBS\/046605 Mobile Safari\/537.36 V1_AND_SQ_8.9.63_4190_HDBM_T QQ\/8.9.63.11380 NetType\/4G WebP\/0.3.0 Ap",
"url":"\/\u4ea4\u53cb\u6807\u51c6-\u548c\u5e73\u5171\u5904\u4e94\u9879\u539f\u5219.html"},
{"objectId":"4",
"QQAvatar":"",
"comment":"\u600e\u4e48ip\u4e0d\u5bf9",
"insertedAt":{"__type":"Date",
"iso":"2023-06-27T09:38:15.000Z"},"createdAt":"2023-06-27T09:38:15.000Z",
"updatedAt":"2023-06-27T09:38:15.000Z",
"ip":"223.104.150.16",
"link":null,"mail":"2868301418@qq.com",
"nick":"2868301418",
"ua":"Mozilla\/5.0 (Linux; Android 13; V2171A Build\/TP1A.220624.014; wv) AppleWebKit\/537.36 (KHTML, like Gecko) Version\/4.0 Chrome\/109.0.5414.86 MQQBrowser\/6.2 TBS\/046605 Mobile Safari\/537.36 V1_AND_SQ_8.9.63_4190_HDBM_T QQ\/8.9.63.11380 NetType\/4G WebP\/0.3.0 Ap",
"url":"\/\u4ea4\u53cb\u6807\u51c6-\u548c\u5e73\u5171\u5904\u4e94\u9879\u539f\u5219.html"},
Common Field Descriptions
- objectId: Unique identifier for the comment (e.g., “4” or “5”)
- QQAvatar: QQ avatar link (currently empty)
- comment: Comment content (contains Unicode escape sequences, e.g.,
\u600e\u4e48means “how”) - insertedAt/createdAt/updatedAt: Timestamp (ISO 8601 format)
- ip: Commenter’s IP address
- link: Link provided by the commenter (may be
null) - mail: Commenter’s email address
- nick: Commenter’s nickname
- ua: User agent (browser/device info)
- url: Relative path of the commented post
Special Fields
- pid: Parent comment ID
- rid: Root comment ID
If "link" is null, there’s no line break between "link" and "mail". JSON is insensitive to line breaks, so this can be ignored.
Now, wrap the entire content in [ ] at the beginning and end of the file, then save it.
Modifying Comment Attributes
The file can now be imported into LeanCloud, but some adjustments are still needed.
Export2Valine sets the URL for comment associations as \/slug, e.g., "url": "\/Summary-of-the-First-Semester-of-Junior-Year.html", where \/ is an escaped /.
To link comments to the new blog’s posts, manually update the url to match the new blog’s post links.
For example, my Hugo-generated site has folders like zh-cn, zh-tw, en, and ja (due to multi-language support). Chinese posts are under /zh-cn/post/category/.
In my local blog source files, posts are organized into folders by category, e.g., /content/post/Thoughts/最近写的诗.md generates a relative URL like zh-cn/post/thoughts/最近写的诗.
If your new blog’s posts are in the root directory with unchanged names, no URL modifications are needed.
If they’re all under /post/, use Find and Replace to change:
"url":"\/
to:
"url":"\/post\/
For my case, I temporarily replaced it with:
"url":"\/zh-cn\/post\/
Similarly, comments on standalone pages like “Friends” or “Thoughts” should be updated to their new relative URLs.
For example, the Friends page:
"url":"\/links.html
should be replaced with:
"url":"\/zh-cn\/friend\/
First, apply bulk replacements for post and standalone pages where possible. Otherwise, it’ll be tedious to modify them after import.
When using Find and Replace, try to target the largest common segments to avoid accidental changes.
Remember to escape \/!!!
Importing to LeanCloud
In LeanCloud’s console → Data Storage → Import & Export, select the modified JSON file, set Class to Comment, and import.
Note: If you’ve previously tested Waline comments or attempted to import Comment, Waline may have already created the Comment class. Subsequent imports will fail silently (LeanCloud may claim success, but no new data appears).
To fix this, go to the console → Structured Data, delete the Comment class, and try importing again. The LeanCloud page may not refresh immediately—use Ctrl+F5 to force a cache refresh.
After a successful import, manually adjust the url for each comment.
For example, my posts need to be categorized under "url":"\/zh-cn\/post\/category\/. Use LeanCloud’s batch operations and filtering features to streamline this process.
Afterword
Organizing comments didn’t take too long—120 comments, mostly my own musings on the “Thoughts” page, allowed for bulk URL fixes. The handful of reader comments were scattered across just a few posts, making them easy to update via filtering. Whether that’s a good or bad thing, I’m not sure (laughs).
Whether they’re my soliloquies or others’ remarks, each comment holds unique significance. Revisiting them periodically brings new reflections.
As I said at the beginning, they’re traces of my growth, proof of my existence, and part of “me”.
And you, dear reader, are the one who gives me value.
If you have time, please leave a comment—it’ll genuinely make my day (as long as it’s kind, of course).

When will I have a drink and discuss the details again?