cyilin / 博客迁移记录

Created Fri, 26 Sep 2025 22:23:17 +0800 Modified Sun, 14 Dec 2025 23:57:20 +0800

前言

最近花了将博客从notionnext转移到hugo

转移的理由有以下:

  1. notionnext需要在notion上写作,但众所周知,由于GFW的存在,写作体验不能说良好,不如本地
  2. notionnext是依靠notion的api获取数据库的内容再进行渲染,而之前就因为接口更改、个人没能及时更新仓库的原因导致图片无法加载出来,一度以为个人是notion上的图片都炸了。
  3. hugo有这非常活跃的社区,且主题非常多,涵盖面非常更广;notionnext虽然也有非常多的主题,但还是比不上hugo的社区丰富。
  4. 自用的obsidian和notion有一小部分的语法冲突。
  5. 纯粹就是闲到蛋疼。

综上所述,为了摆脱对我而言不怎么便利notion写作和较为丰富的社区主题,便是我从notionnext迁移到hugo的原因。

那么接下来讲述的目标就很清晰了:

  • 部署hugo博客
  • 将文章从notionnext转移到hugo

部署hugo博客

我们分以下三步来进行hugo博客的部署:

  1. 理解hugo的工作原理。
  2. 安装hugo。
  3. 将博客运转在服务器上。

hugo的本质

想要真正部署好hugo,首先需要知道hugo到底是什么。

hugo和notionnext一样,本质上都是一个静态网页生成器。hugo一般是在自己电脑上运行生成网页文件;而notionnext则是已经写好程序和网页,可以在云端进行实时的信息获取、生成网页内容并运行。

因此hugo需要在本地生成静态网页文件后,还需要将这些放在一个web服务器上才能将博客搭建起来,无论什么修改都需要进行生成“网页文件-将文件推送到web服务器这么一个流程”;notionnext则不需要这么麻烦,只需要写文章笔记即可,上述流程会由服务器自动完成。

hugo部署

hugo比起notionnext要“干净”,是不含主题文件的,需要额外下载。

hugo本体

hugo比起notionnext要“干净”,本体只是一个由命令行操控的静态网页生成器。

来到hugo的github,选择一个符合本地系统的包下载。

注意,一定要是本地系统,不是服务器。如果是Windows就下载Windows,Linux就下载Linux。除非搭建、写好博客、生成网页这几个环节不出错且不需要预览,否则务必请下载适合本地系统运行的程序,不然会出现“顺利生成预览网站但就是进不去,网站无法连接”的情况。我在这里撞墙了好久才弄明白,hugo生成的预览静态网站,默认使用的域名是localhost。因此必须要使用本地系统来运行hugo。这就是我碰到的第一个大坑,害我白白浪费一小时。

进入到hugo程序所在的文件,生成一个新的站点:

hugo new site blog

hugo程序所在的文件夹会就地生成一个blog文件夹,也就是hugo项目的根目录。将hugo的程序复制一份到blog文件夹中,后续的操作全部都在blog文件夹中。

hugo主题

我们可以来到Hugo Themes中下载一个喜欢的主题。我目前使用的是Github Style风格。

因此就用这个主题作为演示,想要部署其他主题的用户可以结合作者教程去部署。

到 blog/themes 中下载主题文件。

git clone https://github.com/MeiK2333/github-style.git

编辑hugo配置

基本每个hugo主题都会自带一个预设hugo.toml,可以根据自身需求去配置hug。Github Style则是themes/github-style/config.template.toml。将其复制到项目根目录并重命名为hugo.toml。

个人使用快速启动配置:

baseURL = "your blog domain" # 博客地址,填入后续搭建真实的地址
languageCode = "zh-cn"
title = "your blog name" # 博客名称
theme = "github-style" # 博客选择,默认不动
pygmentsCodeFences = true # 代码区域高亮 
pygmentsUseClasses = false # true:使用css来配置代码高亮;false:使用内联样式配置代码高亮。需要 pygmentsCodeFences 为 true

[params] # 博客主页个人介绍以及个性化配置
  author = "author" # 作者
  description = "personal description" # 个人描述
  github = "your github" # 个人github账户名
  twitter = "your twitter" # 个人推特账户名
  email = "your email" # 个人邮箱
  facebook = "your facebook" # 个人脸书
  linkedin = "your linkedin" # 个人领英
  instagram = "your instagram" # 个人instagram
  tumblr = "your tumblr" # 个人tumblr主页
  url = "domain" # 个人主页,建议放入 baseURL 相同的链接
  keywords = "blog, google analytics" # SEO搜索引擎关键词,可注释
  rss = true # rss订阅
  lastmod = true # 处理文章生成时间
  favicon = "/images/github-mark.png" # 网站图标,即浏览器
  avatar = "/images/avatar.png" # 个人头像
  headerIcon = "/images/github-mark-white.png" # 网站抬头
  location = "China" # 地区
  userStatusEmoji = "🤣" # 头像旁边的emoji
  enableGitalk = false # gitalk评论功能关闭
  enableSearch = true # 搜索功能启用
  
[outputs] # 本地搜索相关配置
  home = ["html", "json"]
  
[outputFormats.json] # 本地搜索相关配置
  mediaType = "application/json"
  baseName = "index"
  isPlainText = false

[params.gitalk] # gitalk相关配置,需要将 enableGitalk 设置为 true
  clientID = "client ID" # Your client ID
  clientSecret = "client secret" # Your client secret
  repo = "Repository name" # 博客的github地址Repository name,即仓库名。用于生成作用于评论的issue
  owner = "your github" # 您的GitHub ID
  admin = "your github" # 您的GitHub ID
  id = "location.pathname" # 文章页面的链接地址就是ID
  labels = "gitalk" # Github issue labels. 
  perPage = 100 # 可评论数量,最大为100
  pagerDirection = "last" # 时间倒序来排列评论
  createIssueManually = true # 设置为true,如果是管理员登录,会自动创建issue,如果是false,需要管理员手动添加第一个评论(issue)
  distractionFreeMode = false # 如果启用,则输入 (cmd|ctrl + enter) 可以快速回复
  proxy = "https://cors-anywhere.azm.workers.dev/https://github.com/login/oauth/access_token" # 为不方便登录github的读者通过反向代理,可自行搭建

[frontmatter]
  lastmod = ["lastmod", ":fileModTime", ":default"] # 文章时间处理配置

[services]
  [services.googleAnalytics]
    ID = "UA-123456-789" # googleAnalytics id

faviconavatarheaderIcon需要放在themes/github-style/static/images中,并在hugo.toml改成相对应的文件名。需要注意,图像需要为像素为100整数倍的正方形图像,例如300×300、700×700。

写完hugo.toml后,在项目根目录进入cmd,然后输入hugo server -D,浏览器访问http://localhost:1313便可进入到hugo生成的预览网站,针对hugo做出的任何修改都可以得到实时的反馈。

到content文件夹中新建一个readme.md,这边便是个人详细介绍,可以随意填写,比如写上自己的各种主页

### Links

- bilibili:
- github:
- email:

hugo博客发布

前文也已经写了,hugo本质上是一个静态网页生成器,我们需要将它生成的静态网站文件发布到类似nginx之类的web服务器或者诸如github page之类的网站托管平台上。个人还是推荐优先选择github page,对于日后回档、备份等非常方便,它已经救了我好几次了。

在项目根目录进入cmd,输入hugo,便可以在public文件夹中找到编译好的网站文件。直接全部上传到web服务器或者网页托管平台即可成发布。

如果发布后出现css丢失而本地预览时一切正常,即成功发布的网站上只有文字没有样式,大概率是hugo.toml中的baseURL写错了,需要写上博客真实的地址。修改完成后再重新编译、上传发布。


博客文章的编写

编写之前需要进行文章的预设进行修改。可以根据themes/archetypes/default.md修改archetypes/default.md里面的内容。

由于个人使用的是obsidian进行markdown的编写,使用yaml语法写的预设放在obsidian正好就是笔记属性模块,因此我更喜欢写成这样:

---
date: {{ .Date }}
draft: true
title: {{ replace .File.ContentBaseName "-" " " | title }}
summary:
tags:
---

drafttrue是,文章处于草稿状态,不会在网站中显示。

在项目根目录进入cmd,输入

hugo new post/<文件名称>.md

在content/post便会生成一个带着预设的markdown文件,需要在这个文件上进行博客文章的撰写。使用hugo生成静态网站文件前也需要将博客文章放入到该文件夹中。

图片附件的引用

图片附件的文件存放

如果跟我一样是使用obsidian进行markdown编辑,那么需要进行一番设置,不然就会跟我一样掉入到第二个大坑

obsidian对于图片等附件,默认的做法是直接塞到仓库的根目录下;而hugo则需要将引用的图片放到项目的 static/images 目录下,这与Obsidian的默认存放方式不同,直接迁移会不方便。

而且obsidian对图片应用默认使用它所谓的WIKI链接,即![[图片名]]的格式,而不是markdown中标准的url ![]() 格式,而hugo则不认WIKI链接格式。

综上所诉,使用obsidian写文章时需要进行一些设置:

  1. 点击左下角齿轮图标进入,选择“文件与链接”,将”内部链接类型“改为”基于当前笔记的相对路径“。
  2. 关闭”使用WIKI链接“。
  3. ”附件默认存放路径“改为”指定的附件文件夹“,并将”附件文件夹路径“写入images

这样,所有图片都会进入到obsidian中的images文件夹中,后续挪动图片到hugo时,只需要将images文件夹放入到hugo项目中的static文件夹即可。

如果觉得所有的博客的图片都放在一个文件夹中还是显得混乱,那么可以选择安装obsidian的Custom Attachment Location插件。它能够接管所有的markdown文件关于附件重命名、存放位置并实时编写markdown url格式,且提供相当的自定义功能。

我个人对于图片附件的存放预期是在images文件夹中建立一个跟markdown笔记同名的文件夹,该笔记的图片附件通通放入其中,并根据放入时间进行重命名。因此我的对Custom Attachment Location进行了如下配置:

  • ”新附件位置“写入./images/${noteFileName}
  • ”生成的附件文件名“写入${date:YYYYMMDDHHmmss}
  • ”markdown url格式“写入images/${noteFileName}/${originalAttachmentFileName}.${originalAttachmentFileExtension}
  • ”附加重命名模式“改为”全部“。

这样设置后,图片和其他附件会存储在obsidian仓库的根目录/images/<文章笔记同名文件夹>,而且会根据时间来重命名所有附件。

图片附件引用的url格式修改

如果此时兴冲冲的就运行hugo预览网站,那么大概率的结果是——文章的图片无法正常引用。而这,也是我碰到的第三个大坑。

经过大半天的研究,我意识到还是hugo本质是一个静态网站生成器的问题,貌似我踩的每一个坑在某种程度上都是因为这个原因。它的一个功能是将markdown文件转换成静态网站所需的html文件,因此生成的博客文件对于图片的链接是:https://<博客域名>image/<图片>,而正确的链接格式应为:https://<博客域名>/images/<图片>。问题在于原链接在 images 前少了一个斜杠/

现在需要做的事情就很明显了:在所有的markdown中,图片的引用链接从![](/images/xxx)改为![](/images/xxx)。不过一个一个的改也显得太费人工了,对付这种有着明显特征的重复性劳动,python脚本显然是一个更好的选择。我们只需要将![](/images/改为![](/images/即可。

import os

def precise_replace():
    print("开始精确替换图片路径...")
    print(f"处理目录: {os.getcwd()}")
    print()
    
    file_count = 0
    total_replacements = 0
    
    for root, dirs, files in os.walk('.'):
        for file in files:
            if file.endswith('.md'):
                file_path = os.path.join(root, file)
                try:
                    with open(file_path, 'r', encoding='utf-8', errors='ignore') as f:
                        content = f.read()
                    
                    # 精确替换:只替换 ![](/images/ 为 ![](/images/
                    old_pattern = '![](/images/'
                    new_pattern = '![](/images/'
                    
                    if old_pattern in content:
                        new_content = content.replace(old_pattern, new_pattern)
                        
                        if content != new_content:
                            # 计算替换次数
                            replacements = content.count(old_pattern) - new_content.count(old_pattern)
                            
                            # 直接保存,不备份
                            with open(file_path, 'w', encoding='utf-8') as f:
                                f.write(new_content)
                            
                            total_replacements += replacements
                            file_count += 1
                            print(f"✓ 已处理: {file} ({replacements} 次替换)")
                
                except Exception as e:
                    print(f"✗ 处理文件失败: {file} - {str(e)}")
    
    print()
    print("处理完成!")
    print(f"共处理了 {file_count} 个文件")
    print(f"总共进行了 {total_replacements} 次替换")
    
    if total_replacements == 0:
        print("\n提示: 没有进行任何替换,可能是因为:")
        print("1. 文件中没有找到 '![](/images/' 这样的模式")
        print("2. 文件编码问题")
        print("3. 图片链接已经是指定格式")
    
    input("按回车键退出...")

if __name__ == "__main__":
    precise_replace()

上述脚本需要放在content文件夹中,每次修改了博客文章都需要运行一次,它会自动检测修改content及其子文件夹中的markdown文件。

notionnext博客迁移

notionnext对笔记的格式跟hugo非常类似,基本上导出就可以直接放到obsidian正常读取。但notionnext导出的笔记里内容会带一个标题和笔记属性。先要将多余的笔记属性和标题删除可以使用下面这个python脚本:

import os
import glob

def process_markdown_files():
    """
    处理当前目录及子目录下所有.md文件
    """
    # 获取当前目录
    current_dir = os.getcwd()
    
    # 查找所有.md文件(包括子目录)
    md_files = glob.glob(os.path.join(current_dir, "**", "*.md"), recursive=True)
    
    # 定义需要删除的关键字
    keywords = ['type', 'status', 'slug', 'summary', 'tags', 'category']
    
    processed_count = 0
    
    for file_path in md_files:
        try:
            # 读取文件内容
            with open(file_path, 'r', encoding='utf-8') as file:
                lines = file.readlines()
            
            # 如果文件行数不足,跳过处理
            if len(lines) < 3:
                print(f"跳过文件 {file_path},行数不足")
                continue
            
            # ① 删除第一行和第二行
            lines = lines[2:]
            
            # ② 检查新的前6行(原文件的第3-8行),删除包含关键字的行
            lines_to_check = min(6, len(lines))  # 确保不超过文件行数
            indices_to_remove = []
            
            for i in range(lines_to_check):
                line = lines[i].strip()  # 去除首尾空白字符
                # 检查行开头是否包含关键字(忽略大小写)
                if any(line.lower().startswith(keyword) for keyword in keywords):
                    indices_to_remove.append(i)
            
            # 从后往前删除,避免索引变化问题
            for index in sorted(indices_to_remove, reverse=True):
                del lines[index]
            
            # ③ 再删除一行(新的第一行)
            if len(lines) > 0:
                lines = lines[1:]
            
            # 写回文件
            with open(file_path, 'w', encoding='utf-8') as file:
                file.writelines(lines)
            
            processed_count += 1
            print(f"已处理文件: {file_path}")
            
        except Exception as e:
            print(f"处理文件 {file_path} 时出错: {str(e)}")
    
    print(f"\n处理完成!共处理了 {processed_count} 个.md文件")

if __name__ == "__main__":
    # 确认操作
    confirm = input("此脚本将修改当前目录及子目录下所有.md文件。确认执行?(y/n): ")
    
    if confirm.lower() in ['y', 'yes', '是']:
        process_markdown_files()
    else:
        print("操作已取消")

gitalk评论的启用

gitalk是一个借助github issue开发评论的组件。它会在指定的仓库内发布issue,通过issue的标签Labels来对每一篇博客进行关联,一个issue都对应一篇博客的评论区。优点是方便简单,只需要一个github账号便可启用。

首先需要新建一个OAuth Apps,在"Homepage URL"和"Authorization callback URL"都填入博客的地址。建立完毕后就可以获得clientID,然后生成一个clientSecret

然后新建一个仓库用于issue的存放,之后所有的评论都会放在该仓库的issue中。

回到hugo.toml中进行编辑:

[params]
  enableGitalk = false
  
[params.gitalk]
  clientID = "刚刚获取的clientID"
  clientSecret = "刚刚获取的clientSecret"
  repo = "刚刚新建的仓库名" # 不需要带用户名,只要仓库名称即可。
  owner = "your github" # 您的GitHub ID
  admin = "your github" # 您的GitHub ID
  id = "location.pathname" # 文章页面的链接地址就是ID
  labels = "gitalk" # Github issue labels. 
  perPage = 100 # 可评论数量,最大为100
  pagerDirection = "last" # 时间倒序来排列评论
  createIssueManually = true # 设置为true,如果是管理员登录,会自动创建issue,如果是false,需要管理员手动添加第一个评论(issue)
  distractionFreeMode = false # 如果启用,则输入 (cmd|ctrl + enter) 可以快速回复
  # proxy = "https://cors-anywhere.azm.workers.dev/https://github.com/login/oauth/access_token" # 为不方便登录github的读者通过反向代理,可自行搭建

而这个时候,熟悉的大坑时间又来了,算算这是第四个大坑了,大坑好多。我的gitalk显示Error: Validation Failed报错,也就是未找到可关联的issue。

造成这个报错是由好几个因素叠加造成的:

  • gitalk在关联issue和文章的时候,会自动生成两个Labels并打上相关的issue,一个是上面设置的labels = "gitalk",一个是直接将博客文章的网址放入到Labels中。
  • Labels命名的限制是50个字符
  • hugo生成博客文章网址的时候,是根据文章所在的markdown文件名称来生成的。

由于这三个因素叠加,文章所在的markdown文件名称所在的文件名称是中文时,实际的生成的网址会非常长,很容易就突破50个字符的大关。这边是这次报错的由来。

GitHub的Labels有50个字符的长度限制,是固定死的。我们所能做的就只能是缩短字符数量。这里就有两个缩短字符的思路:

  • 将markdown文件改为较为简短的英文,
  • 寻找其他字符串作为关联不同文章的Labels名称,这个字符串是需要唯一的。

前者好说,改文件名就行,但这种方式不方便obsidian笔记和博客文章的相互对照。因此我选择第二个方式,而字符串的方式选择md5哈希

md5哈希作为gitalk绕过labels限制的办法其实很早就有,这里就直接给出成品。需要编辑themes/github-style/layouts/partials/gitalk.html

{{ if .Site.Params.enableGitalk }}
{{/* 检查是否启用了Gitalk评论系统 */}}

<div id="gitalk-container" class="gitalk-container"></div>
{{/* Gitalk评论容器,评论将渲染在这个div中 */}}

<!-- 引入MD5库 -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/blueimp-md5/2.19.0/js/md5.min.js"></script>
{{/* 引入MD5加密库,用于生成固定长度的页面ID */}}

<link rel="stylesheet" href='{{ "css/gitalk.css" | absURL }}'>
{{/* 引入Gitalk样式文件 */}}

<script src='{{ "js/gitalk.min.js" | absURL }}'></script>
{{/* 引入Gitalk主脚本文件 */}}

<script>
  // 获取当前页面的路径(不包含域名)
  var pageUrl = window.location.pathname;
  
  // 对页面路径进行MD5哈希,生成固定长度的唯一ID
  // 解决GitHub标签50字符长度限制的问题
  var hashId = md5(pageUrl);
  
  // 初始化Gitalk评论系统
  const gitalk = new Gitalk({
    clientID: '{{ .Site.Params.gitalk.clientID }}',    // GitHub OAuth应用的Client ID
    clientSecret: '{{ .Site.Params.gitalk.clientSecret }}', // GitHub OAuth应用的Client Secret
    repo: '{{ .Site.Params.gitalk.repo }}',            // 存放评论的GitHub仓库名
    owner: '{{ .Site.Params.gitalk.owner }}',          // GitHub仓库所有者用户名
    admin: ['{{ .Site.Params.gitalk.owner }}'],        // 管理员用户名列表(可自动创建Issue)
    id: hashId,                                        // 页面唯一标识符(使用MD5哈希值)
    title: '{{ .Title }}' || document.title,           // Issue标题,使用文章标题或页面标题
    distractionFreeMode: false                         // 禁用无干扰模式(允许使用快捷键提交)
  });
  
  // 立即执行函数,用于渲染Gitalk评论框
  (function() {
    // 检查是否在本地开发环境(localhost或127.0.0.1)
    if (["localhost", "127.0.0.1"].indexOf(window.location.hostname) != -1) {
      // 本地环境不渲染Gitalk,显示提示信息
      document.getElementById('gitalk-container').innerHTML = 'Gitalk评论在本地预览时默认不显示。';
      return;
    }
    
    // 在生产环境中渲染Gitalk评论框
    gitalk.render('gitalk-container');
  })();
</script>

{{ end }}
{{/* 结束Gitalk启用条件判断 */}}

还需要编辑hugo.tom,将id = "location.pathname"注释掉

# id = "location.pathname"

现在重新部署,gitalk应该就恢复正常了。

代码块高亮

第五个坑,便是代码高亮。由于代码块背景一直是黑色的,跟github那种浅灰的风格非常不搭配,索性重新加载了一个css文件来更改代码块的背景颜色。

需要到themes/github-style/static/css中新建一个custom.css,并写入代码块背景的样式

/* 专门针对代码块的背景色 */
pre, code, .highlight, .chroma {
    background-color: var(--color-canvas-subtle, #f6f8fa) !important;
    /* 使用CSS变量,如果变量不存在则回退到#f6f8fa */
}

/* 确保代码块文字颜色也是深色 */
pre, code {
    color: var(--color-fg-default, #24292f) !important;
}

回到hugo.toml,设置加载custom.css

pygmentsUseClasses = true

[params]
  custom_css = ["css/custom.css"]

重新部署后代码块应该就会变成github的浅灰色风格了。


尾声

在吃下这么多坑之后,也是终于将博客调教成自己想要的模样了,真是一场酣畅淋漓的大战啊