❗️ 注意:离本文创建时间已经过去了
天,请注意时效性
🖥 说明:本文记录了从 Jekyll 博客引擎迁移到 Hexo 过程中遇到的一些问题。
前言
我是一名前端开发人员,因此对其他的一些脚本语言如 Ruby 并不熟悉。刚开始写博客的时候使用了 Github Pages 作为博客平台,它默认使用的是 Jeklly 框架,想着内容比框架更重要,因此也就用着了,这一用就是 10 年。
期间换过一些主题,后来锁定了 Hux 提供的主题,简洁大方好看,而且是开源的:
黄玄的博客 | Hux Blog 「离开世界之前 一切都是过程」 https://huangxuan.me/
我针对这个主题做了很多的定制化内容,如自定义右侧内容、自定义数据、使用 Notion 作为数据源渲染等。
但随着苹果的 M 系列芯片的发布,我越来越难以处理 Ruby 在 Intel 和 Apple 芯片之间的差异,举个例子来说,我构建的时候需要特定使用 x86 架构的指令才能偶然正确 build,这还是我不能随便动任意一个依赖的前提下:
1 arch -x86_64 bundle exec jekyll server --trace --config=_config.dev.yml --ssl-key local.xheldon.cn.key --ssl-cert local.xheldon.cn.pem
因此我意识到如果再不尽快更换框架,我可能将来就完全无法发布博客了。
技术选型
这一节没有什么好说的,你基本可以认为,Hexo 是 Jekyll 的 JavaScript 实现。里面的很多概念,95% 的都相同,因此迁移上手无难度。
更重要的是,Hexo 中也有人做了 Hux 的博客主题模板,因此我就直接拿来用了,在这个过程中简单记录一下过程。
迁移过程
插件迁移
这个属于比较容易的,在 Jekyll 中的插件,在 Hexo 中我是用辅助函数实现,如下是我处理来自 Notion 的 Bookmark 的标签的函数(路径是 _plugins/add-attribute.rb
:)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 module Jekyll class RenderBookMarkBlock < Liquid::Block def initialize (tag_name, attr, tokens ) super attrs = attr.scan(/url\=\"(.*)\"\stitle\=\"(.*)\"\simg\=\"(.*)\"\syid\=\"(.*)\"\sbid\=\"(.*)\"/ ) if !attrs.empty? @url = attrs[0 ][0 ] @title = attrs[0 ][1 ] @img = attrs[0 ][2 ] @yid = attrs[0 ][3 ] @bid = attrs[0 ][4 ] @firstChar = @title .empty? ? "" : (@title )[0 ].upcase @error = "" else attrs = attr.scan(/url\=\"(.*)\"\stitle\=\"(.*)\"\simg\=\"(.*)\"/ ) @url = attrs[0 ][0 ] @title = attrs[0 ][1 ] @img = attrs[0 ][2 ] @firstChar = @title .empty? ? "" : (@title )[0 ].upcase @error = "" end end def render (context ) @desc = super if !@yid .nil ? && !@yid .empty? "<p class='embed-responsive embed-responsive-16by9'><iframe src='https://www.youtube.com/embed/#{@yid } ?rel=0' title='YouTube video player' frameborder='0' allow='accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture' allowfullscreen></iframe></p>" elsif !@bid .nil ? && !@bid .empty? "<p class='embed-responsive embed-responsive-16by9' style='border-bottom: 1px solid #ddd;'><iframe src='//player.bilibili.com/player.html?bvid=#{@bid } &high_quality=1&as_wide=1' scrolling='no' border='0' frameborder='no' framespacing='0' allowfullscreen></iframe></p>" else "<p><a class='link-bookmark' href='#{@url } ' target='_blank'><span data-bookmark-img='#{@img } ' data-bookmark-title='#{@firstChar } '><img src='#{@img } '/></span><span><span>#{@title } </span><span>#{@desc } </span><span>#{@url } </span></span></a></p>" end end end end Liquid : :Template .register_tag('render_bookmark' , Jekyll : :RenderBookMarkBlock )
而在 Hexo 中我是这么写的(路径是 scripts/liquid.js
):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 hexo.extend .tag .register ('render_bookmark' , function (args, content ) { const [url, title, img, yid, bid] = args.map (getValue); const firstChar = title ? title[0 ].toUpperCase () : '' ; const strip_html = hexo.extend .helper .get ('strip_html' ).bind (hexo); const trim = hexo.extend .helper .get ('trim' ).bind (hexo); if (yid) { return `<p class='embed-responsive embed-responsive-16by9'><iframe src='https://www.youtube.com/embed/${yid} ?rel=0' title='YouTube video player' frameborder='0' allow='accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture' allowfullscreen></iframe></p>` } else if (bid) { return `<p class='embed-responsive embed-responsive-16by9' style='border-bottom: 1px solid #ddd;'><iframe src='//player.bilibili.com/player.html?bvid=${bid} &high_quality=1&as_wide=1' scrolling='no' border='0' frameborder='no' framespacing='0' allowfullscreen></iframe></p>` ; } return `<p><a class='link-bookmark' href='${url} ' target='_blank'><span data-bookmark-img='${img} ' data-bookmark-title='${firstChar} '><img src='${img} '/></span><span><span> ${title} </span><span> ${strip_html(trim(content))} </span><span> ${url} </span></span></a></p>` ; }, { ends : true , });
Permalink 问题
不知道为什么,我在 Hexo 中设置了 :category/:name.html
但是依然给我生成的是 life/2024-life-xxx.html
(使用 -
目录分割) 而预期应该是 life/xxx.html
,看源码 name
使用了 slug
,的 basename
,但是 slug
生成逻辑是基于 folder 路径然后加上 - 的,因此我只能自己手动修改文件名。Jekyll 中,文件名前的日期格式,如 2024-02-12-xxx.md 中,2024-02-12 会被忽略,title 直接就是 xxx,但是在 Hexo 中 title 在 post 中读取的 title front matter,所以只能写一个 filter 插件来最终确定 permalink 地址:
1 2 3 4 5 6 7 8 9 10 hexo.extend .filter .register ('post_permalink' , function (data ) { const arr = data.split ('/' ).filter (Boolean ); const categories = arr[0 ]; const name = arr[1 ]; return `${categories} /${name.split('-' ).filter(Boolean ).slice(3 ).join('-' )} ` ; });
另外,permalink 不能是纯数字,得用字符串:
不然报错(看 .endsWith 就知道为什么报错啦):
EJS 模板语法问题
ejs 模板嵌套的时候,与 Jekyll 的 liquid 语法不同的是,它不能在模板中自定义 front-matter。也就是说,模板之间的传参,只能外层到内层,不能从内到外。比如我 index 的 layout 是 page,同时还设置了 fornt-matter,但是此 front-matter 无法被 page.ejs 这个模板读取到,因此我只能每个用到的地方都往下传参。
Markdown 语法渲染问题
Hexo 自带的 marked 的 markdown 渲染跟 jekyll 的 karmarkdown 渲染有出入,前者会把诸如 ## h2 (换行+空行)段落 中的空行+段落给忽略,而后者不会。
虽然无伤大雅,但是这样会导致我在首页的时候,内容摘要会少一个空格导致不完全一致,而我希望尽可能的完全一致,这样 SEO 才不会降权,才能让搜索引擎不会认为我的内容有巨大改动。
于是我使用了 markdown-it 进行处理,安装 hexo-renderer-markdown-it 即可。
Liquid 排序问题
我 Jekyll 的 liquid 的排序是这么用的: {{ tags | split:'`**`SEPARATOR`**`' | sort }}
:
问题是,Liquid 的排序在 sort 部分相同后,会按照后面的 href 字符串排序的,因此继承过来:
Mermaid 语法问题
mermaid 不能被 highlight 高亮,需要在 hexo 的 config 中排除掉:
1 2 exclude_languages: - mermaid
Markdown 无法使用 EJS 语法问题
暂时没管,曲线绕。
分页生成问题
hexo-generator-index 的 pagination 生成的是 page/2 page/3 但是 jekyll 中的格式是 page2, page3 所以复制了该插件源码放到本地进行修改(位于 scripts/pagination.js
):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 'use strict' ;const pagination = require ('hexo-pagination' ); hexo.config .index_generator = Object .assign ( { per_page : typeof hexo.config .per_page === 'undefined' ? 10 : hexo.config .per_page , order_by : '-date' , }, hexo.config .index_generator ); hexo.extend .generator .register ('index' , function (locals ) { const config = this .config ; const posts = locals.posts .sort (config.index_generator .order_by ); posts.data .sort ((a, b ) => (b.sticky || 0 ) - (a.sticky || 0 )); const paginationDir = config.pagination_dir || 'page' ; const path = config.index_generator .path || '' ; return pagination (path, posts, { perPage : config.index_generator .per_page , layout : ['index' , 'archive' ], format : paginationDir + '%d/' , data : { __index : true , }, }); });
删除线实现问题
默认删除线是 s 标签,但是 jekyll 是 del,用 after_render:html 直接替换(位于 scripts/tag-del.js
):
1 2 3 hexo.extend .filter .register ('after_render:html' , function (str ) { return str.replace (/<s>/g , '<del>' ).replace (/<\/s>/g , '</del>' ); });
构建后多余文件问题
有些文件是多余的,构建后删除掉:
具体有:
我使用了自定义的标签样式来渲染来自 Notion Bookmark 的,以期望跟 Notion 的 Bookmark 一样好看,但是如此一来,RSS 阅读器如 Reeder 就无法正确渲染出样式了,因此我处理了以下,在 Jekyll 中使用的是模板语法处理函数,也即在构建的时候动态替换掉自定义的样式:
1 2 3 4 5 6 7 8 9 module Jekyll module BookmarkFilter def bookmark_filter (input) input.gsub (/^\<p\>\<a\s+class=\"link-bookmark\"\shref=(.*)\starget=\"_blank\"\>\<span\>(.*)\<\/span\>\<span\>\<span\>(.*)\<\/span\>\<span\>\n(.*)\n\<\/span\>\<span\>(.*)\<\/span\>\<\/span\>\<\/a\>\<\/p\>$/ , '<p><a href=\1 target="_blank">\3</a></p>' ); end end end Liquid ::Template .register_filter (Jekyll ::BookmarkFilter )
而在 Hexo 中是在构建之后打补丁的方式处理(位于 scripts/rss-gene.js
):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 const fs = require ('fs' );const path = require ('path' );const ejs = require ('ejs' );const rootDate = new Date ();function getDate (_date ) { const date = _date ? new Date (_date) : rootDate; const days = ['Sun' , 'Mon' , 'Tue' , 'Wed' , 'Thu' , 'Fri' , 'Sat' ]; const months = [ 'Jan' , 'Feb' , 'Mar' , 'Apr' , 'May' , 'Jun' , 'Jul' , 'Aug' , 'Sep' , 'Oct' , 'Nov' , 'Dec' , ]; const dayName = days[date.getDay ()]; const day = String (date.getDate ()).padStart (2 , '0' ); const month = months[date.getMonth ()]; const year = date.getFullYear (); const hours = String (date.getHours ()).padStart (2 , '0' ); const minutes = String (date.getMinutes ()).padStart (2 , '0' ); const seconds = String (date.getSeconds ()).padStart (2 , '0' ); const timezoneOffset = -date.getTimezoneOffset (); const sign = timezoneOffset >= 0 ? '+' : '-' ; const offsetHours = String ( Math .floor (Math .abs (timezoneOffset) / 60 ) ).padStart (2 , '0' ); const offsetMinutes = String (Math .abs (timezoneOffset) % 60 ).padStart (2 , '0' ); return `${dayName} , ${day} ${month} ${year} ${hours} :${minutes} :${seconds} ${sign} ${offsetHours} ${offsetMinutes} ` ; } hexo.extend .generator .register ('xml' , function (locals ) { const template = '\uFEFF' + `<?xml version="1.0" encoding="UTF-8"?> <rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"> <channel> <title>Xheldon Blog</title> <description>The Answer to Life, the Universe and Everything is...</description> <link>https://www.xheldon.com</link> <atom:link href="https://www.xheldon.com/feed.xml" rel="self" type="application/rss+xml" /> <pubDate><%= getDate() %></pubDate> <lastBuildDate><%= getDate() %></lastBuildDate> <generator>Hexo v<%= version %></generator> <% for (post of posts.sort((a, b) => (new Date(b.date).getTime()) - (new Date(a.date).getTime())).slice(0, 10)) { %> <item> <title><%= post.title %></title> <description><%= bookmark_filter(post.content) %></description> <pubDate><%= getDate(post.date) %></pubDate> <link><%= post.permalink %></link> <guid isPermaLink="true"><%= post.permalink %></guid> <% for (tag of post.tags.data) { %> <category><%= tag.name %></category> <% } %> <% for (cat of post.categories.data) { %> <category><%- escape_html(cat.name) %></category> <% } %> </item> <% } %> </channel> </rss>` ; const bookmark_filter = hexo.extend .helper .get ('bookmark_filter' ).bind (hexo); const escape_html = hexo.extend .helper .get ('escape_html' ).bind (hexo); const data = { posts : locals.posts .toArray (), getDate, version : hexo.version , escape_html, bookmark_filter, }; const jsonContent = ejs.render (template, data); const outputPath = path.join ('source/_posts' , 'feed.xml' ); fs.writeFileSync (outputPath, jsonContent, { encoding : 'utf8' }); return { path : 'feed.xml' , data : jsonContent, }; });
Service-Worker 问题
移除了 service worker,因为每次构建,页面的 tags 部分一定会变,导致 html 页面一定会更新,会导致经常需要手动刷新页面,不胜其烦,因此直接移除了。
其他问题
后话
基本就是这么多,使用 BeyondCompare 逐行对比后基本可以最小化变化的迁移过去。
- EOF -