typecho-to-hugo

在上一篇文章 个人博客的最终归宿是静态网站吗 决定从 Typecho 迁移到 Hugo 后,为了保持博客样式不变,从 hugo new theme 开始,从零写了一个主题,经过这一周来 50+ 小时的努力,终于能 rm -rf typecho 后将网站重新上线了。

总体来说,样式还原度 98%,功能还原度 90%,整个过程主要分为数据迁移和主题制作两大部分,本文将简单记录下实现过程。

首先是数据迁移。包括文章和评论的迁移,整个过程得益于插件的帮助“比较”顺利。

转移文章

由于一开始考虑得不够全面,导致转移文章上有点费时。

1. 导出文章

使用的是前端大佬怡红公子的文章导出插件。进入到 Tyepcho 目录插件文件夹:

cd /path/to/typecho/usr/plugins
git clone https://github.com/lizheming/typecho-export-hugo.git Export2Hugo

启用插件后,上方菜单栏-控制台-导出至Hugo,开始导出。但下载后无法解压,尝试修改 Action.php 里第 69-73 行,改为 tar 打包:

69
70
71
72
73
$filename = "hugo.".date('Y-m-d').".tar.gz";
$outputFile = $dir."/".$filename;
exec("cd $dir && tar -czf $outputFile content"); 

header("Content-Type:application/tar+gzip");

但仍然提示文件错误,最后在服务器临时文件目录下 /tmp/Export2Hugo 直接下载回了本地,可以正常解压。

这里友情提醒下,建议提前想好你的 Hugo 文章 Front Matter 中需要哪些参数,插件中只包含一些默认的基本参数,最好提前修改下插件再导出。比如我的文章中需要用到的参数较多,还包括 lastmod 最后修改日期,toc 是否目录显示,copyright 版权信息,images 头部图片等等,我一开始没想好,导致后面想到一个添加一次,“手动遍历”了好几次 😖。

同时插件中默认导出的日期和 Hugo 默认 ISO8601 日期格式不一致,缺失了时区信息,虽然并不影响并且可以通过设置解决,但我认为还是尽量按照默认规范来,避免后期各种麻烦。显然,这个小问题也让我又“手动遍历”了一次文章… 简单的修改 Action.php 里第 44 行:

44
$time = date('c', $content["created"]);

即可和 Hugo 默认 .Date 时间格式一致。

2. 导入文章

导入文章就很简单了,直接导出的 Markdown 文件复制到 Hugo 站点 Content 目录下自定义文件夹中即可。

转移评论

上一篇文章中提到过,这里选择的评论工具是 Artalk,Artalk 的文档介绍得非常详细。因此迁移过程比较丝滑,主要时间花在配置参数和重写评论样式上。

1. 导出评论

直接使用 Artalk 提供的 Typecho 评论导出插件,按照文档导出即可,获得到一份 Artrans 格式的文件。

2. 部署 Artalk

这里我选择使用 Docker Compose 进行部署,在合适的位置新建一个 artalk 文件夹用来存放配置文件和数据。

编写 artalk-go.yml 配置文件,这一步可以忽略直接在前端配置(推荐),但我比较习惯后端配置文件(可能会覆盖前端)的形式:

cd /path/to/artalk && mkdir data
wget -O data/artalk-go.yml https://raw.githubusercontent.com/ArtalkJS/Artalk/master/conf/artalk.example.zh-CN.yml

参照配置说明按照自己的实际需求修改好后创建一个 docker-compose.yaml 文件:

version: "3.5"
services:
  artalk:
    container_name: artalk
    image: artalk/artalk-go
    restart: always
    ports:
      - 8080:23366
    volumes:
      - ./data:/data

启动 Artalk:

docker compose up -d

接下来配置 Nginx 反代,解析到二级域名:

server_name artalk.example.com;
...
location / {
    proxy_redirect off;
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_pass http://localhost:23366/;
}

或者直接反代到二级目录:

server_name www.example.com;
...
location ^~ /artalk/ {
    proxy_redirect off;
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_pass http://localhost:8233/;
}

3. 导入评论

将 Artalk 置入 Hugo,可以参照文档

登录 https://artalk.example.com 或者 https://www.example.com/artalk 进入后台,在「控制中心」找到「迁移」选项,按提示输入站点名称和地址后导入即可。

至此,网站的数据转移大致就处理完成了。


接着是主题制作。因为完全可以把现有 Typecho 主题的 CSS 照搬过来,因此在样式上其实工程量并不大,舍得花时间就行。最重要的是提前构思好整个框架,搭建好每个模板的结构。

主要精力花在一些功能的实现上,整个过程中感受到 Hugo 社区还是很活跃的,但中文文档相对较少并且大多数都过时了,大概情况是用中文搜索可以解决 30% 的问题,用英文搜索可以解决 90% 的问题。

主要功能实现

如今的静态网站已经和我记忆中的手撸 HTML 不一样了,其实更准确的说是动态编写、静态生成,得益于 Hugo 丰富的功能,可以“轻松”的实现一些“伪动态”功能。

1. 解决图片占位抖动 CLS 问题

图片占位防止 CLS 抖动这个问题我在用 Typecho 的时候研究过,因此还是沿用以前 曲线解决 Typecho 图片占位抖动问题 这篇文章中的思路,将 Markdown 图片写为下面的形式:

![alt text](/images/atpx.com.png "1920/1080")

也不用在额外进行其它处理,直接通过在 Hugo 使用 Fancybox 实现图片灯箱/放大功能 中提到过的重写 Markdown 图片解释,顺便加入目前用的图片灯箱工具 PhotoSwipe,将 render-image.html 文件编辑以下内容:

<div class="img-wrapper" id="gallery--individual">
    {{ $caption := "" }}
    {{ with .Title }}
    {{ $caption = . | safeHTML }}
    {{ end }}
    <a href="{{ .Destination | safeURL }}" data-pswp-width="{{ index (split $caption "/") 0 }}" data-pswp-height="{{ index (split $caption "/") 1 }}">
        <img style="aspect-ratio: {{ $caption | safeCSS }}" src="{{ .Destination | safeURL }}" alt="{{ .Text }}" width="{{ index (split $caption "/") 0 }}" loading="lazy" />
    </a>
</div>

2. 随机文章和搜索功能

这两个功能一直是静态网站的痛点,只能依靠 JS 或者第三方工具实现,还是用以前用过的方法,通过检索一个包含网站所有文章的 JSON 文件来实现。

a. 编辑站点配置文件 config.toml ,添加以下内容用来输出一个 JSON 地图:

[outputs]
    home = ["HTML", "RSS", "JSON"]

b. 在主题 layouts 目录下新建一个 index.json 文件,加入以下内容:

[ {{- $i := 0 -}}
{{- range where .Site.RegularPages "Section" "blog" -}}
   {{- if not .Params.Draft -}}
      {{- if gt $i 0 }},{{ end -}}
      {"date":"{{ .Date.Unix }}", "url":"{{ .Permalink }}", "title":{{ .Title | jsonify  }}, "summary":{{ .Summary | plainify | jsonify }} }
      {{- $i = add $i 1 -}}
   {{- end -}}
{{- end -}} ]

上面的代码中只加入了日期,链接,标题和摘要数据,你可以根据自己的需求修改。当然,要不要加入全文内容 .Conent 对象是一个值得纠结的问题,加入可以让搜索更加全面精准,但如果文章量很多的话也会导致性能问题,同时也相当于给采集站和垃圾内容农场拱手送上自己的劳动成果虽然和等它们直接爬区别不大

c. 随机文章功能通过 JS 来实现,简单来说就是得到文章的数量,生成几个不重复的随机数然后取对应的值,具体可以参照以下代码:

/* 
* Get random posts
* Written by ATP on 2023-03-01
* Website: https://atpx.com 
 */
function randomPosts(n) {
    const addList = document.querySelector('.random-posts h2');
    let postList = [],
        postIdSet = [],
        postInfo,
        id;
    fetch('/index.json')
        .then(res => res.json())
        .then((postsJSON) => {
            let allPostLength = Object.keys(postsJSON).length;
            for ( let i = 0; i < n; i++) {
                id = Math.floor(Math.random()*allPostLength);
                if (postIdSet.indexOf(id) == -1){
                    postIdSet.push(id);
                    postInfo = '<li><a href="' + postsJSON[id].url + '">' + postsJSON[id].title + '</li>';
                    postList.push(postInfo);
                } else {
                    i--;
                }
            }
            addList.insertAdjacentHTML('afterend', postList.join(''));
        }).catch(err => console.error(err)
    );
}

在需要展示随机文章的地方调用,例如随机展示 5 篇文章:

<div class="random-posts">
    <h2>Random Posts</h2>
    <script>randomPosts(5)</script>
</div>

我这里只展示了标题,你可以在本文底部看到效果。

搜索功能和上面的思路一致,并且官方也提供了很多更好的方法,迫于篇幅,这里不再赘述。

3. 文章目录滚动高亮

这也是一个在前面的文章 我选择了放弃 jQuery 中解决过的问题,同时 Hugo 内置了文章目录功能,可以直接调用,因此只需要照搬并稍作修改即可。

a. 在 single.html 中合适的地方添加文章目录:

<nav class="post-toc">
    <h2>Table of Contents</h2>
    {{ .TableOfContents }}
</nav>

b. 修改 CSS 文件,将目录固定在页面右方并添加高亮样式:

.post-toc {
    position: fixed;
    top: 50px;
    right: 50px
}
.post-toc a.active {
    color: blue
    transition: all .25s ease-in-out
}

c. 添加 JS 函数,这里写的是当 <h2><h3> 滚动到页面上方 1/4 处则高亮显示:

/* 
* Highlight ToC on scrolling
* Written by ATP on 2023-03-05
* Website: https://atpx.com 
 */
const postTOC = document.querySelector('.post-toc'),
    headingObserver = new IntersectionObserver(headings => {
    headings.forEach(heading => {
        const id = heading.target.getAttribute('id');
        if (heading.isIntersecting) {					
            inactive();
            document.querySelector(`.post-toc a[href="#${id}"]`).classList.add('active');
        }
    });
}, {
    rootMargin: '0px 0px -75%'
});		
document.querySelectorAll('.post-content h2[id],h3[id]').forEach((heading) => {
    headingObserver.observe(heading);
});
    
function inactive() {
    document.querySelectorAll('.post-toc a').forEach((a) => {
        a.classList.remove('active');
    });
}

遇到的一些问题

这里也简单记录一些在写主题中遇到的问题,方便后面的朋友避免踩坑。

1. 代码块字体大小

Hugo 现在自带代码高亮功能,直接将样式生成到静态文件中,不用在前端 JS 修改,效率更高,于是直接抛弃 PrismJS,使用自带的功能。

但发现有代码块字体大小不一的问题,尝试修改代码高亮主题、!important 字体大小都没有用,让我一度认为是 Hugo 使用的 Chroma 的问题,最后一番探索后,在一个 issue 中才知道是存在于 iOS Safari 的一个远古问题,在代码块样式中添加以下 CSS 解决:

code,
pre {
    text-size-adjust: 100%;
    -ms-text-size-adjust: 100%;
    -moz-text-size-adjust: 100%;
    -webkit-text-size-adjust: 100%;
}

hugo-code-size-issue-on-ios

2. Markdown 渲染

Hugo 默认使用 Goldmark 作为 Markdown 解释器,但在处理 CJK 字符时会有一些问题,这里用使用删除线 ~~删除线~~ 举例,如果语法块头尾中出现了全角符号(例如: ~~删除线。~~尾巴)则会导致解释失败,需要在语法块后面的内容前加上空格(例如:~~删除线。~~ 尾巴)才能正常解释,但排版就不好看了。这个问题也好解决,花时间把所有文章改一遍,避免该情况。

整个过程中还遇到了很多问题,不过突然不想写了。


总的来说,这折腾一番迁移到 Hugo 上还是值得的,有种回归大自然的奇妙感觉,之后应该不会再折腾迁移到其它平台了?

甚至一眼看到了这个博客的终点,最后丢到某个「靠谱」的静态网站托管平台上不再维护,在漫长岁月中逐渐消失在互联网上。但在那之前,还有一段努力写字的时间。

如果你认为这篇文章还不错,可以考虑支持作者