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 %}
控制流标签
<!-- 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 插件扩展过滤器和标签 |
下一章:布局与包含