强曰为道
与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

Jekyll 静态站点完全教程 / 第5章:Liquid 模板语言

第5章:Liquid 模板语言

5.1 Liquid 简介

Liquid 是由 Shopify 开发的模板语言(Template Language),Jekyll 将其作为默认模板引擎。它的设计哲学是安全简洁——不允许执行任意 Ruby 代码,适合在不受信任的环境中运行。

Liquid 三种基本标记

标记语法用途
输出标记{{ ... }}输出变量或表达式
标签标记{% ... %}执行逻辑(循环、条件等)
注释标记{% comment %}...{% endcomment %}模板注释
<!-- 输出标记:渲染变量 -->
<h1>{{ page.title }}</h1>

<!-- 标签标记:执行逻辑 -->
{% if page.published %}
  <p>This post is published.</p>
{% endif %}

<!-- 注释:不会出现在输出中 -->
{% comment %}
  This is a comment, not rendered.
{% endcomment %}

5.2 变量

变量赋值

{% assign my_name = "Jekyll" %}
{% assign count = 10 %}
{% assign is_active = true %}
{% assign fruits = "apple,banana,cherry" | split: "," %}

<p>Hello, {{ my_name }}!</p>
<p>Count: {{ count }}</p>

变量作用域

{% assign outer = "I'm outer" %}

{% if true %}
  {% assign inner = "I'm inner" %}
  {{ outer }}   <!-- ✅ 可访问 -->
  {{ inner }}   <!-- ✅ 可访问 -->
{% endif %}

{{ outer }}     <!-- ✅ 可访问 -->
{{ inner }}     <!-- ✅ 可访问(assign 无块级作用域) -->

注意事项:Liquid 的 assign 没有块级作用域,变量在整个模板中都可访问。

全局对象

对象说明示例
site_config.yml 中的所有配置{{ site.title }}
page当前页面的 Front Matter{{ page.title }}
layout布局变量{{ layout.name }}
content布局中的页面内容{{ content }}
paginator分页数据{{ paginator.total_pages }}
<!-- site 对象:访问 _config.yml -->
{{ site.title }}
{{ site.description }}
{{ site.url }}
{{ site.posts.size }}
{{ site.pages | size }}

<!-- page 对象:访问当前页面 Front Matter -->
{{ page.title }}
{{ page.url }}
{{ page.date | date: "%Y-%m-%d" }}
{{ page.content }}

<!-- content 对象:在布局中使用 -->
<!-- _layouts/default.html -->
<html>
<body>
  {{ content }}
</body>
</html>

5.3 过滤器(Filters)

过滤器用于修改变量的输出,使用管道符号 | 连接,支持链式调用。

字符串过滤器

过滤器说明示例输出
append追加字符串{{ "hello" | append: " world" }}hello world
prepend前置字符串{{ "world" | prepend: "hello " }}hello world
capitalize首字母大写{{ "hello" | capitalize }}Hello
downcase转小写{{ "HELLO" | downcase }}hello
upcase转大写{{ "hello" | upcase }}HELLO
strip去除首尾空白{{ " hello " | strip }}hello
lstrip去除左侧空白{{ " hello" | lstrip }}hello
rstrip去除右侧空白{{ "hello " | rstrip }}hello
strip_html去除 HTML 标签{{ "<p>hi</p>" | strip_html }}hi
strip_newlines去除换行符{{ "a\nb" | strip_newlines }}ab
newline_to_br换行转 <br>{{ "a\nb" | newline_to_br }}a<br>\nb
replace替换字符串{{ "hello" | replace: "l", "L" }}heLLo
replace_first替换首次匹配{{ "hello" | replace_first: "l", "L" }}HeLlo
remove删除匹配{{ "hello" | remove: "l" }}heo
remove_first删除首次匹配{{ "hello" | remove_first: "l" }}helo
truncate截断字符串{{ "hello world" | truncate: 5 }}he...
truncatewords按词截断{{ "hello beautiful world" | truncatewords: 1 }}hello...
split分割字符串{{ "a,b,c" | split: "," }}["a","b","c"]
size获取长度{{ "hello" | size }}5

数字过滤器

过滤器说明示例输出
abs绝对值{{ -5 | abs }}5
ceil向上取整{{ 4.3 | ceil }}5
floor向下取整{{ 4.7 | floor }}4
round四舍五入{{ 4.5 | round }}5
plus{{ 5 | plus: 3 }}8
minus{{ 5 | minus: 3 }}2
times{{ 5 | times: 3 }}15
divided_by{{ 10 | divided_by: 3 }}3
modulo取余{{ 10 | modulo: 3 }}1
at_least最小值{{ 5 | at_least: 10 }}10
at_most最大值{{ 5 | at_most: 3 }}3

数组过滤器

过滤器说明示例
first第一个元素{{ fruits | first }}
last最后一个元素{{ fruits | last }}
join连接为字符串{{ fruits | join: ", " }}
sort排序{{ fruits | sort }}
sort_natural自然排序(不区分大小写){{ fruits | sort_natural }}
reverse反转{{ fruits | reverse }}
uniq去重{{ fruits | uniq }}
map提取指定属性{{ site.posts | map: "title" }}
where筛选{{ site.posts | where: "author", "张三" }}
where_exp表达式筛选{{ site.posts | where_exp: "post", "post.date > '2025-01-01'" }}
group_by分组{{ site.posts | group_by: "category" }}
sort_natural自然排序{{ array | sort_natural }}
compact移除 nil{{ array | compact }}
concat合并数组{{ a | concat: b }}
sample随机取一个{{ array | sample }}
slice切片{{ "hello" | slice: 0, 3 }}hel

日期过滤器

<!-- 格式化日期 -->
{{ page.date | date: "%Y-%m-%d" }}
<!-- 输出: 2025-01-15 -->

{{ page.date | date: "%Y年%m月%d日 %H:%M" }}
<!-- 输出: 2025年01月15日 10:30 -->

<!-- 常用日期格式 -->
<!-- %Y = 2025  %y = 25  %m = 01  %d = 15 -->
<!-- %H = 10   %M = 30  %S = 00  %p = AM/PM -->
<!-- %B = January  %b = Jan  %A = Wednesday  %a = Wed -->

URL 过滤器

<!-- relative_url:添加 baseurl 前缀 -->
{{ '/assets/css/style.css' | relative_url }}
<!-- 输出: /assets/css/style.css(baseurl 为空时) -->

<!-- absolute_url:生成完整 URL -->
{{ '/about/' | absolute_url }}
<!-- 输出: https://example.com/about/ -->

<!-- slugify:URL 友好化 -->
{{ "Hello World!" | slugify }}
<!-- 输出: hello-world -->

链式调用

<!-- 多个过滤器链式调用 -->
{{ page.content | strip_html | truncatewords: 50 }}

<!-- 复杂链式示例 -->
{% assign sorted_posts = site.posts
  | where: "published", true
  | sort: "date"
  | reverse
  | limit: 5 %}

{% for post in sorted_posts %}
  <li>{{ post.title }}</li>
{% endfor %}

5.4 标签(Tags)

控制流标签

<!-- if / elsif / else -->
{% if page.author == "张三" %}
  <p>作者:张三</p>
{% elsif page.author == "李四" %}
  <p>作者:李四</p>
{% else %}
  <p>作者:匿名</p>
{% endif %}

<!-- unless(条件为 false 时执行) -->
{% unless page.comments == false %}
  <div class="comments">评论区</div>
{% endunless %}

<!-- 等价于 -->
{% if page.comments != false %}
  <div class="comments">评论区</div>
{% endif %}

<!-- case / when -->
{% assign handle = "cake" %}
{% case handle %}
  {% when "cake" %}
    <p>This is a cake</p>
  {% when "cookie" %}
    <p>This is a cookie</p>
  {% else %}
    <p>This is something else</p>
{% endcase %}

比较运算符

运算符说明示例
==等于if page.author == "张三"
!=不等于if page.draft != true
>大于if post.date > site.start_date
<小于if post.date < site.end_date
>=大于等于if site.posts.size >= 10
<=小于等于if page.weight <= 5
contains包含(字符串/数组)if post.tags contains "jekyll"
and逻辑与if a and b
or逻辑或if a or b

循环标签

<!-- for 循环 -->
{% for post in site.posts %}
  <article>
    <h2><a href="{{ post.url | relative_url }}">{{ post.title }}</a></h2>
    <time>{{ post.date | date: "%Y-%m-%d" }}</time>
  </article>
{% endfor %}

<!-- for 循环内置变量 -->
{% for item in array %}
  {{ forloop.index }}      <!-- 当前迭代次数(从1开始) -->
  {{ forloop.index0 }}     <!-- 当前迭代次数(从0开始) -->
  {{ forloop.first }}      <!-- 是否第一次迭代 -->
  {{ forloop.last }}       <!-- 是否最后一次迭代 -->
  {{ forloop.length }}     <!-- 数组总长度 -->
  {{ forloop.rindex }}     <!-- 反向索引(到1结束) -->
  {{ forloop.rindex0 }}    <!-- 反向索引(到0结束) -->
{% endfor %}

<!-- limit 和 offset -->
{% for post in site.posts limit: 5 offset: 2 %}
  <p>{{ post.title }}</p>
{% endfor %}

<!-- reversed:反转循环 -->
{% for post in site.posts reversed %}
  <p>{{ post.title }}</p>
{% endfor %}

<!-- range 循环 -->
{% for i in (1..5) %}
  <span>{{ i }}</span>
{% endfor %}

<!-- 数组循环 -->
{% assign fruits = "apple,banana,cherry" | split: "," %}
{% for fruit in fruits %}
  <li>{{ fruit | capitalize }}</li>
{% endfor %}

<!-- 哈希循环 -->
{% for item in page.social_links %}
  <a href="{{ item[1] }}">{{ item[0] }}</a>
{% endfor %}

<!-- 循环控制 -->
{% for post in site.posts %}
  {% if post.draft %}
    {% continue %}   <!-- 跳过本次迭代 -->
  {% endif %}
  <p>{{ post.title }}</p>
{% endfor %}

<!-- empty 分支 -->
{% for post in site.posts %}
  <p>{{ post.title }}</p>
{% empty %}
  <p>暂无文章</p>
{% endfor %}

其他常用标签

<!-- assign:赋值 -->
{% assign greeting = "Hello" %}
{% assign total = 10 | plus: 5 %}

<!-- capture:捕获多行内容到变量 -->
{% capture sidebar %}
  <aside>
    <h3>侧边栏</h3>
    <p>自动生成的内容</p>
  </aside>
{% endcapture %}

<div class="layout">
  <main>{{ content }}</main>
  {{ sidebar }}
</div>

<!-- include:包含其他文件 -->
{% include header.html %}
{% include nav.html active="home" %}
{% include image.html src="/images/photo.jpg" alt="Photo" %}

<!-- raw:禁止 Liquid 解析 -->
{% raw %}
  {{ this will not be parsed }}
  {% neither will this %}
{% endraw %}

<!-- comment:模板注释 -->
{% comment %}
  这段内容不会出现在输出中
{% endcomment %}

<!-- increment / decrement:计数器 -->
{% increment counter %}  <!-- 输出 0 -->
{% increment counter %}  <!-- 输出 1 -->
{% increment counter %}  <!-- 输出 2 -->

<!-- render:Jekyll 4.0+ 渲染组件 -->
{% render "card.html" title: post.title url: post.url %}

5.5 高级过滤器技巧

where 过滤器深度用法

<!-- 按属性筛选 -->
{% assign published_posts = site.posts | where: "published", true %}
{% assign featured = site.posts | where: "featured", true %}

<!-- where_exp 表达式筛选 -->
{% assign recent = site.posts | where_exp: "post", "post.date > '2025-01-01'" %}
{% assign long_posts = site.posts | where_exp: "p", "p.content.size > 5000" %}

<!-- 组合使用 -->
{% assign recent_featured = site.posts
  | where: "featured", true
  | where_exp: "p", "p.date > '2024-01-01'"
  | sort: "date"
  | reverse
  | limit: 3 %}

group_by 过滤器

<!-- 按分类分组 -->
{% assign posts_by_category = site.posts | group_by: "category" %}
{% for category in posts_by_category %}
  <h2>{{ category.name }} ({{ category.size }})</h2>
  <ul>
    {% for post in category.items %}
      <li>{{ post.title }}</li>
    {% endfor %}
  </ul>
{% endfor %}

map 过滤器

<!-- 提取所有文章标题 -->
{% assign titles = site.posts | map: "title" %}
{{ titles | join: " | " }}

<!-- 嵌套属性 -->
{% assign avatars = site.data.authors | map: "avatar" %}

<!-- 与 uniq 组合 -->
{% assign all_tags = site.posts | map: "tags" | uniq | sort %}

5.6 自定义过滤器

当内置过滤器不能满足需求时,可以创建自定义过滤器。

创建自定义过滤器

# _plugins/custom_filters.rb

module Jekyll
  module CustomFilters
    # 阅读时间估算(中文按每分钟 300 字计算)
    def reading_time(input)
      words = input.gsub(/<[^>]*>/, '').gsub(/\s+/, '').length
      minutes = (words / 300.0).ceil
      "#{minutes} 分钟"
    end

    # 中文日期格式
    def chinese_date(input)
      date = input.is_a?(String) ? Time.parse(input) : input
      months = %w[一月 二月 三月 四月 五月 六月 七月 八月 九月 十月 十一月 十二月]
      "#{date.year}#{months[date.month - 1]}#{date.day}日"
    end

    # 标签生成云
    def tag_cloud(tags)
      tags.map { |tag| "<span class='tag'>#{tag}</span>" }.join(" ")
    end

    # Markdown 转纯文本摘要
    def plain_excerpt(input, max_length = 200)
      text = input.gsub(/<\/?[^>]*>/, '')
        .gsub(/\{[^}]*\}/, '')
        .gsub(/#+\s/, '')
        .gsub(/\*+/, '')
        .strip
      text.length > max_length ? "#{text[0...max_length]}..." : text
    end

    # 数字转中文
    def number_to_chinese(input)
      numbers = %w[零 一 二 三 四 五 六 七 八 九]
      input.to_s.chars.map { |c| numbers[c.to_i] }.join
    end
  end
end

Liquid::Template.register_filter(Jekyll::CustomFilters)

使用自定义过滤器

<!-- 在模板中使用 -->
<span>阅读时间:{{ content | reading_time }}</span>
<time>{{ page.date | chinese_date }}</time>
<div class="excerpt">{{ page.content | plain_excerpt: 150 }}</div>

5.7 业务场景示例

场景1:文章归档页

<!-- archive.html -->
{% assign posts_by_year = site.posts | group_by_exp: "post", "post.date | date: '%Y'" %}

{% for year in posts_by_year %}
  <h2>{{ year.name }}</h2>
  <ul class="archive-list">
    {% for post in year.items %}
      <li>
        <time>{{ post.date | date: "%m-%d" }}</time>
        <a href="{{ post.url | relative_url }}">{{ post.title }}</a>
      </li>
    {% endfor %}
  </ul>
{% endfor %}

场景2:侧边栏目录生成

<!-- _includes/toc.html -->
<nav class="table-of-contents">
  <h4>目录</h4>
  <ul>
    {% assign headings = content | split: "<h2" %}
    {% for heading in headings offset: 1 %}
      {% assign title = heading | split: "</h2>" | first | split: ">" | last %}
      {% assign id = title | slugify %}
      <li><a href="#{{ id }}">{{ title }}</a></li>
    {% endfor %}
  </ul>
</nav>

场景3:相关文章推荐

<!-- _includes/related-posts.html -->
{% assign max_related = 3 %}
{% assign min_tags_match = 1 %}
{% assign related_posts = "" | split: "" %}

{% for post in site.posts %}
  {% if post.url == page.url %}{% continue %}{% endif %}

  {% assign same_tags = 0 %}
  {% for tag in page.tags %}
    {% if post.tags contains tag %}
      {% assign same_tags = same_tags | plus: 1 %}
    {% endif %}
  {% endfor %}

  {% if same_tags >= min_tags_match %}
    {% assign related_posts = related_posts | push: post %}
  {% endif %}
{% endfor %}

{% if related_posts.size > 0 %}
  <aside class="related-posts">
    <h3>相关文章</h3>
    <ul>
      {% for post in related_posts limit: max_related %}
        <li><a href="{{ post.url | relative_url }}">{{ post.title }}</a></li>
      {% endfor %}
    </ul>
  </aside>
{% endif %}

5.8 扩展阅读


本章小结

要点说明
输出标记{{ variable }} 输出变量值
标签标记{% tag %} 执行逻辑控制
过滤器| filter 修改输出,支持链式调用
循环for/forloop 遍历数组和集合
条件if/unless/case 控制流程
自定义通过 Ruby 插件扩展过滤器和标签

下一章:布局与包含