Solitude 主题改造记录
更新日志
新增 Memos 说说接入相关修改
新增中文排版相关修改
新增底栏图片修改
新增游戏界面修改
更新引用段落到右键菜单相关修改
更新 Umami 统计的部署
更新轻量友链朋友圈的部署
更新一些琐碎的修改
更新参考其他博客教程的改造
更新 MathJax 与 Shiki 的适配方式
发布文章
本文章将会持续更新,所有使用 Solitude 期间对网站的修改都会在此进行总结。
数学公式
博主作为数学系的学生,在博客中分享部分数学学习笔记算是半个刚需了。Solitude 主题自带 KaTeX 支持,如果只是简单的数学公式数学需求,是完全够用的。但是 KaTeX 的数学支持比较弱,且移动端的多行公式浏览体验不佳,因此我把博客的公式渲染引擎换成了 MathJax。
最开始博客是直接引入了 MathJax 的 js 文件,在客户端进行公式渲染。但是很快我就发现该方式和 pjax 不合,尝试在引入 js 的标签中添加 pjax
也无效,因此改为在服务端渲染。
服务端渲染最简单的办法就是去使用 NexT 主题的一个插件 hexo-filter-mathjax。首先安装该插件、卸载原先的 Markdown 渲染引擎并安装对 MathJax 支持更好的 hexo-renderer-pandoc,最后清除 Hexo 缓存:
1 | npm uninstall hexo-renderer-marked --save # npm uninstall hexo-renderer-kramed --save # 根据此前安装的 Markdown 渲染引擎来卸载 npm install hexo-renderer-pandoc --save npm install hexo-filter-mathjax --save hexo clean |
接着在 _config.yml
中添加 MathJax 与 Pandoc 相关配置即可。
1 | mathjax: tags: none # or 'ams' or 'all' single_dollars: true # enable single dollar signs as in-line math delimiters cjk_width: 0.9 # relative CJK char width normal_width: 0.6 # relative normal (monospace) width append_css: true # add CSS to pages rendered by MathJax every_page: false # if true, every page will be rendered by MathJax regardless the `mathjax` setting in Front-matter packages: # extra packages to load extension_options: {} # you can put your extension options here # see http://docs.mathjax.org/en/latest/options/input/tex.html#tex-extension-options for more detail pandoc: args: - "-f" - "markdown" - "-t" - "html" - "--mathjax" |
代码高亮
博主还在使用 Typst 进行部分课程笔记与作业的书写,也会在博客中分享一些 Typst 使用经验。不幸的是,Hexo 自带的两个代码语法高亮引擎——highlight.js 和 Prismjs 都不支持对比较新的 Typst 生成语法高亮。
解决办法之一是使用 Typst.ts 在 Node 环境中解析 Typst 代码语法,并发送给 highlight.js。Typst.ts 的子项目 highlighter 提供了一个实现,而下面这篇文章则给出了在 Hexo 上的使用方法与实现思路。这也是我最开始使用的方法。
另一个办法则是改用 Shiki 进行公式渲染。Shiki 的优点在于它支持导入自定义的主题与语言,因此不需要担心语言支持问题。我在几个已有项目的基础上花了点时间开发了一个更适合 Solitude 主题使用的 Shiki 插件:
目前本博客也在使用该插件进行代码语法高亮渲染。
主题适配
正在整理这部分代码,准备提 PR
在 Solitude 上使用该插件需要对主题的部分代码进行修改:
1 | // highlight const {syntax_highlighter: syntaxHighlighter, highlight, prismjs, shiki} = hexo.config let {enable: highlightEnable, line_number: highlightLineNumber} = highlight let {enable: prismjsEnable, line_number: prismjsLineNumber} = prismjs let {enable: shikiEnable, line_number: shikiLineNumber} = shiki // for hexo > 7.0 if (syntaxHighlighter) { highlightEnable = syntaxHighlighter === 'highlight.js' prismjsEnable = syntaxHighlighter === 'prismjs' shikiEnable = syntaxHighlighter === 'shiki' } style.define('$highlight_enable', highlightEnable) style.define('$highlight_line_number', highlightLineNumber) style.define('$prismjs_enable', prismjsEnable) style.define('$prismjs_line_number', prismjsLineNumber) style.define('$shiki_enable', shikiEnable) style.define('$shiki_line_number', shikiLineNumber) style.define('$language', config.language) |
1 | const $syntaxHighlight = syntax === 'highlight.js' ? document.querySelectorAll('figure.highlight') : document.querySelectorAll('pre[class*="language-"]') const $syntaxHighlight = syntax === 'highlight.js' || syntax === 'shiki' ? document.querySelectorAll('figure.highlight') : document.querySelectorAll('pre[class*="language-"]') |
1 | if $highlight_enable @require "highlight/index.styl" if $prismjs_enable @require "prismjs/index.styl" if $shiki_enable @require "shiki/index.styl" |
1 | if hexo-config('highlight.enable') @import "diff" figure pre margin 0 padding 8px 0 border none table display block border none overflow-y hidden overflow-x auto td padding 0 border none height 100% &.gutter opacity .6 user-select none min-width initial pre overflow auto line-height 1.6 margin 0 padding 8px .5rem border none color var(--efu-secondtext) background var(--efu-secondbg) border-right var(--style-border-always) text-align center &.code width 100% display flex position relative pre padding-right .5rem padding-left .5rem line-height 1.6 width 100% .line &.marked background-color: rgba(97, 97, 97, .314) |
1 | figure.highlight table::-webkit-scrollbar color var(--efu-secondbg) height 6px background var(--efu-hl-bg) border-radius 6px display initial [data-theme=dark] & td.code span color var(--shiki-dark) !important // background-color var(--shiki-dark-bg) !important // 不建议使用 font-style var(--shiki-dark-font-style) !important font-weight var(--shiki-dark-font-weight) !important text-decoration var(--shiki-dark-text-decoration) !important |
这样就可以适配 Solitude 主题了。
MathJax 适配
此外,使用 Shiki 进行代码高亮渲染会导致 MathJax 的 LiteDOM adaptor 解析文档失败,因此需要把 hexo-filter-mathjax 的代码进行部分修改。
新建 mathjax.js
,放到博客的 scripts
目录下:
1 | "use strict"; const { mathjax } = require("mathjax-full/js/mathjax.js"); const { TeX } = require("mathjax-full/js/input/tex.js"); const { SVG } = require("mathjax-full/js/output/svg.js"); const { LiteAdaptor } = require("mathjax-full/js/adaptors/liteAdaptor.js"); const { RegisterHTMLHandler } = require("mathjax-full/js/handlers/html.js"); const { escapeHTML } = require("hexo-util"); let { AllPackages } = require("mathjax-full/js/input/tex/AllPackages.js"); const config = (hexo.config.mathjax = Object.assign( { tags: "none", single_dollars: true, cjk_width: 0.9, normal_width: 0.6, append_css: true, every_page: false, extension_options: {}, }, hexo.config.mathjax )); const adaptor = new LiteAdaptor({ fontSize: 16, cjkCharWidth: config.cjk_width, unknownCharWidth: config.normal_width, }); RegisterHTMLHandler(adaptor); const render = (content) => { if (Array.isArray(config.packages)) { AllPackages = AllPackages.concat(config.packages); } const tex = new TeX( Object.assign( { packages: AllPackages, tags: config.tags, }, config.extension_options ) ); const svg = new SVG({ fontCache: "none", }); const html = mathjax.document(content, { InputJax: tex, OutputJax: svg, }); html.render(); const htmlContent = adaptor.innerHTML(adaptor.body(html.document)); if (html.outputJax.math.display) { const latexContent = html.outputJax.math.math; const outputHtml = htmlContent.replace( "</mjx-container>", `<button class="copy-button" onclick="rm.copyText(\`${escapeHTML( latexContent.replace(/\\/g, "\\\\") )}\`)"><i class="solitude fas fa-copy"></i></button></mjx-container>` ); return outputHtml; } return htmlContent; }; hexo.extend.filter.register("after_render:html", (html, { page }) => { if ( config.every_page || page.mathjax || (page.__index && page.posts.toArray().find((post) => post.mathjax)) ) { return html .replace(/<span\s+class="math\s+[^"]*">\\[\(\[].*?\\[\)\]]<\/span>/gs, render) .replace('<div id="post">', '<div id="post"><link rel="stylesheet" href="/css/mathjax.css">'); } return html; }); |
接着在 source
目录下新建 css/mathjax.styl
文件:
1 | mjx-container[jax='SVG'] direction ltr > svg overflow visible + br display none &[display='true'] overflow auto hidden display block text-align center margin 1em 0 &[justify='left'] text-align left &[justify='right'] text-align right [jax='SVG'] mjx-tool display inline-block position relative width 0 height 0 > mjx-tip position absolute top 0 left 0 mjx-tool > mjx-tip display inline-block padding 0.2em border 1px solid #888 font-size 70% background-color #f8f8f8 color black box-shadow 2px 2px 5px #aaaaaa g &[data-mml-node='merror'] > g fill red stroke red > rect[data-background] fill yellow stroke none &[data-mml-node='mtable'] > line[data-line] stroke-width 70px fill none > rect[data-frame] stroke-width 70px fill none > .mjx-dashed stroke-dasharray 140 > .mjx-dotted stroke-linecap round stroke-dasharray 0, 140 > svg overflow visible &[data-mml-node='maction'][data-toggle] cursor pointer mjx-status display block position fixed left 1em bottom 1em min-width 25% padding 0.2em 0.4em border 1px solid #888 font-size 90% background-color #f8f8f8 color black foreignObject[data-mjx-xml] font-family initial line-height normal overflow visible .MathJax path stroke-width 3 .math.display display block position relative .copy-button display inline position relative right -1em top 0 opacity 0 width 0 &:hover .copy-button opacity 1 |
然后就可以把 hexo-filter-mathjax 给卸载掉了。由于改为了生成后渲染,Hexo 生成的 db.json 也更加友好——这意味着本地搜索的 xml 可以大幅压缩,Algolia 搜索则可以使用 TeX 语法来搜索数学公式。
其他修改
这部分的改造比较琐碎,因此集中在一节。
Pandoc Markdown 引用支持
本站使用 hexo-renderer-pandoc 进行文章渲染,这也意味着可以使用 Pandoc 版本的扩展 Markdown 格式来书写文章。 我的多复变函数论学习笔记原文均为 Typst 编写,每隔一段时间会使用 Pandoc 转换部分笔记到 Markdown,进行简单修改后放到博客上。我的笔记包含许多的引用,这些引用转换到 Markdown 格式时会像这样:
1 | **定理** (多圆柱上的 Cauchy 积分公式): ... []{#theorem_多圆柱上的_Cauchy_积分公式} ... **推论**: ... _证明_: 对 ... 应用[\[theorem_多圆柱上的_Cauchy_积分公式\]](#theorem_多圆柱上的_Cauchy_积分公式){.ref},得 ... |
进行简单修改(修改引用的位置和引用文本)后,Pandoc 渲染出的 html 是这样的:
1 | <span id="theorem_多圆柱上的_Cauchy_积分公式"><strong>定理</strong> (多圆柱上的 Cauchy 积分公式)</span> <a href="#theorem_多圆柱上的_Cauchy_积分公式" class="ref" data-pjax-state="">定理 1</a> |
显然,我需要为这个引用块添加跳转功能以方便阅读,这里我选择直接在主题的 main.js
里加上 ref 跳转的功能。
1 | class toc { // ... } class ref { static init() { const el = document.querySelectorAll('a.ref') el.forEach((e) => { e.addEventListener('click', (event) => { event.preventDefault() utils.scrollToDest(utils.getEleTop(document.getElementById(decodeURI(event.target.hash.replace('#', '')))), 300) }) }) } } // ... window.refreshFn = () => { // ... if (is_post || is_page) { addHighlight(); tabs.init(); ref.init(); } // ... } |
防止友链的随机访问跳转到非博客友链
修改主题中关于随机友链的 pug:
1 | - var datalinks = [] - var data = site.data.links.links if(data) each item in data each y in item.link_list - datalinks.push({ name: y.name,link: y.link}) if y.noTravel - datalinks.push({ name: y.name, link: y.link, noTravel: y.noTravel}) else - datalinks.push({ name: y.name, link: y.link}) script. const links = !{JSON.stringify(datalinks)} const travelLinks = links.filter((i) => !('noTravel' in i && i.noTravel)) const randomText = '!{_p('link.random')}' function travelling() { const link = links[utils.randomNum(links.length)]; const link = travelLinks[utils.randomNum(travelLinks.length)]; |
然后在 links.yml
中为不应该跳转的友链添加上 noTravel: true
即可。
1 | - name: Hexo link: https://hexo.io/ descr: 快速、简洁且高效的博客框架 avatar: https://hexo.io/icon/favicon-196x196.png noTravel: true |
增加背景图片切换开关
改的东西太多太琐碎了,直接看 commit。
修改页脚友链数量
一样,改的东西琐碎了点,直接看 commit
修改评论区样式
样式文件直接拿走即可。基于 Solitude 主题自带 Twikoo 样式进行修改,主要改了: - 把头像预览加回来了(仅限评论区顶部编辑栏) - 把 Markdown 预览和取消回复加回来了 - 补充了管理面板样式
删除无用 css
说是删除,其实是避免渲染,把主题 source/css 目录下的 var.styl 和 third_party 文件夹,名字前面都加上一个下划线来避免渲染,同时修改 index.styl 中对应的路径即可。
修改引用到评论右键菜单
这个改起来简单,直接放代码:
1 | const sco = { // ... toTalk(txt) { utils.scrollToDest(utils.getEleTop(document.getElementById('post-comment')), 300); const toTalkFn = () => { const inputs = ["#wl-edit", ".el-textarea__inner", "#veditor", ".atk-textarea"]; inputs.forEach(selector => { const el = document.querySelector(selector); if (el) { el.dispatchEvent(new Event('input', { bubble: true, cancelable: true })); el.value = '> ' + txt.replace(/\n/g, '\n> ') + '\n\n'; el.focus(); el.setSelectionRange(-1, -1); } }); utils.snackbarShow(GLOBAL_CONFIG.lang.totalk, false, 2000); } hpcesia.waitTwikoo(toTalkFn) }, // ... } const hpcesia = { // ... waitTwikoo(callback, scale = 100){ setTimeout(() => { if (window.twikoo) callback() else waitTwikoo(callback) }, scale); } // ... } |
新增游戏界面
其实本质是把原本的 equipment 页面套个壳。你也可以直接查看 commit 快速了解我的改动。
在 solitude/layout/includes/page 目录中新建 games.pug 文件,写入如下内容:
1 | include ../widgets/page/banner #games if site.data.games each cls in site.data.games .game-group h2.game-group-title= cls.name .game-group-desc= cls.desc .game-group-content each item in cls.list .game-item .game-item-cover img.game-item-image(src=item.image, alt=item.name) .game-item-info .game-item-name(onclick='utils.copy("' + item.name + '")')= item.name if item.score .game-item-score= "评分: " + item.score .game-item-spec= item.spec .game-item-desc= item.desc if item.link .game-item-toolbar a.game-item-link(href=item.link, target="_blank") 详情 a.bber-reply(onclick=`sco.toTalk('${item.name}\\n\\n${item.spec}\\n\\n${item.desc}')`) i.solitude.fa-solid.fa-comment(style="font-size: 1rem;") |
1 | block content main.layout#content-inner(class=page.aside ? '' : 'hide-aside') #page case page.type //- ... when 'games' include includes/page/games //- ... |
1 | - name: 好游戏 # 分类名称 desc: 比较喜欢的游戏 # 分类描述 list: # 游戏列表 - name: Mincraft # 游戏名称 score: 9/10 # 游戏评分,可选 spec: 神作,无需多言 # 副标题 desc: "探索随机生成的世界,建造从最简单的住宅到最宏伟的城堡等一切不可思议之物。您可以在创造模式中享用无限资源,也可以在生存模式中挖掘整个世界,合成武器和盔甲,抵御各种危险生物。攀登崎岖的群山,探明复杂的洞穴,挖掘大型矿脉。探索错综的洞穴与石笋洞穴生物群系。用蜡烛照亮世界,展示出自己作为知识渊博的地下冒险者和登山大师的风采!" # 游戏描述 link: http://www.minecraft.net/ # 游戏链接 image: https://bu.dusays.com/2024/11/04/672882a184cb7.webp # 游戏封面 |
hexo generate
,游戏页面的 HTML 已可正确渲染。如果发现不能正常渲染请使用 hexo clean
清除缓存后重试。 接着来为该界面添加样式。在 solitude/scripts/helper/stylus.js 中定义 games 页面样式引入所需的 stylus 变量:
1 | hexo.extend.filter.register('stylus:renderer', function (style) { // ... style.define('$equipment', !!(data && data.equipment)) style.define('$games', !!(data && data.games)) // highlight const {syntax_highlighter: syntaxHighlighter, highlight, prismjs, shiki} = hexo.config // ... }); |
1 | // ... if $about @import "_about/about" if $games @import "games.styl" // ... |
1 | #games margin-top 26px .game-group .game-group-title line-height 1 .game-group-desc line-height 1 margin 4px 0 8px 0 color var(--efu-secondtext) .game-group-content display flex flex-direction row flex-wrap wrap margin 0 -8px .game-item width calc(33% - 12px) border-radius 12px border var(--style-border-always) overflow hidden margin 8px 6px background var(--efu-card-bg) box-shadow var(--efu-shadow-border) min-height 400px position relative user-select none +maxWidth1200() width calc(50% - 12px) +maxWidth768() width 100% .game-item-cover width 100% height 200px background var(--efu-secondbg) display flex justify-content center .game-item-image object-fit cover width 100% .game-item-info padding 8px 16px 16px 16px .game-item-name font-size 18px font-weight 700 line-height 1 margin-bottom 8px white-space nowrap text-overflow ellipsis width fit-content display inline &:hover color var(--efu-vip) cursor pointer .game-item-score display inline position absolute right 1em font-size 0.8em color var(--efu-secondtext) .game-item-spec font-size 12px color var(--efu-secondtext) line-height 1 margin-bottom 12px white-space nowrap overflow hidden text-overflow ellipsis .game-item-desc line-height 1.4em color var(--efu-secondtext) height 5.6em display -webkit-box overflow hidden -webkit-line-clamp 4 -webkit-box-orient vertical font-size 14px .game-item-toolbar display flex justify-content space-between position absolute bottom 9px left 0 width 100% padding 0 16px .game-item-link font-size 12px background var(--efu-gray-op) padding 4px 8px border-radius 8px cursor pointer &:hover background var(--efu-main) color var(--efu-white) |
中英混排间距与标点压缩
我使用的是赫蹏(hètí)来进行标点压缩与中英文间距调整,参考:
因为直接改的主题,因此要改的东西多了点,可以参看 commit 了解我的修改。
非原创修改
这部分的改造主要是参考其他博客的教程进行的,且不需要对 Solitude 主题做出较大的改动,因此只放教程博文链接和少量的修改。
访问统计
直接照着做就行,Solitude 主题配置方法一模一样。
侧边栏来访者卡片
直接照着做就行。
友链朋友圈
我这边使用的是 LiuShen 大佬的轻量友链朋友圈,使用 GitHub Action + Netlify 的方式进行部署。
本身没什么好说的,大部分都是直接照着部署即可,不过针对 Solitude 主题需要进行少量修改。
首先是 link.js,也就是用来生成 friend.json 的代码,我根据自己对主题的修改和主题本身的 links.yml 文件结构做了小小的修改:
1 | const YML = require("yamljs"); const fs = require("fs"); let friends = []; let data_f = YML.parse( fs.readFileSync("source/_data/links.yml") .toString() .replace(/(?<=rss:)\s*\n/g, ' ""\n') ).links; data_f.forEach((entry, index) => { let lastIndex = 3; if (index < lastIndex) { const filteredLinkList = entry.link_list.filter((i) => !("noTravel" in i && i.noTravel)); friends = friends.concat(filteredLinkList); } }); // ... |
还有就是 Netlify 部署静态网页所需的一点小改动。因为我博客是部署在 GitHub Page 上的,所以需要配置一下跨域。在 main 分支的 static 目录下新建 netlify.toml 文件,里面写上
1 | [[headers]] for = "/*" [headers.values] access-control-allow-origin = "https://blog.hpcesia.com" # 改成你自己的博客地址 |
同时修改 .github/workflow/friend_circle_lite.yml,commit 中加上 netlify.toml:
1 | - name: Commit changes run: | mkdir pages cp -r main ./static/netlify.toml ./static/index.html ./static/readme.md ./static/favicon.ico ./static/bg-light.webp ./static/bg-dark.webp all.json errors.json pages/ cd pages |
然后再运行 Action 即可。
Umami 统计
Umami 是一个可以自部署的开源网站统计工具,可以代替 51LA 进行网站统计。
我自己选择的部署方式是 Vercel + Supabase + Cloudflare Worker,全免费部署。首先需要将 Umami 部署到 Vercel 上,这里我参考的是一篇英文的保姆级教程:
然后需要利用 Cloudflare Worker 来将 Umami 的 API 暴露出来,这里我参考的是梦爱吃鱼大佬的文章:
最后是在 Solitude 主题中接入,翻看源码可以发现关于页面的统计卡片事实上是实现了 Umami 统计的接入的(但是主题文档压根没提),于是我们只需要修改一下 about.yml 即可:
1 | tj: provider: custom url: https://umami-api.hpcesia.com/ # 换成你自己部署的 API 入口 img: https://7.isyangs.cn/1/65eb2e9109826-1.png |
底栏图片
我参考的是下面这篇文章的修改方法:
因为我不准备放小动物(目前放的是七七探头.webp
),因此稍微修改了一下,你可以直接看 commit 来查看我的修改方式。
Memos 说说接入
参考的是:
根据 0.22 版本 Memos 的 API 变化和 Solitude 主题的更新,做了一些修改,篇幅较长,单独开了一篇文章: