文章

Jekyll 架构总览

系统梳理 Jekyll 架构:pages、posts、collections、front matter、Liquid、_data、layout、permalink、theme 与构建流程。

Jekyll 架构总览

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

相关记录:

  1. 总体架构
  2. Page
  3. Post
    1. 站内链接与资源
    2. site.posts
    3. Tags 与 Categories
    4. Excerpt
    5. Draft
  4. Front Matter
    1. Defaults
  5. Collection
    1. 做 collection 目录页
  6. _data
  7. 静态文件与 Sass
  8. 目录结构总览
  9. Liquid
  10. Variables
  11. Layout
  12. Permalink
  13. Theme
  14. 学习路径
  15. 感想

总体架构

Jekyll 的核心任务是:把源文件、模板、数据和配置合成一个静态网站。

flowchart TD
    A["内容<br/>pages/posts/collections"] --> E["Jekyll build"]
    B["元数据<br/>front matter"] --> E
    C["模板<br/>_layouts/_includes/Liquid"] --> E
    D["数据与配置<br/>_data/_config.yml"] --> E
    F["资源<br/>assets/_sass/static files"] --> E
    E --> G["_site<br/>HTML/CSS/JS/图片"]

学习 Jekyll 时,最重要的是把这些概念分层:

概念回答的问题
内容层page / post / collection / draft哪些文件会生成页面
元数据层front matter / defaults页面有哪些变量
模板层layout / include / Liquid页面怎么渲染
路径层permalink / url页面生成到哪里
数据层_data / site.data全站结构化数据从哪来
主题层theme / override默认文件从哪来,怎么覆盖

Page

page 是 Jekyll 最基础的内容。它和普通静态网站类似:Markdown 或 HTML 文件会生成页面。

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

有目录时,路径也跟目录相关:

1
2
3
4
5
.
├── documentation
│   └── doc1.md   # => /documentation/doc1.html
└── design
    └── draft.md  # => /design/draft.html

如果想改变路径,用 permalink

Post

Jekyll 天然支持博客,文章放在 _posts。文件名必须符合:

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:

1
2
3
4
5
6
7
---
title: "Welcome to Jekyll!"
---

# Welcome

Hello world.

如果 _config.yml 里给 posts 设置了默认 layout,单篇就不用重复写。

站内链接与资源

引用文章可以用 Jekyll linking tags,避免 permalink 改了以后链接失效。

图片、PDF 等静态资源可以直接用 Markdown:

1
2
![My helpful screenshot](/assets/screenshot.jpg)
[get the PDF](/assets/mydoc.pdf)

绝对路径的 / 指的是站点根路径,不是 Linux 文件系统根目录。

site.posts

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>

Tags 与 Categories

tags/categories 定义在 front matter 中,也能从路径推断。

例如:

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

这篇文章会拥有 movieshorror 两个 category。

category 会影响默认 permalink:

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

如果 front matter 又写了:

1
categories: classic hollywood

最终路径可能变成:

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

所以 categories 别乱写,它不仅是标签,还会影响路径。

Excerpt

post 支持摘要,默认取第一段。也可以定义 excerpt_separator

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

Excerpt with multiple paragraphs.

<!--more-->

Out-of-excerpt.

目录页可以输出:

1
2
3
{{ post.excerpt }}

Draft

草稿放在 _drafts,文件名不需要日期:

1
_drafts/a-draft-post.md

默认不会发布。预览草稿:

1
bundle exec jekyll serve --drafts

Front Matter

所有带 YAML front matter 的文件都会被 Jekyll 处理。

1
2
3
4
5
6
7
8

---
title: Blogging Like a Hacker
name: puppylpg
---

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

front matter 变量可以通过 page 访问。常见预定义变量:

变量作用
layout使用哪个 layout
permalink自定义 URL
published是否发布
datepost 发布时间
categoriespost 分类
tagspost 标签

Defaults

经常重复的 front matter 可以放在 _config.yml 的 defaults:

1
2
3
4
5
6
7
defaults:
  - scope:
      path: ""
      type: posts
    values:
      layout: post
      author: puppylpg

参考 Front matter defaults

Collection

除了默认的 pagespostsdrafts,也可以自定义 collection。

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

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

关键点:

  • output: true 才会生成页面。
  • 对应目录是 _books
  • site.books 可以遍历文档。
  • site.collections 会包含所有 collection,posts 是硬编码的一种。

Chirpy 的 tabs 就是 collection:

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

sort_by 可以按 front matter 字段排序。

做 collection 目录页

如果 _tabs/tutorials.md 的文件名和 collection label 一致,就可以动态找到对应 collection:

1
2
3
4
5
6
7
8
9
10
11
{% assign basename = page.name | split: '.' | first %}
{% for collection in site.collections %}
  {% if basename == collection.label %}
    {% assign sorted = collection.docs | sort: 'date' | reverse %}
    {% for collect_doc in sorted %}
      <a href="{{ collect_doc.url | relative_url }}">{{ collect_doc.title }}</a>
    {% endfor %}
  {% endif %}
{% endfor %}

flowchart LR
    A["page.name = tutorials.md"] --> B["basename = tutorials"]
    B --> C["site.collections"]
    C --> D["collection.label == tutorials"]
    D --> E["collection.docs"]
    E --> F["渲染教程列表"]

_data

_data 用来放结构化变量。所有 YAML/JSON/CSV/TSV 文件都能通过 site.data 访问。

例如 _data/members.yml

1
2
3
4
- name: Eric Mill
  github: konklone
- name: Parker Moore
  github: parkr

模板里:

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>

Chirpy 的多语言 tabs 就是 _data/locales/zh-CN.yml 这种结构:

1
2
3
4
5
6
tabs:
  home: 首页
  categories: 分类
  tags: 标签
  archives: 归档
  about: 关于

静态文件与 Sass

没有 front matter 的文件属于 static files,可以通过 site.static_files 获取。参考 Jekyll static files

还可以在 defaults 中给静态文件补变量:

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

模板中筛选:

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

Sass 资源见 Jekyll assets。当时只想写一句:不懂。但现在至少知道它属于资源构建层。

目录结构总览

典型结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
.
├── _config.yml
├── _data
│   └── members.yml
├── _drafts
├── _includes
│   ├── footer.html
│   └── header.html
├── _layouts
│   ├── default.html
│   └── post.html
├── _posts
│   └── 2009-04-26-barcamp-boston-4-roundup.md
├── _sass
├── _site
├── .jekyll-cache
├── .jekyll-metadata
└── index.md

. 开头的文件默认会被忽略。想加入构建结果,要在 _config.yml 中显式配置 include;想排除文件,用 exclude

Liquid

Jekyll 使用 Liquid 模板语言。

Liquid 有三类核心语法:

类型语法作用
Object&#123;&#123; page.title &#125;&#125;输出变量
Tag&#123;% if page.show_sidebar %&#125;控制逻辑
Filter&#123;&#123; "hi" \| capitalize &#125;&#125;转换数据

Jekyll 在 Liquid 之上扩展了一些 tag/filter,例如 includelinkpost_urlrelative_url、日期格式化等。

flowchart TD
    A["Liquid 基础语法"] --> B["Object"]
    A --> C["Tag"]
    A --> D["Filter"]
    C --> E["Jekyll 扩展<br/>include/link/post_url"]
    D --> F["Jekyll 扩展<br/>relative_url/date_to_xmlschema"]

代码高亮可以用 Jekyll 的 highlight tag:

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

不过实际写文章时,用 Markdown fenced code block 更简单。

Variables

Jekyll 的变量分好几类,参考 Jekyll variables

  • site
  • page
  • layout
  • theme
  • content
  • paginator
  • collection 和 document 对象

判断变量属于哪一类,基本就知道模板里应该从哪里取值。

Layout

layout 文件放在 _layouts。可以继承:

1
2
3
---
layout: default
---

子 layout 的内容会填入父 layout 的 content 插槽。这就是 minima 和 Chirpy 都大量使用的模板分层。

Permalink

permalink 决定最终页面路径。post 默认类似:

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

对 page 和普通 collection 来说,没有 post 那套日期和 category 语义,可用 placeholder 不同。参考 Jekyll permalinks

实践上只要记住两步:

  1. 页面根据 permalink 生成 URL。
  2. 模板用 page.urlpost.urlrelative_url 渲染链接。

Theme

Jekyll 主题如果以 gem 安装,可以被本地同路径文件覆盖。参考 Jekyll themes

Jekyll 会先找站点本地文件,再找主题默认文件。

可覆盖目录包括:

  • /assets
  • /_data
  • /_layouts
  • /_includes
  • /_sass

这非常强大,也意味着每一个本地覆盖文件都是升级主题时的手动合并债务。

学习路径

flowchart TD
    A["先理解内容类型<br/>page/post/collection"] --> B["再理解元数据<br/>front matter/defaults"]
    B --> C["再理解模板<br/>layout/include/Liquid"]
    C --> D["再理解路径<br/>permalink/url"]
    D --> E["最后理解主题覆盖与部署"]

Jekyll 不难,但概念多。把这些概念分层之后,遇到问题就能定位到是哪一层:内容没被识别、变量没传进去、模板没渲染、路径不对、还是主题覆盖没生效。

感想

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

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