博客从Hugo迁移到Ghost

博客从Hugo迁移到Ghost
Photo by Andrew Neel / Unsplash

我的博客从2023年开始从WordPress迁移到Hugo,使用Hugo有很多的优点,比如说省钱,只需把博客部署在GitHub上,就能剩下一笔VPS的开支。但是它也有很多的缺点,最让我感觉到不方便的地方在于,我不能随时随地的写博客。

最早的时候,我在我店里的电脑上部署了Hugo,我写博客就只能到店里之后才能进行,当然也可以在别的地方写好,然后在店里的电脑上进行上传,但是这个过程是不连续的。有时候我们做事情讲究一鼓作气,很多时候,因为不连续的原因,写博客的兴致也少了很多。

后来,我们Hugo系统部署在我的VPS上。这样我就可以随时随地的写博客了,但是还有一个问题就是,写博客需要打开终端,连上VPS,步骤太多。因此,我不止一次的想要从Hugo迁移会WordPress。但是从网上搜索的教程全都是从WordPress迁移到Hugo的。大佬们好像是不屑于再回归WordPress。

最近了解到了ghost这个系统,我用docker安装上之后体验了一下,感觉不错。没有WordPress那么臃肿,有后台可以直接写文章。于是就想把博客迁移到Ghost,网上搜索相关教程当然也是无果的。博友建议我寻求AI的帮助。

疫情期间,我也自学过一点点python。于是我就想把Hugo的md文件,转化成ghost导入的json文件。借助ChatGPT,终于把代码给搞了出来。代码如下:

转换代码


import os
import json
import re
import yaml
import markdown
from datetime import datetime

def parse_markdown(filepath):
    with open(filepath, 'r', encoding='utf-8') as f:
        text = f.read()

    match = re.match(r'^---\n(.*?)\n---\n(.*)', text, re.DOTALL)
    if not match:
        print(f"⚠️ 跳过文件:{filepath},没有找到 front matter")
        return None

    front_matter_text, content_part = match.groups()
    try:
        front_matter = yaml.safe_load(front_matter_text)
    except Exception as e:
        print(f"⚠️ YAML 解析错误:{filepath} -> {e}")
        return None

    title = front_matter.get('title')
    raw_date = front_matter.get('date')
    tags = front_matter.get('tags', [])

    if not title or not raw_date or not content_part.strip():
        print(f"⚠️ 数据不完整,跳过文件:{filepath} -> title: {title}, date: {raw_date}, content_length: {len(content_part.strip())}")
        return None

    try:
        if isinstance(raw_date, datetime):
            dt = raw_date
        elif isinstance(raw_date, str):
            try:
                dt = datetime.fromisoformat(raw_date.replace("Z", "+00:00"))
            except ValueError:
                try:
                    dt = datetime.strptime(raw_date, "%Y-%m-%d %H:%M:%S")
                except ValueError:
                    dt = datetime.strptime(raw_date, "%Y-%m-%dT%H:%M:%S%z")
        else:
            print(f"⚠️ 日期格式无法识别,跳过:{filepath} -> date: {raw_date}")
            return None
    except Exception as e:
        print(f"⚠️ 日期解析失败:{filepath} -> {e}")
        return None

    date_str = dt.strftime('%Y-%m-%dT%H:%M:%S.000Z')

    html_content = markdown.markdown(content_part.strip(), extensions=['extra', 'codehilite', 'tables'])

    return {
        'title': title,
        'slug': os.path.splitext(os.path.basename(filepath))[0],
        'html': html_content,
        'created_at': date_str,
        'updated_at': date_str,
        'tags': tags
    }


def walk_md_files(base_dir):
    for root, _, files in os.walk(base_dir):
        for file in files:
            if file.endswith('.md'):
                yield os.path.join(root, file)

def main():
    base_dir = './'
    print(f"开始遍历目录:{base_dir}")
    posts = []
    tags_set = set()

    for filepath in walk_md_files(base_dir):
        print(f"📄 正在处理文件:{filepath}")
        post = parse_markdown(filepath)
        if post:
            post_tags = []
            for tag_name in post['tags']:
                tag_str = str(tag_name).strip()
                if tag_str:
                    tags_set.add(tag_str)
                    post_tags.append(tag_str)

            post['tags'] = post_tags
            posts.append(post)

    ghost_data = {
        "meta": {
            "exported_on": int(datetime.now().timestamp() * 1000),
            "version": "5.0.0"
        },
        "data": {
            "posts": [],
            "tags": [],
            "posts_tags": [],
            "users": [
                {
                    "id": 1,
                    "name": "admin",
                    "slug": "admin",
                    "email": "admin@example.com"
                }
            ]
        }
    }

    for i, post in enumerate(posts, start=1):
        ghost_data["data"]["posts"].append({
            "id": i,
            "title": post["title"],
            "slug": post["slug"],
            "html": post["html"],
            "status": "published",
            "created_at": post["created_at"],
            "updated_at": post["updated_at"],
            "published_at": post["created_at"],
            "author_id": 1
        })

    tag_id_map = {tag: idx + 1 for idx, tag in enumerate(tags_set)}
    for tag, tag_id in tag_id_map.items():
        ghost_data["data"]["tags"].append({
            "id": tag_id,
            "name": tag,
            "slug": tag.lower().replace(" ", "-")
        })

    for post_id, post in enumerate(posts, start=1):
        for tag in post["tags"]:
            ghost_data["data"]["posts_tags"].append({
                "post_id": post_id,
                "tag_id": tag_id_map[tag]
            })

    with open("ghost_import.json", "w", encoding="utf-8") as f:
        json.dump(ghost_data, f, ensure_ascii=False, indent=2)
    print("✅ 已生成 ghost_import.json 文件")

if __name__ == "__main__":
    main()

使用方法:

把这个代码保存成Hugotoghost.py,把这个文件放在content文件夹里。然后输入命令:

python hugotoghost.py

会生成一个ghost_import.json的文件,在ghost的后台把这个文件上传即可。

Read more

热浪记:从乾隆八年吹来的风

热浪记:从乾隆八年吹来的风

郑州的风,已经很久没有凉意了。 中午走出家门,热浪扑面而来,仿佛不是在街头行走,而是在一个被烤熟的城市里艰难穿行。脚下的地砖仿佛在冒烟,树叶也蔫头蔫脑的,天上的云像是被阳光蒸发了似的,连影子都不敢投下来。 我看了看天气预报,未来七天,最高气温在39℃到42℃之间徘徊,最低气温也在29℃以上,甚至有天的低温是33℃。再加上高达60多的湿度。这个城市,像一口沸腾的大锅,白天蒸,夜里焖。 这样的热,让人喘不过气来。更让想起了以前看过的一段历史 那是乾隆八年的夏天——1743年,一样是在中原大地,气温也曾达到过44.4℃。那时没有空调,没有冰箱,连电扇都还没影儿,百姓靠着蒲扇和井水熬过一个个闷热的夜晚。 据史书记载,那年夏天“暑气熏蒸,草木皆焦”、“土石皆焦,桅顶流金”、“铅锡销化”、“墙壁重阴亦炎如火灼”,短短几日间,因酷暑暴毙者过万。乾隆皇帝不得不下令开仓放药,设凉棚、发汤药、赈济百姓。这是中国历史上,

By 老刘
现代八股文

现代八股文

现在的阅读理解题,越来越像一场心理战。 你以为是在考你读没读懂文章,其实是在考你猜不猜得中出题人的心思。女儿每次考完语文都会吐槽:他怎么知道的作者写文章时候的想法?没想到一语成谶,前几天看新闻,上海一场考试中用了一篇青年作家的文章,题目拿去请这位原作者本人来答,结果他第一眼就把“标准答案 C”排除了。作者本人也说,出题人想的有点复杂了。这也就怪不得有经验的出题人都会使用已经故去作者的文章,不会引起争议啊,看来这次的出题人还是有点年轻。 为什么会这样? 因为现在的考试,已经不满足于考“理解”了,它要考“揣摩”。揣摩出题人的意思,揣摩题目的陷阱,更重要的是,揣摩出题人的期待。文章写的是“天边的晚霞”,你若说它“美丽”,太肤浅;说它“象征人生”,略显老套;要想拿满分,必须说它“投射了主人公内心的矛盾与挣扎”,哪怕文章里连个人影都没有。总之,你得拐几个弯,绕几个圈,再从某种“文学母题”或“价值引导”里掏出一个“

By 老刘
白月光:不敢多想的人事

白月光:不敢多想的人事

人这一生,总会有那么几样东西,轻轻地飘在心里,既不落地,也不散去。它们从不喧哗,却在你某个疲惫的午后、某段仓促的旅途、或一场春雨之后,悄然浮现。我们给它起了个好听又带点哀伤的名字——白月光。 说白了,白月光其实是一种遗憾。它是你没能在那个时刻再多靠近一步的那个人,是你童年某一段被记忆滤镜柔化了的光景,是你长大之后每次归乡都觉得“哪里不一样了”的那种熟悉感错位。它不一定高贵,不一定完美,甚至不一定真实。它只是你在现实生活磕磕绊绊之后,为了安顿自己那颗不肯妥协的心,编织出来的一个安静的幻影。 初恋的情人也许早已在朋友圈晒娃,但你记得的是她笑起来像月亮弯弯;童年的伙伴可能连名字都模糊了,但你记得那棵老槐树下的影子,总是比黄昏先到一步;而故乡,像一幅始终未完成的水墨画,留白处最多,也最撩人心绪。 夏丐尊在《春的欢悦与感伤》中写道: “前几天吃到油菜心和马兰头的时候,我不禁起了怀乡之念,想起故乡的春日的光景来。我所想的只是故乡的自然界,园中菜花已发黄金色了吧,燕子已回来了吧,窗前的老梅花已结子如豆了吧,杜鹃已红遍了屋后的山上了吧……只想着这些,怕去想到人事。因为乡村的凋敝我是知道的,故乡

By 老刘
十五年后:一个家庭的沉沦与命运的回音

十五年后:一个家庭的沉沦与命运的回音

十五年前,我写下《郁闷,这个世界还有这样无耻之人》这篇文章时,内心翻滚的不是小事的愤怒,而是对底线被践踏的失望。邻居从我家拉电线充电,还一副理所当然的样子,最后竟连我家的充电器都“借用”。甚至因为我给他写了纸条,还想打我。当时我说:“这个世界还有这样无耻之人”,只是没想到,十五年后再听到他们家的消息,竟是一出更荒诞的悲剧。 那家的男人已经去世。留下两个老婆争房产,最后还是他儿子获得了这套回迁房。他的儿子大学毕业了,却像一艘没有舵的船,一路磕磕绊绊: 交房一年来,看他儿子在业主群里抱怨,被电梯夹了不止一次,骑车进地下室摔过多回,车轮卡在道缝中,正常走路还能摔个仰八叉。毕业后没有工作,前不久小区换物业的时候,经介绍在小区做监控员,不服从管理员的管理(其实也是小事,人家让他把桌子上保持整洁,他就是不听,弄的乱七八糟的),和物业经理骂了一架,被辞退。邻里原想通融一二,以他母亲那无理取闹的性格,当然是又去物业把物业经理骂了一顿,结果也就那样了。 前几天偶尔和一个邻居聊起他家的情况,那个邻居说,他要打你那不是基本操作嘛,人早年曾在派出所拿刀扎他自己母亲,打你都是轻的😅。如今看来,

By 老刘