博客从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

Ghost 博客与 Mailgun 邮件服务申请全流程

Ghost 博客与 Mailgun 邮件服务申请全流程

Ghost 的邮件系统与 Mailgun 深度集成,如果想要在 Ghost 上实现邮件订阅、密码找回、通知等功能,申请一个 Mailgun 账户几乎是必选项。好消息是——Mailgun 已经恢复免费套餐,每天可发送 100 封邮件,对个人博客来说完全够用,而且现在验证信用卡不再是必需。坏消息是——Mailgun 的风控依然非常严格,尤其是中国大陆用户,注册和解封过程可能会比较“曲折”。下面是我亲身踩坑并最终申请成功的全过程,希望能帮到你。 1. 中国大陆能正常访问,但注册有坑 Mailgun 官网在中国大陆是可以直接打开的,但注册过程中会遇到两个大坑: 1. 不挂梯子 → 验证码加载不出来注册页的验证根本就不显示,导致注册按钮点不动。 2. 挂了梯子 → 手机号与 IP 不匹配触发风控注册时需要验证手机号,如果你的 IP 地址(梯子出口)与手机号所在地区不一致,就很容易被判定为风险账号,触发临时封禁。

By laoliu
说话,是世界上最轻易的事,也是最难的事

说话,是世界上最轻易的事,也是最难的事

昨天继续研习南怀瑾先生的《易经杂说》,看到《易经·系辞上传》的一段话: 子曰:君子居其室,出其言善,则千里之外应之,况其迩者乎? 居其室,出其言不善,则千里之外违之,况其迩者乎? 言出乎身,加乎民;行发乎迩,见乎远; 言行,君子之枢机。枢机之发,荣辱之主也。 言行,君子之所以动天地也,可不慎乎? 这段话放在两千多年前,已经算是对信息传播极限的高度描写了——在一个没有网络、没有印刷术的时代,孔子就已经意识到:话是有传播力的,而且可以影响到你看不见的人。 他用“枢机”来比喻言行的重要性。枢机是门轴,平时不起眼,但门的开合全在它一动之间。言行之于一个人,也是这样——不经意的一句话,可能成就一生的名誉,也可能带来一世的羞辱。 佛家讲“不妄语”,涵义更广:不仅不能撒谎,还要避免虚夸、恶口、

By laoliu
Ubuntu部署ghost博客所踩的小坑

Ubuntu部署ghost博客所踩的小坑

Ghost 6.0 发布后,我就被它宣传的“联邦宇宙(Activitypub)”和“内置 Web 分析”功能狠狠吸引了。可惜一直用的 1Panel 面板并没有 Ghost 的升级选项,考虑到 Ghost 6 改动幅度较大,盲猜1Panel里的Docker需要重建,再加上想要尽快体验ghost的联邦宇宙等功能,所以打算用官方推荐的方式“一劳永逸”。 换系统,换方式 我挑了个快要过期的 VPS 练手,把系统换成了 Ubuntu 24.04。结果部署异常顺利,Ghost 6.0 安装一气呵成。于是果断把主 VPS 也重装,迁移博客数据,正式切换新架构。 Ghost 官方推荐部署环境如下: * ✅ Ubuntu 22.04 或

By laoliu
佛门变与不变:从释永信案说起

佛门变与不变:从释永信案说起

最近网上又热闹了,释永信的事成了热门话题。7月27日晚,少林寺发布通报,说他涉嫌刑事犯罪,被有关部门带走调查。挪用、侵占寺产、私生活混乱——这些原本只在坊间流传的质疑,如今一个个被坐实。紧接着,中国佛教协会也迅速表态,注销了他的戒牒,意思是:他不再是出家人了。 这事儿引发了很多讨论,有人骂佛教变质,有人说和尚怎么都这样。但我觉得,问题不在佛教,而在人。宗教本无错,错的是人心。古人早就说了,“道高一尺,魔高一丈”,这世上真正能守住本心的,少。 释永信掌管少林几十年,把一个千年古刹搞成了商业帝国,景区票、武僧团、文创、演出、电商、甚至医疗、地产,一个都没落下。少林寺一年进账上亿,方丈出访,排场比国宾都阔气。这哪里像是修行?更像是一场包装精良的文化资本秀。说到底,是被这个时代的名利场裹挟了。 说释永信是个极端个案,其实也不尽然。从历史到文学,从《

By laoliu