Jekyll Generator 插件开发完全指南:从数据注入到运行时动态生成页面
【免费下载链接】jekyll:globe_with_meridians: Jekyll is a blog-aware static site generator in Ruby项目地址: https://gitcode.com/gh_mirrors/je/jekyll
本文基于 Jekyll 官方文档中的 Generators 章节,结合源码实现,系统讲解 Jekyll 生成器(Generator)插件的工作原理与开发方法。读完本文,你将能够编写自定义 Generator 插件:在构建时向已有页面注入构建期计算的数据,或在运行时动态创建整站页面(如按分类自动生成目录页),并理解生成器在构建流水线中的执行时机、优先级与安全模式(safe mode)下的行为边界。
生成器是什么:一段在构建流水线中运行的 Ruby 代码
Jekyll 在把站点渲染成静态文件之前,需要一套机制来"基于你自己的规则创建额外内容"——这正是生成器的定位。一个生成器就是Jekyll::Generator的子类,它只需定义一个generate方法,该方法接收一个Jekyll::Site实例作为参数,generate的返回值会被忽略——所有工作都以"副作用"的形式完成:修改内存中的站点结构(页面、数据哈希等)。
从源码可以看到Jekyll::Generator本身几乎是空的:
# lib/jekyll/generator.rb module Jekyll Generator = Class.new(Plugin) endlib/jekyll/generator.rb 中它只是Jekyll::Plugin的一个空子类。真正的能力来自Plugin基类与Jekyll::Site的构建流程。
执行时机:在内容清点之后、渲染之前
官方文档明确说明:生成器在 Jekyll 完成对现有内容的清点(inventory)之后、站点生成之前运行。这一点可以直接在Jekyll::Site#process中验证:
# lib/jekyll/site.rb def process return profiler.profile_process if config["profile"] reset read # 从磁盘读取所有页面、文章、静态文件、数据 generate # 依次运行所有生成器 render cleanup write end也就是说,当你的generate(site)被调用时,站点已经完成读取:
- 带 front matter 的页面是
Jekyll::Page的实例,可通过site.pages访问; - 静态文件是
Jekyll::StaticFile的实例,可通过site.static_files访问; - 数据文件(
_data目录)可通过site.data访问; - 文章的分类与标签可通过
site.categories、site.tags访问(见 lib/jekyll/site.rb#L272-L274 中categories的实现)。
generate阶段由 lib/jekyll/site.rb#L190-L198 的Site#generate驱动,它会遍历所有已实例化的生成器并逐个调用generate(self),同时用 debug 日志记录每个生成器的耗时:
# lib/jekyll/site.rb def generate generators.each do |generator| start = Time.now generator.generate(self) Jekyll.logger.debug "Generating:", "#{generator.class} finished in #{Time.now - start} seconds." end nil end实例一:向已有页面注入构建期计算的数据
最简单的生成器用法,是把构建时算好的值注入到模板的未定义变量中。官方文档的例子中,模板reading.html有两个未定义变量ongoing和done,由生成器在构建时赋值:
module Reading class Generator < Jekyll::Generator def generate(site) book_data = site.data['books'] ongoing = book_data.select { |book| book['status'] == 'ongoing' } done = book_data.select { |book| book['status'] == 'finished' } # get template reading = site.pages.find { |page| page.name == 'reading.html'} # inject data into template reading.data['ongoing'] = ongoing reading.data['done'] = done end end end这个例子的关键机制在于:reading.data返回的就是渲染 Liquid 模板时用到的数据哈希。修改page.data等于在渲染前"改写"了页面的 front matter,模板中的{{ page.ongoing }}、{{ page.done }}因此可以在渲染阶段取到值。由于Site#process中generate位于render之前,这种注入对最终输出是生效的。
这个模式适合一切"数据在构建期即可确定、但来源不在 front matter 里"的场景:聚合_data中的多份数据、按状态过滤列表、预计算分页信息等。
实例二:运行时动态生成整站页面(分类目录页)
更复杂的用法是让生成器在运行时创建全新的页面。文档给出的目标是:为站点中每个已注册的分类创建一页,渲染该分类下所有文章的列表。这类动态页面有两条设计要点:
- 由于页面在运行时才创建,其内容、front matter 和其他属性都要由插件自己设计。因为目的是渲染某个分类下所有文档的列表,所以输出文件的 basename 取
index.html最合理; - 能够用 front matter defaults 配置这些页面会非常理想,因此给这些页面赋一个特定的
type(这里是categories)很有价值。
完整实现如下(继承自文档中的SamplePlugin示例):
module SamplePlugin class CategoryPageGenerator < Jekyll::Generator safe true def generate(site) site.categories.each do |category, posts| site.pages << CategoryPage.new(site, category, posts) end end end # Subclass of `Jekyll::Page` with custom method definitions. class CategoryPage < Jekyll::Page def initialize(site, category, posts) @site = site # the current site instance. @base = site.source # path to the source directory. @dir = category # the directory the page will reside in. # All pages have the same filename, so define attributes straight away. @basename = 'index' # filename without the extension. @ext = '.html' # the extension. @name = 'index.html' # basically @basename + @ext. # Initialize data hash with a key pointing to all posts under current category. # This allows accessing the list in a template via `page.linked_docs`. @data = { 'linked_docs' => posts } # Look up front matter defaults scoped to type `categories`, if given key # doesn't exist in the `data` hash. data.default_proc = proc do |_, key| site.frontmatter_defaults.find(relative_path, :categories, key) end end # Placeholders that are used in constructing page URL. def url_placeholders { :path => @dir, :category => @dir, :basename => basename, :output_ext => output_ext, } end end end逐段解析这个自定义 Page 子类
构造函数:Jekyll::Page实例的 URL 由若干属性拼出,动态页面没有磁盘上的源文件,因此这些属性必须手工初始化:@site是站点实例;@base指向站点源目录;@dir是页面"驻留"的目录(这里直接取分类名);@basename/@ext/@name共同决定文件名为index.html。@data哈希中预置了linked_docs键,把该分类下的所有文章塞进去,模板即可通过page.linked_docs遍历列表。
front matter defaults 的接入:data.default_proc是这段代码的精华。它给数据哈希挂了一个default_proc——当模板访问page中不存在的键时,就会回退到site.frontmatter_defaults.find(relative_path, :categories, key)。这正是让动态页面"能被_config.yml的 defaults 配置"的机制。FrontmatterDefaults#find的签名是find(path, type, setting):按path(页面相对路径)与type(这里传入:categories)过滤匹配的 defaults 集合,按作用域优先级返回对应设置的默认值。
url_placeholders的覆盖:Jekyll::Page#url_placeholders默认只提供:path、:basename、:output_ext三个占位符。自定义子类在此基础上追加了:category,使得 permalink 模板中可以写:category这样的占位符并被正确替换——这就是为什么示例的 permalink 可以写成categories/:category/。
用 front matter defaults 为生成的页面指定布局与输出路径
生成器把页面"生"出来之后,布局(layout)和输出路径(permalink)就完全交给配置文件管理了。文档给出的_config.yml配置:
# _config.yml defaults: - scope: type: categories # select all category pages values: layout: category_page permalink: categories/:category/这里scope.type: categories与生成器中frontmatter_defaults.find(relative_path, :categories, key)传入的类型符号一一对应:每个动态生成的分类页渲染时取不到自身data中的layout/permalink,就会命中这条 defaults 规则,从而统一使用category_page布局、输出到categories/:category/路径。生成器负责"造页面",配置负责"定规矩",两者解耦。
技术要点:接口、优先级与安全模式
官方文档以表格形式给出了生成器必须实现的方法——只有一个:
| 方法 | 说明 |
|---|---|
generate | 以副作用(side-effect)的形式生成内容 |
在"只实现一个方法"之外,源码层面还有两个值得了解的机制,均继承自Jekyll::Plugin:
优先级(priority)
Jekyll::Plugin定义了五级优先级 lib/jekyll/plugin.rb#L5-L11:
PRIORITIES = { :low => -10, :highest => 100, :lowest => -100, :normal => 0, :high => 10, }.freeze多个生成器共存时,实例化顺序由优先级决定。Site#instantiate_subclasses在Site#setup阶段完成生成器的筛选、排序与实例化:
# lib/jekyll/site.rb def instantiate_subclasses(klass) klass.descendants.select { |c| !safe || c.safe }.tap do |result| result.sort! result.map! { |c| c.new(config) } end end两个细节值得注意:
- 排序发生在实例化之前(
result.sort!后map!成实例),排序依据是Plugin基类中基于PRIORITIES值实现的<=>(优先级高的先排)。因此在插件中可以通过priority :high之类的类方法声明执行顺序,让"数据准备型"生成器先于"依赖其结果的"生成器运行; - 每个生成器实例化时都会收到站点配置(
c.new(config)),即你的initialize可以接收config参数读取自定义配置项。
安全模式(safe mode)与safe声明
instantiate_subclasses中的筛选条件!safe || c.safe揭示了安全模式的行为:当--safe开启(典型如托管平台构建环境)时,只有显式声明了safe true的生成器才会被实例化和执行。文档第二个示例中的safe true就是为此——声明"我只操作站点内存结构,不做文件读写等危险动作",从而允许在 safe 模式下运行。
这一筛选逻辑同样体现在插件的加载阶段:PluginManager#require_plugin_files在非 safe 模式下 glob 加载插件目录下所有.rb文件,而 gem 插件则受白名单(whitelist配置)约束(见 lib/jekyll/plugin_manager.rb#L77-L87)。
插件文件的组织与加载:_plugins目录与plugins_dir配置
文档对插件文件组织给出了两条规则:
- 单文件生成器:文件可以任意命名,但必须使用
.rb扩展名; - 跨多文件的生成器:应打包成 Ruby gem 发布(发布目标为 rubygems.org,gem 名取决于该站点的名称可用性——gem 名不可重复)。
Jekyll 默认在源目录下的_plugins目录查找插件,这个默认值定义在 lib/jekyll/configuration.rb#L13:
"plugins_dir" => "_plugins",你可以通过配置文件中的plugins_dir键覆盖默认目录。从PluginManager#plugins_path的实现看,这里有一个容易踩的坑:
def plugins_path if site.config["plugins_dir"].eql? Jekyll::Configuration::DEFAULTS["plugins_dir"] [site.in_source_dir(site.config["plugins_dir"])] else Array(site.config["plugins_dir"]).map { |d| File.expand_path(d) } end end- 当
plugins_dir保持默认值_plugins时,插件目录被解析为源目录内的相对路径(site.in_source_dir(...)); - 一旦你把
plugins_dir改成别的名字,配置值会被Array(...)展开并对每一项执行File.expand_path——即作为绝对/基于当前工作目录的路径处理,可以配置多个目录(数组形式),且不再自动相对源目录解析。
因此自定义plugins_dir时,建议写成绝对路径或明确知道其展开基准。
加载链路小结
把上述机制串起来,一次jekyll build中生成器相关的完整链路是(以 lib/jekyll/commands/build.rb 的Build.process为入口):
Build.process创建Jekyll::Site并调用process_site(site)→site.process(见 lib/jekyll/command.rb#L27-L34);Site#setup先执行plugin_manager.conscientious_require(加载主题依赖、_plugins下的.rb文件、白名单内的 gem 插件),随后self.generators = instantiate_subclasses(Jekyll::Generator)完成生成器的筛选、按优先级排序与实例化(lib/jekyll/site.rb#L128-L135);Site#read清点全部页面、文章、静态文件与数据文件;Site#generate按顺序执行每个生成器的generate(site)——此时site.pages、site.data、site.categories等均已就绪,生成器既可以改写已有页面的data,也可以向site.pages追加全新的Jekyll::Page实例;Site#render之后,注入的数据与新生成的页面随站点一起被渲染输出。
小结
- 生成器是
Jekyll::Generator的子类,只要求实现一个generate(site)方法,通过副作用修改站点内存结构,返回值被忽略; - 运行时机固定:在
Site#read(内容清点)之后、Site#render(渲染)之前,因此可安全访问site.pages、site.static_files、site.data、site.categories; - 两类典型用法:向既有页面注入构建期数据(改写
page.data),以及运行时动态创建Jekyll::Page子类实例(需自行初始化@dir/@basename/@ext/@name/@data,并覆盖url_placeholders支持自定义 permalink 占位符); - 动态页面通过
data.default_proc接入front matter defaults,用配置文件的defaults(scope.type)统一控制其布局与输出路径; - 单文件插件放在
_plugins目录(可用plugins_dir配置覆盖,注意非默认值会被File.expand_path展开),多文件插件应打包为 gem; safe true声明让插件在 safe 模式下仍会运行,priority控制多个生成器间的执行顺序。
【免费下载链接】jekyll:globe_with_meridians: Jekyll is a blog-aware static site generator in Ruby项目地址: https://gitcode.com/gh_mirrors/je/jekyll
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考