Jekyll Front Matter 详解:书写规则、预定义变量与源码实现
【免费下载链接】jekyll:globe_with_meridians: Jekyll is a blog-aware static site generator in Ruby项目地址: https://gitcode.com/gh_mirrors/je/jekyll
Front Matter(前置元数据)是 Jekyll 中决定每个页面、博客文章和集合文档如何被渲染的核心机制:它是一块位于文件最顶部的 YAML 数据块,可以控制布局(layout)、输出 URL(permalink)、发布状态(published)、发布日期与分类标签等。读完本文,你将掌握 Front Matter 的完整书写规则与全部预定义变量,并了解 Jekyll 源码中解析、合并这些变量的具体调用链,能够在站点开发中正确配置元数据、排查解析报错。
Front Matter 的书写规则
任何包含 YAML Front Matter 块的文件都会被视为 Jekyll 的“特殊文件”进行处理。书写上有两条硬性要求:
- Front Matter 必须是文件中的第一个内容;
- 它必须是合法的 YAML,并位于成对的三横线(
---)之间。
一个最基本的示例:
--- layout: post title: Blogging Like a Hacker ---在这两条三横线之间,既可以设置 Jekyll 预定义的变量(见后文参考表),也可以创建完全自定义的变量。这些变量在文件后半部分的 Liquid 标签、以及该页面/文章所依赖的任何布局(layout)和包含文件(include)中都可以访问。
源码视角:Front Matter 如何被识别
Jekyll 用一条固定的正则表达式识别 Front Matter,定义在 lib/jekyll/document.rb:
YAML_FRONT_MATTER_REGEXP = %r!\A(---\s*\n.*?\n?)^((---|\.\.\.)\s*$\n?)!m从这条正则可以看出三个实现细节:
\A锚定文件开头,因此 Front Matter 必须位于文件最顶部,前面不能有任何字符(包括空行);- 闭合分隔符除了
---之外,也接受...(YAML 标准的文档结束标记),这是文档未明确提及、但从源码可以确认的行为; - 匹配成功后,YAML 块内容交给受限的 YAML 解析器
SafeYAML加载,正文内容则通过Regexp.last_match.post_match被剥离出来,二者分离。
对于博客文章/集合文档,解析入口是 Document#read_content:
def read_content(**opts) self.content = File.read(path, **Utils.merged_file_read_opts(site, opts)) if content =~ YAML_FRONT_MATTER_REGEXP self.content = Regexp.last_match.post_match data_file = SafeYAML.load(Regexp.last_match(1)) merge_data!(data_file, :source => "YAML front matter") if data_file end end页面(Page)和布局(Layout)则走 Convertible#read_yaml,逻辑相同:读取文件、剥离 Front Matter、将 YAML 结果赋给data哈希。
UTF-8 编码警告
如果使用 UTF-8 编码,请确保文件中不存在 BOM(字节顺序标记)头字符,否则会对 Jekyll 造成严重问题。由于 BOM 字符位于文件开头、---之前,会直接破坏正则的\A锚定,导致 Front Matter 无法被识别。这一点在使用 Windows 编辑器编写文件时尤其需要注意,相关说明见 Windows 安装文档。
Front Matter 变量是可选的
如果你只想使用 Liquid 标签和变量 而不需要任何 Front Matter 数据,可以直接留空——一对中间没有任何内容的三横线仍然能让 Jekyll 把该文件当作可渲染文件处理。这对 CSS、RSS feed 之类的文件很有用。从源码看,Utils.has_yaml_header? 只检测文件是否以---开头(%r!\A---\s*\r?\n!),并不要求其中有任何键值对,空 Front Matter 同样成立。
解析失败的错误处理
当 YAML 语法非法时,Jekyll 的行为由strict_front_matter配置决定:
- 默认情况下,Document#handle_read_error 记录一条
Error: YAML Exception reading ...日志后继续构建,跳过该文件的问题元数据; - 页面/布局一侧同理,Convertible#read_yaml 捕获
Psych::SyntaxError并警告; - 若站点配置中设置了
strict_front_matter: true,任何 YAML 异常都会被重新抛出,构建直接失败。这是 CI 环境中提前暴露元数据错误的推荐做法。
此外,Convertible#validate_data! 还会校验 Front Matter 必须解析为 Hash(顶层是标量或列表会抛出InvalidYAMLFrontMatterError);permalink为空字符串时抛出InvalidPermalinkError。
预定义全局变量
页面或博客文章的 Front Matter 中可以使用若干预定义的全局变量:
| 变量 | 说明 |
|---|---|
layout | 指定使用的布局文件,写布局文件名(不带扩展名)。布局文件必须放在_layouts目录中。设为null表示不使用任何布局文件(若文件是 post/document 且在 Front Matter defaults 中定义了 layout,则以 defaults 为准);从 3.5.0 起,post/document 中使用none会无视 front matter defaults地跳过布局,而页面中使用none则会被当作查找一个名为 "none" 的布局。 |
permalink | 当需要让文章 URL 脱离站点全局风格(默认/year/month/day/title.html)时,设置此变量,它将作为最终 URL 使用。 |
published | 设为false时,该文章在站点生成时不会出现。 |
layout变量的底层机制
布局的查找由 LayoutReader 完成:它扫描_layouts目录(配置项layouts_dir)下的所有文件,并额外合并主题(theme)提供的布局。布局的“名字”就是去掉扩展名后的文件名(见 LayoutReader#layout_name),因此layout: post会匹配post.html或post.markdown。
layout: none的语义在源码中对应no_layout?判断:
- 博客文章/集合文档:Document#no_layout?,
place_in_layout?为!(asset_file? || yaml_file? || no_layout?); - 页面:Convertible#no_layout? 与 L253-L255。
需要注意的是,文章文档的data哈希带有default_proc(见 Document#initialize),未显式设置的键会回退到 front matter defaults;而显式写layout: none时该键真实存在,none比较直接命中,因此能覆盖 defaults 中定义的布局。null(即完全不写或显式置空)则触发 defaults 回退,这就是官方文档中两者行为差异的来源。
published变量与--unpublished预览开关
published的判定与执行分散在三处:
- Document#published?:只有当 Front Matter 明确写出
published: false时才返回 false; - Collection 读文档:
docs << doc if site.unpublished || doc.published?,即未开启预览开关时,published: false的文档根本不进入集合; - Publisher#publish?:输出阶段再次校验
data.fetch("published", true) || @site.unpublished,还叠加了“未来日期隐藏”规则。
因此要预览被标记为未发布的页面,运行jekyll serve或jekyll build时加上--unpublished开关(它把site.unpublished置为真,绕过上述两道检查)。Jekyll 还专门提供了针对博客文章草稿的 drafts 功能,适合“尚未写完”的场景。
自定义变量
你也可以设置自己的 Front Matter 变量并在 Liquid 中访问。例如定义了一个叫food的变量,就可以在页面中使用它:
--- food: Pizza --- <h1>{{ page.food }}</h1>这些自定义变量之所以能在布局与 include 中使用,是因为渲染时 Front Matter 数据会被整体注入 Liquid 的 payload。以 Convertible#to_liquid 为例,页面把自己的data(即 Front Matter 解析结果)与实例属性合并后交给 Liquid,布局链共享同一 payload,因此任意层级的布局与 include 都能读到page.food;博客文章中则通过post.food或page.food访问。test/source/_posts/2014-12-20-properties.text 等测试源文件覆盖了这类自定义属性在文章中的透传行为。
预定义变量:博客文章专用
以下变量开箱即用,专门用于博客文章(post)的 Front Matter:
| 变量 | 说明 |
|---|---|
date | 覆盖文件名中的日期。可用于保证文章排序正确。日期格式为YYYY-MM-DD HH:MM:SS +/-TTTT,其中小时、分钟、秒和时区偏移均可省略。 |
category/categories | 不通过目录组织文章时,可指定文章所属的一个或多个分类。站点生成时,文章表现得如同以常规方式设置了这些分类。categories(复数键)可以写成 YAML 列表,也可以是空格分隔的字符串。 |
tags | 与分类类似,可为文章添加一个或多个标签。同样支持 YAML 列表或空格分隔字符串两种写法。 |
date的解析与覆盖优先级
Front Matter 中的date会覆盖从文件名解析出的日期,这是保证文章按预期排序的关键。日期字符串最终由 Utils.parse_date 解析:
def parse_date(input, msg = "Input could not be parsed.") @parse_date_cache ||= {} @parse_date_cache[input] ||= Time.parse(input).localtime rescue ArgumentError raise Errors::InvalidDateError, "Invalid date '#{input}': #{msg}" end底层就是 Ruby 的Time.parse,因此YYYY-MM-DD HH:MM:SS +/-TTTT中小时、分钟、秒、时区偏移可任意省略;解析失败会抛出带文件名的InvalidDateError(在 Document#merge_date! 中被调用)。
文件名日期与 Front Matter 日期的优先级逻辑见 Document#populate_title / modify_date:文件名匹配DATE_FILENAME_MATCHER(如2010-01-09-time-override.md)时提取出日期,但如果 Front Matter 已经显式设置了date,则文件名日期不再生效。测试源文件 test/source/_posts/2010-01-09-date-override.markdown 与 test/source/_posts/2010-01-09-time-override.markdown 正是针对该覆盖行为的验证用例。
category/categories的归一化
分类的最终统一处理在 Document#populate_categories:
def populate_categories categories = Array(data["categories"]) + Utils.pluralized_array_from_hash( data, "category", "categories" ) categories.map!(&:to_s) categories.flatten! categories.uniq! merge_data!({ "categories" => categories }) end要点有三:
- 单数键
category会通过pluralized_array_from_hash合并进categories,因此单数/复数两种写法最终等价; - 分类列表会被扁平化、字符串化并去重;
- 字符串形式的分类会在合并时按空格拆分,见 Document#merge_categories!:
other["categories"].split if other["categories"].is_a?(String),这就是“空格分隔字符串”写法被支持的原因。
仓库中的 test/source/_posts/2009-01-27-array-categories.markdown、2009-01-27-category.markdown 以及带目录前缀的 test/source/_posts/es/ 等测试文件覆盖了列表、单数、混合与路径目录等多种分类形态。值得注意的一个附加行为:当文章位于_posts的子目录或带前缀的目录(如es/_posts)时,Document#categories_from_path 会把这些“超目录”自动并入分类——即目录分类与 Front Matter 分类可以叠加。
tags的处理
标签的处理逻辑与分类同构,见 Document#populate_tags:单数键tag会被合并进tags,然后扁平化后写回。空格分隔字符串在 YAML 层面即为一个字符串值,列表写法则直接是数组,两种形式最终都归一为字符串数组。
避免重复:Front Matter Defaults
如果不想在每篇文章中重复书写常用的 Front Matter 变量(预定义变量与自定义变量均适用),可以在站点配置中为它们定义 defaults,只在必要时覆盖(或不覆盖)。
从源码看,这条回退链是全局机制:Document#initialize 为data设置了default_proc,任何 Front Matter 中缺失的键都会通过site.frontmatter_defaults.find(relative_path, type, key)自动补全;页面一侧则由 Convertible#to_liquid 在渲染前用deep_merge_hashes把 defaults 合入。lib/jekyll/frontmatter_defaults.rb 负责按scope(path、type、collection 等条件)匹配具体的 default 条目,其配置语法与使用示例见上文链接的官方文档。
小结:一条 Front Matter 的完整生命周期
结合以上源码,一个包含 Front Matter 的文章从读入到输出的链路可以概括为:
- 识别与剥离:
YAML_FRONT_MATTER_REGEXP在 document.rb#L13 定位文件顶部的 YAML 块,正文与元数据分离; - 安全解析:
SafeYAML.load将 YAML 解析为受限的 Ruby 对象,存入data哈希,并与 front matter defaults 合并; - 字段归一化:
populate_title(从文件名补全 title/slug/date)、populate_categories、populate_tags依次把category/categories、tag/tags归一为列表; - 发布判定:
published?与Publisher决定是否进入输出; - 渲染:
data经to_liquid注入 Liquid payload,layout/permalink分别决定布局包裹(Convertible#render_all_layouts)与最终 URL 计算(Document#url 中permalink优先于模板)。
理解这条链路后,无论是书写 Front Matter、配置 defaults,还是排查 “YAML Exception” 报错,都能对应到具体的源码位置与测试用例(测试源文件集中在 test/source 下,如 test/source/_drafts/draft-properties.text、test/source/front_matter.erb),便于进一步深入验证。
【免费下载链接】jekyll:globe_with_meridians: Jekyll is a blog-aware static site generator in Ruby项目地址: https://gitcode.com/gh_mirrors/je/jekyll
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考