文章

Jekyll

jekyll用了这么久了,皮肤都换了三套了,是时候系统看一下jekyll的架构了。

  1. page
  2. post
    1. 链接
    2. 目录 - site.posts
    3. tag和category - site.tags/site.categories
    4. excerpt
    5. draft
  3. Front Matter
    1. defaults
  4. collection
    1. collection variable
    2. 做个collection目录页
  5. _data
  6. Sass
  7. 静态文件
  8. 目录结构
  9. Liquid
  10. variable
  11. laytout
  12. permalink
  13. theme
  14. 感想

使用jekyll创建一个站点非常容易。

jekyll命令的更多用法可参考Command Line Usage

下面来了解一下jekyll的整体架构。

page

page是jekyll最基本的内容,其实就和在apache下访问一个普通的静态网站一样。不同的是,.md(可以包括front matter)也会被jekyll渲染为html文件

1
2
3
4
.
├── about.md    # => http://example.com/about.html
├── index.html    # => http://example.com/
└── contact.html  # => http://example.com/contact.html

如果有文件夹,访问路径要带上文件夹,和静态网站一样:

1
2
3
4
5
6
.
├── about.md          # => http://example.com/about.html
├── documentation     # folder containing pages
│   └── doc1.md       # => http://example.com/documentation/doc1.html
├── design            # folder containing pages
│   └── draft.md      # => http://example.com/design/draft.html

使用permalinks可以在jekyll渲染时改变文件的路径。

post

jekyll天然对博客提供支持,所以提供了post layout,只要把博客内容放在_posts文件夹下即可。同样,可以是markdown,也可以是html。当然,我们一般用markdown,渲染的事情交给jekyll。

post文件要求名称符合下面的格式:

1
YEAR-MONTH-DAY-title.MARKUP

比如:

1
2
2011-12-31-new-years-eve-is-awesome.md
2012-09-12-how-to-write-a-blog.md

post文件必须带有front matter,一般主要是用来指定一个layout,比如:

1
2
3
4
5
6
7
8
9
10
---
layout: post
title:  "Welcome to Jekyll!"
---

# Welcome

**Hello world**, this is my first Jekyll blog post.

I hope you like it!

_posts文件夹下,默认的layout就是post。也可以在config里手动配置默认layout。

链接

可以使用post_url做文档之间的站内引用,这样就不必考虑文档的permalink发生变动导致超链接失效。

图片、pdf等内容的超链接直接用markdown语法即可,绝对路径的root代表本工程的文件夹,而非整个linux系统的root。相对路径的起点也是jekyll root:

1
2
3
4
... which is shown in the screenshot below:
![My helpful screenshot](/assets/screenshot.jpg)

... you can [get the PDF](/assets/mydoc.pdf) directly.

目录 - site.posts

目录就是所有博客的集合。所以可以用下面的模板创建一个目录页面:

1
2
3
4
5
6
7
8
9
<ul>
  {% for post in site.posts %}
    <li>
      <a href="{{ post.url }}">{{ post.title }}</a>
    </li>
  {% endfor %}
</ul>

tag和category - site.tags/site.categories

支持博客自然支持tag和category,他们可以定义在front matter里。

同样可以用类似的方法创建一个tag集合的页面:

1
2
3
4
5
6
7
8
9
10
{% for tag in site.tags %}
  <h3>{{ tag[0] }}</h3>
  <ul>
    {% for post in tag[1] %}
      <li><a href="{{ post.url }}">{{ post.title }}</a></li>
    {% endfor %}
  </ul>
{% endfor %}

除了在front matter里定义,_posts文件夹前的文件夹默认也会被jekyll作为category:

1
movies/horror/_posts/2019-05-21-bride-of-chucky.markdown

这个post就会拥有movies和horror两个category。

和tag不同的是,category会影响post的路径默认的permalink是permalink: /:categories/:year/:month/:day/:title:output_ext,包含category。如果上述例子里的front matter里又定义了categories: classic hollywood,则这篇文章会有四个category,最终渲染后的html路径为:

  • movies/horror/classic/hollywood/2019/05/21/bride-of-chucky.html

excerpt

jekyll的post也支持博客里常见的摘要,默认就是post文章的第一段。不过也可以使用excerpt_separator自定义分隔符,可以放在front matter里,也可以放在config里:

1
2
3
4
5
6
7
8
9
---
excerpt_separator: <!--more-->
---

Excerpt with multiple paragraphs

Here's another paragraph in the excerpt.
<!--more-->
Out-of-excerpt

因此可以用下面的代码渲染出带摘要的目录:

1
2
3
4
5
6
7
8
9
10
<ul>
  {% for post in site.posts %}
    <li>
      <a href="{{ post.url }}">{{ post.title }}</a>
      {{ post.excerpt }}
    </li>
  {% endfor %}
</ul>

draft

没写完的post可以暂存为草稿,放到_drafts下。文件不需要带date,jekyll也不会把它渲染出来:

1
2
3
4
.
├── _drafts
│   └── a-draft-post.md
...

想预览草稿的话,可以使用带--drafts参数的jekyll servejekyll build

文件的修改时间默认就是它的date。

Front Matter

所有带前言(YAML格式)的文件都会被jekyll处理。

前言里可以放预定义/自定义的变量,然后在正文中使用liquid标签访问:

1
2
3
4
5
6
7
8
9
---
layout: post
title: Blogging Like a Hacker
name: puppylpg
---

<h1>{{ page.name | downcase }}</h1>

所有定义在页头里的变量,在Liquid中都可以使用page变量访问,比如上面定义的name的访问方式就是{{ page.name }}

jekyll的page或post有一些预定义好的变量,可以直接在前言里用:

  • layout:使用哪个layout渲染本文件。layout模板文件都放在_layouts下;
  • permalink:自定义本文的url;
  • published:设为false后就不会发表,不会生成为一个html页面;

post还有一些独有的预定义变量

  • date:YYYY-MM-DD HH:MM:SS +/-TTTT,hours, minutes, seconds, and timezone offset are optional
  • categories
  • tags

defaults

如果有些前言里的变量经常出现,可以使用defaults配置在config里,有需要的时候再override即可。

比如,为本项目下所有的posts类型的文件设置默认的变量layout: "default"

1
2
3
4
5
6
7
8
defaults:
  -
    scope:
      path: "" # an empty string here means all files in the project
      type: "posts" # previously `post` in Jekyll 2.2.
    values:
      layout: "default"
      author: "puppylpg"

collection

除了jekyll默认创建的pages/posts/drafts三个集合,我们也可以自己创建集合。

自定义一个books集合,自定义permalink,并设置默认的layout为page:

1
2
3
4
5
6
7
8
9
10
11
12
13
collections:
  # custom collection type
  books:
    output: true
    permalink: /:collection/:path

defaults:
  - scope:
      path: _books
      type: books
    values:
      layout: page
      comments: true

必须设置output: true,jekyll才会渲染这个集合里的文件

从3.7.0起,也可以设置一个collections_dir: my_collections,然后它下面的每一个文件夹都是collection,比如my_collections/_booksmy_collections/_recipes,直接就创建出了books和recipes两个collection。与此同时,_posts等预定义的collection所在的文件,也要放到这个文件夹下面。

默认情况下,posts的遍历顺序是date变量的倒序,其他collection是date的升序(如果没有,就是按他们的path排序)。也可以使用sort_by自定义排序条件

比如自定义一个tabs集合,按照front matter里定义的order排序:

1
2
3
4
collections:
  tabs:
    output: true
    sort_by: order

然后给_tabs下的文件都制定order,比如:

1
2
3
4
5
6
7
8
---
# the default layout is 'page'
icon: fas fa-info-circle
order: 4
---

> 读书破万卷,下笔无有神,但一样快乐!
{: .prompt-tip }

最后做一个页面,把tabs都渲染出来,site.tabs直接就是按照order升序排列了。

可以自定义一个sidebar,在里面遍历所有的site.tabs

1
2
3
4
5
6
7
8
9
10
11
12
13
      {% for tab in site.tabs %}
        <li class="nav-item{% if tab.url == page.url %}{{ " active" }}{% endif %}">
          <a href="{{ tab.url | relative_url }}" class="nav-link">
            <i class="fa-fw {{ tab.icon }}"></i>
            {% capture tab_name %}{{ tab.url | split: '/' }}{% endcapture %}

            <span>{{ site.data.locales[include.lang].tabs.[tab_name] | default: tab.title | upcase }}</span>
          </a>
        </li>
        <!-- .nav-item -->
      {% endfor %}

collection variable

collection所有的属性都可以在这里获得,posts是jekyll硬编码的collection,所以遍历collection的时候一定会有posts。

collection的docs属性返回的是每一篇文档数组,文档有这些属性,比如title/content/url等,可以方便地用于渲染文档的内容。

做个collection目录页

使用刚学的collection知识练练手——

既然定义了多个collection,现在想为每个collection做个目录页。也就是在books页展示books colleciton的所有条目;在life页展示life collection的所有条目。最重要的一步就是获取该页面对应的collection的所有目录。怎么确定该页面对应的collection?有多种办法,比如在该页面的markdown文件的Front Matter里设置一个变量,标记这个页面所属的collection。或者既然_tabs下的这些文件的文件名本身就对应一个collection,那用文件名标记collection也是不错的办法。

1
2
3
4
5
6
7
  {% for collection in site.collections %}
    {% assign basename = page.name | split: '.' | first %}
    {% if basename == collection.label %}
      {% assign sorted = collection.docs | sort: 'date' | reverse %}
      {% for collect_doc in sorted %}

  1. 首先使用site.collections获取所有collection;
  2. 然后获取该页面对应的文件名,我们的文件名和collection名称是对应的;
  3. 接着使用collection.label获取当前遍历到的collection的名字,看是否和文件名对应;
  4. 如果对应,那这就是我们要渲染的collection,遍历它的collection.docs即可。注意默认是按照date正序,需要改成倒序。

_data

除了jekyll内置的变量、front matter里定义的变量,还可以用_data来自定义变量所有放在_data下面的yaml/json/csv/tsv文件都可以作为变量文件

比如:

1
2
3
4
5
6
7
8
- name: Eric Mill
  github: konklone

- name: Parker Moore
  github: parkr

- name: Liu Fengyun
  github: liufengyun

就可以通过site.data.members访问每一个人了:

1
2
3
4
5
6
7
8
9
10
11
<ul>
{% for member in site.data.members %}
  <li>
    <a href="https://github.com/{{ member.github }}">
      {{ member.name }}
    </a>
  </li>
{% endfor %}
</ul>

在上述sidebar渲染tabs collection的例子中,_data结构如下:

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
$ tree _data
_data
├── locales
│   ├── ar.yml
│   ├── bg-BG.yml
│   ├── cs-CZ.yml
│   ├── de-DE.yml
│   ├── el-GR.yml
│   ├── en.yml
│   ├── es-ES.yml
│   ├── fi-FI.yml
│   ├── fr-FR.yml
│   ├── hu-HU.yml
│   ├── id-ID.yml
│   ├── it-IT.yml
│   ├── ko-KR.yml
│   ├── my-MM.yml
│   ├── pt-BR.yml
│   ├── ru-RU.yml
│   ├── sl-SI.yml
│   ├── sv-SE.yml
│   ├── th.yml
│   ├── tr-TR.yml
│   ├── uk-UA.yml
│   ├── vi-VN.yml
│   ├── zh-CN.yml
│   └── zh-TW.yml
└── origin
    ├── basic.yml
    └── cors.yml

zh-CN内容示例如下:

1
2
3
4
5
6
7
8
# The tabs of sidebar
tabs:
  # format: <filename_without_extension>: <value>
  home: 首页  
  categories: 分类  
  tags: 标签  
  archives: 归档  
  about: 关于

然后就可以使用变量site.data.locales[include.lang].tabs.[tab_name]获取相应include.lang对应的locale里tab_name的语言表示了:

1
2
3
4
5
6
7
8
9
10
11
12
13
      {% for tab in site.tabs %}
        <li class="nav-item{% if tab.url == page.url %}{{ " active" }}{% endif %}">
          <a href="{{ tab.url | relative_url }}" class="nav-link">
            <i class="fa-fw {{ tab.icon }}"></i>
            {% capture tab_name %}{{ tab.url | split: '/' }}{% endcapture %}

            <span>{{ site.data.locales[include.lang].tabs.[tab_name] | default: tab.title | upcase }}</span>
          </a>
        </li>
        <!-- .nav-item -->
      {% endfor %}

其中:

  1. site.data.locales来自_data
  2. include.lang来自include sidebar.html传的参数lang,就像这样: ``` $ find . -type f -exec grep “include sidebar” {} + ./_layouts/default.html:
1
```

Sass

不懂

静态文件

静态文件就是没有front matter的文件,可以通过site.static_files获取。包含这么多属性

静态文件虽然不能加front matter,但是可以在config的defaults里设置:

1
2
3
4
5
defaults:
  - scope:
      path: "assets/img"
    values:
      image: true

然后就可以使用filter把他们过滤出来:

1
2
3
4
5
6
{% assign image_files = site.static_files | where: "image", true %}
{% for myimage in image_files %}
  {{ myimage.path }}
{% endfor %}

目录结构

介绍了上面那么多概念,jekyll的目录结构基本都出来了:

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
.
├── _config.yml
├── _data
│   └── members.yml
├── _drafts
│   ├── begin-with-the-crazy-ideas.md
│   └── on-simplicity-in-technology.md
├── _includes
│   ├── footer.html
│   └── header.html
├── _layouts
│   ├── default.html
│   └── post.html
├── _posts
│   ├── 2007-10-29-why-every-programmer-should-play-nethack.md
│   └── 2009-04-26-barcamp-boston-4-roundup.md
├── _sass
│   ├── _base.scss
│   └── _layout.scss
├── _site
├── .jekyll-cache
│   └── Jekyll
│       └── Cache
│           └── [...]
├── .jekyll-metadata
└── index.html # can also be an 'index.md' with valid front matter

每一部分的详细作用可以参考这里

.开头的文件会被jekyll忽略,想加入到构建后的_site里,需要在config里显式配置include。同理,想排除的文件需要显式配置exclude,比如readme、changelog等。

Liquid

Jekyll使用Liquid模板语言引擎来处理模板。

Liquid有三个主要部分:

  • Object:object包含liquid要输出到网页上的内容,当被双括号包围时,就会输出该变量的值。比如{{ page.title }}
  • Tag:{% if page.show_sidebar %},大括号加百分号,用于逻辑控制,html里嵌入代码,相当于JSP;
  • Filter:竖线,对内容进行处理,相当于lambda里的map函数。用在两个地方
    • 输出,用双括号:{{ "hi" | capitalize }}
    • 赋值,用括号百分号:{% assign sorted = collection.docs | sort: 'date' | reverse %}

这里的tag指的是liquid的tag,即控制流、循环、模板、变量相关的tag,不是jekyll用于支持博客的tag。jekyll对liquid的tag做了扩充,引入了一些内置tag,比如includepost_url也是jekyll内置的一个tag(所以它要被大括号和百分号包围)

在代码高亮上,jekyll内置了highlight tag(基于Rouge),还支持某些行高亮:

1
2
3
4
5
6
7
{% highlight ruby linenos mark_lines="1 2" %}
def foo
  puts 'foo'
end
{% endhighlight %}

实际使用的时候,这些代码不能再被markdown的三撇号包裹,且高亮特定行并没有出现。效果如下:

>
1
2
3
def foo
  puts 'foo'
end

所以不如用markdown的高亮。

jekyll对liquid的filter也做了扩充,引入了一些和路径、日期处理等相关的filter,可以参考这里

variable

jekyll的variable分为好几类,每一类可用的属性不同

  • site
  • page
  • layout
  • theme
  • content
  • paginator

也包括之前介绍的collection和doc。

laytout

模板文件,放在_layouts下。

layout可以堆叠,jekyll称之为继承.

permalink

permalink决定了最终页面的路径。

对post来说,permalink默认是:

1
permalink: /:categories/:year/:month/:day/:title:output_ext

permalink里可用的placeholder在这里,也可以用用一些jekyll预定义好的格式,但是front matter里不支持这种写法,pretty就是普通的pretty。

但是对post和draft以外的page和collection来说,没有时间和category,所以就等价于/:title:output_ext。他们可用的placeholder和posts的是不一样的。

permalink在jekyll里的逻辑如下:

  1. 给某一个page指定一个permalink,那么只要访问这个url,就能到达这个page;
  2. 然后在我们做的页面上,渲染一个带有href的元素,使之可以抵达该页面。对于post来说,就是创建个目录页面,可以遍历site.posts,然后使用(假设变量是post)post.url获取其值,作为href的地址<a href="{{ post.url | relative_url }}">{{ post.title }}</a>

theme

对于各种theme,如果是以gem的形式安装的,我们可以在自己的工程下用同名文件覆盖theme本身的文件。

Jekyll will look first to your site’s content before looking to the theme’s defaults for any requested file in the following folders: /assets/_data/_layouts/_includes/_sass

感想

后面可以考虑搞个ci,自动部署到自己的小服务器上。

本文由作者按照 CC BY 4.0 进行授权