今年三月份的时候因为受不了jekyll的种种问题,便花了点时间写了个自用的生成器。sushi的自定义灵活度应该要比jekyll和hugo之流要高得多,一旦配置完成,应该会十分顺手——所以就有了这篇介绍如何配置sushi的文章。

使用sushi的理由

灵活性和自定义是sushi的特色。

sushi力争做到KISS〔Keep It Simple Stupid〕,不做任何多余的事情。sushi只做一件事情,用一句话概括,就是:根据各处的配置,调用converter把你的博文转化为html格式,然后放到约定好的位置,其余的事情一概不做

这件事情十分简单,使得sushi的代码本身十分的简单。而“其余的事情”就交给其余的程序去做,sushi不会强制用户选择哪个程序来完成这些”外围“的工作,允许用户发挥想象力,进行灵活的自定义,比如:

  1. 把markdown或者其他格式翻译为html(交给pandoc,或者用户自己写一个翻译器)

  2. 主题管理

  3. 部署(如果在本地测试,可以用sfz;正式部署可以用nginx、caddy等)

  4. 统一管理图片等资源(这个工作完全是多余的)

  5. 全文检索(交给专门的全文检索软件)

但也不是说sushi什么都不做,sushi本身围绕着组织站点结构这一任务展开,对于分类〔taxonomy〕、分页〔pagination〕、套模板〔template〕也有简单但是灵活的接口。

后记中具体举了一些例子,说明为什么这些灵活性是很重要的。多个简单的程序之间分工合作,也能实现很多复杂的任务,而灵活性是这种分工的前提。

当然,如果你不需要这些灵活性,不在乎自定义性,而是需要一个包打天下、囊括所有任务的生成器,那么就没有必要使用sushi,可能你更适合WordPress或者Jekyll、Hugo。

sushi原理简介

因为sushi遵循KISS原则,所以使用sushi必须对其原理有个大致的了解。

站点结构

sushi对于站点结构的要求很少,但站点必须放在一个文件夹内(下面称为站点目录)。站点目录下必须要有_site.yml(站点配置文件),以及_converters(转换器目录)、_includes(页面片段目录)、_templates(模板目录)。sushi会自动把生成后的站点放到_gen(生成目录)中。如下:

site-example # 站点目录
├── _converters # 转换器目录
├── _gen # 生成目录
├── _includes # 页面片段目录
├── _site.yml # 站点配置
└── _templates # 模板目录

这些下划线_开头的目录和文件总体的作用,就是为sushi生成页面提供必要的信息,具体作用后面将会解释。

这些目录和文件的命名是可以修改的,sushi不强制用户使用默认的命名。如果要使用自定义命名,则需要在命令行参数或者配置文件中指明。

工作流程

首先,sushi会读取_site.yml作为站点配置文件,构建。然后紧接着读取_gen,再然后是_converters_templates_includes。读取结束后,sushi就掌握了这个站点整体有哪些配置、有哪些可用的转换器、哪些可用的模板、哪些页面片段、已经生成了哪些页面。

接下来,sushi开始遍历整个站点树,所有不以下划线_开头的文件和文件夹都会被访问一遍,访问的同时sushi用自己内部的数据结构表示这一站点树。以_下划线开头的文件和目录都会被sushi忽略。

然后进入生成阶段。sushi根据遍历得到的站点树,开始进行生成。sushi会在_gen目录中创建一个一模一样的站点树。在这个生成后的站点树中,所有的页面文件都被替换为渲染后的HTML文件或者其他的某种文件。

这里展开说明生成一个页面的过程:首先会读取它的扉页内容〔Front Matter〕,然后读取其主体部分。如果配置文件中指定了生成器,那么就调用指定的生成器(生成器是一个可执行程序,sushi会把页面的主体部分送入生成器的标准输入,然后读取其标准输出作为渲染的结果)。接着sushi把渲染后的结果,连同站点配置、站点树、扉页中的页面配置、所需的页面片段填入指定的liquid模板中,最后把最终结果输出到站点树的对应位置;如果没有指定生成器,sushi则执行默认操作,即原封不动地把文件复制到生成的站点树的对应位置。

再展开说套用模板的过程:sushi使用liquid模板语言,有关详情可以参照liquid的文档。sushi会将读取的站点配置存到site对象中,扉页的配置存到page对象中,分页器相关的内容放到paginator对象中,站点树的内容存放到sitetree对象中等等。这些对象使得模板编写变得十分灵活(虽然有时可能会显得不太优雅),具体详情可以参照README。

基本使用

开始之前先安装sushi。目前(2022.12.29)暂时建议使用cargo安装。

cargo install sushi-gen

然后,找个站点主题(或者自己做一个),比如Empty

git clone https://github.com/fpg2012/sushi-theme-empty

然后,找个合适的位置,然后创建站点,假设你的站点名字叫做Hey

ssushi init --theme ${path_to_theme} Hey

${path-to-theme}替换成你刚才下载的主题的路径。然后站点就创建完成了。

以Empty主题为例,所有posts文件夹下的内容都会被列到主页index.html中。以这个主题出发,参考文档进行自定义应该会容易很多。

外部工具

Pandoc

sushi本身不提供页面渲染的功能,它只调用你提供的渲染器。这里十分推荐使用pandoc,因为它很方便自定义。如果在可以写个shell脚本直接调用Pandoc。比如在Linux下,可以写成:

#!/bin/bash
pandoc -t html

如果引入了自定义的pandoc过滤器,那么自行添加相应的参数。然后给这个脚本可执行权限,然后在_site.yml中指定使用它。假如这个脚本叫做convert.sh,那么_site.yml中这样写可以让sushi把所有的md文件都转换成html:

convert_ext: ["md"] # 让sushi把md文件看作是需要渲染的页面文件,而非普通文件
converter_choice:
  "md": "convert.sh" # md文件选用刚才的脚本进行转换

GitHub Pages

要部署到GitHub Pages,首先你需要创建一个GitHub项目,然后启用GitHub Pages功能。然后把生成的_gen目录中的内容传上去。

如果你想要把站点的源文件和生成后的页面放在一个仓库中。可以考虑使用git worktree。比如说把源文件放在main分支中,把生成后的页面放在gh-page分支中,可以这样:

git checkout -b gh-page # 创建gh-page分支
git checkout -b main # 回到main分支
git worktree add --track - gh-page _gen

然后删除_gen中的所有内容。那么之后sushi生成的东西会自动放到gh-page分支中。详情参考worktree的用法。

Pygments

实现代码高亮。有现成的用pygments实现高亮的pandoc filter,直接使用之。

SFZ

sfz可以简化在本地部署静态页面,这对于发布前预览页面尤其有用。sushi没有类似jekyll serve的功能,这部分工作就交给sfz或者其他代劳了。下面给出了一种方法。

首先,把_site.yml复制一份,重命名为_test.yml。然后,修改_test.yml,把里面的url改成http://localhost:5000或者其他本地的地址。然后添加一项:

gen_dir: _test_gen

之后,可以用以下命令模拟jekyll serve

ssushi build -A -c _test.yml && sfz -r _test_gen

Just

sushi生成站点的命令可能会显得有点长。just可以帮助缩短这些命令。

可以创建一个justfile

all:
    ssushi build -A

inc:
    ssushi build

test:
    ssushi build -c _test.yml
    sfz -r _test_gen

test-all:
    ssushi build -A -c _test.yml
    sfz -r _test_gen

clean-gen:
    cd _gen
    rm -rf *
    cd ..

deploy-gen:
    cd _gen
    git add .
    git commit -m "Update: `date`"
    git push
    cd ..

deploy: deploy-gen
    git commit -a
    git push

之后要生成站点,直接敲just,增量生成则敲just inc

要执行类似jekyll serve的操作,直接just test-all或者just test

你可以按自己的需要修改justfile

主题编写

实际上sushi并不存在什么主题,所谓主题其实也和一般站点没有任何差别。ssushi init做的事情其实是把主题的那个文件夹原封不动地拷贝过来。

一般而言,编写主题和写一般的网站基本没有区别,主要的区别在于使用liquid的模板。关于liquid模板,可以参照其文档。

这里建议对于一般的博客,至少准备pagepostarchivehome四个模板便于用户使用,如果可以的话还可以加入rss的模板。模板之间可以有“继承关系”,比如套post模板后的内容需要再套入page模板,这些可以通过模板的扉页来实现。

而通用的Javascript脚本、页面header、footer等部分放到_includes文件夹中,用liquid的include语句包含进来。

上面提到的Empty主题是个不错的起点,可以在其基础上参照文档进行修改和扩展。

后记

我最初编写sushi其实是为了解决两个简单的问题:

  1. 在渲染页面时直接转换数学公式。

  2. 图片可以随心所欲地放在任何位置,并被任何一个markdown页面引用。并且确保在markdown编辑器(比如Typora中)可以正确地查看图片。

这两个看起来微不足道的需求,居然没有办法很好地用Jekyll或者Hugo实现。

第一条不能解决,是因为它们为了追求渲染速度和开箱即用,使用了自己的markdown渲染器,用户很难做修改,除非改源码。但如果要渲染数学公式,就非得修改markdown的渲染过程不可,到这里我就卡住了。主流解决方案是把数学公式渲染的工作甩锅给前端JavaScript,但这会带来一大堆问题;还有人修改了Hugo的源码,但这也非常麻烦。

第二条不能解决,是因为它们对站点结构做了太多的限制,连插图这点小事也要管。为什么就不能保持我站点树的结构不要做修改呢?

所以就有了sushi。渲染器自己定,那就没有什么不能渲染的了(刚好有Pandoc这个神器,自己写filter、reader、writer,什么文档都能转换);不改变站点树,那么自定义站点、使用现有工具都会变得简单。

当然sushi也有自己的问题:

  1. 为了灵活性牺牲了速度。sushi为了调用用户提供的、外部的渲染器,需要通过管道传递数据,速度掉了一大截。如果你用pandoc,并且使用Python编写的pandoc过滤器,那就更慢了。增量生成部分地解决了速度慢的问题,但目前的增量生成仍不完美,依然有待改进。

  2. 开箱即用有一定的门槛。

  3. 用的人少

当然这些缺点都是可以克服的……比如速度慢,你可以修改ssushi的源码(和Jekyll与Hugo的源码不同,sushi的源码很短),使它直接调用Rust编写的渲染器。