news 2026/9/18 23:57:02

Jekyll Generator 插件开发完全指南:从数据注入到运行时动态生成页面

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Jekyll Generator 插件开发完全指南:从数据注入到运行时动态生成页面

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) end

lib/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.categoriessite.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有两个未定义变量ongoingdone,由生成器在构建时赋值:

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#processgenerate位于render之前,这种注入对最终输出是生效的。

这个模式适合一切"数据在构建期即可确定、但来源不在 front matter 里"的场景:聚合_data中的多份数据、按状态过滤列表、预计算分页信息等。

实例二:运行时动态生成整站页面(分类目录页)

更复杂的用法是让生成器在运行时创建全新的页面。文档给出的目标是:为站点中每个已注册的分类创建一页,渲染该分类下所有文章的列表。这类动态页面有两条设计要点:

  1. 由于页面在运行时才创建,其内容、front matter 和其他属性都要由插件自己设计。因为目的是渲染某个分类下所有文档的列表,所以输出文件的 basename 取index.html最合理;
  2. 能够用 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_subclassesSite#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为入口):

  1. Build.process创建Jekyll::Site并调用process_site(site)site.process(见 lib/jekyll/command.rb#L27-L34);
  2. Site#setup先执行plugin_manager.conscientious_require(加载主题依赖、_plugins下的.rb文件、白名单内的 gem 插件),随后self.generators = instantiate_subclasses(Jekyll::Generator)完成生成器的筛选、按优先级排序与实例化(lib/jekyll/site.rb#L128-L135);
  3. Site#read清点全部页面、文章、静态文件与数据文件;
  4. Site#generate按顺序执行每个生成器的generate(site)——此时site.pagessite.datasite.categories等均已就绪,生成器既可以改写已有页面的data,也可以向site.pages追加全新的Jekyll::Page实例;
  5. Site#render之后,注入的数据与新生成的页面随站点一起被渲染输出。

小结

  • 生成器是Jekyll::Generator的子类,只要求实现一个generate(site)方法,通过副作用修改站点内存结构,返回值被忽略;
  • 运行时机固定:在Site#read(内容清点)之后、Site#render(渲染)之前,因此可安全访问site.pagessite.static_filessite.datasite.categories
  • 两类典型用法:向既有页面注入构建期数据(改写page.data),以及运行时动态创建Jekyll::Page子类实例(需自行初始化@dir/@basename/@ext/@name/@data,并覆盖url_placeholders支持自定义 permalink 占位符);
  • 动态页面通过data.default_proc接入front matter defaults,用配置文件的defaultsscope.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),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/18 23:56:05

SRDQN赋能多级供应链库存优化:从啤酒游戏到可部署决策

简介&#xff1a;本资源是一份面向科研人员与1–3年经验研发工程师的深度强化学习实践指南&#xff0c;聚焦供应链库存优化这一经典难题&#xff0c;以啤酒游戏为载体&#xff0c;系统复现并详解SRDQN算法在多级分散式供应链中的创新应用。资源直击牛鞭效应建模痛点&#xff0c…

作者头像 李华
网站建设 2026/9/18 23:53:34

Linux环境下IAR嵌入式工具链安装配置与命令行编译实践

很多嵌入式工程师一提IAR&#xff0c;脑子里第一反应就是Windows下的EWARM IDE。我自己干了这么多年固件开发&#xff0c;以前也是这个印象&#xff0c;直到公司开始搭CI流水线、要用Linux服务器统一出固件包&#xff0c;才不得不正视一个问题&#xff1a;IAR到底能不能在Linux…

作者头像 李华
网站建设 2026/9/18 23:53:24

将 1.5B CAD 生成放进 CI,TaoToken 作为请求出口

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/18 23:52:49

grep转义完全指南:BRE、ERE与-F模式下的正则符号处理

我最早意识到“grep转义”是个值得单独写一篇的东西&#xff0c;是因为一次特别丢人的线上操作。当时我在排查一个Nginx日志里的来源IP分布&#xff0c;想精确统计192.168.1.10这个地址出现了多少次&#xff0c;于是很自然地敲了这条命令&#xff1a;grep "192.168.1.10&q…

作者头像 李华
网站建设 2026/9/18 23:51:20

ChromeDriver 116-119 驱动安装全解:版本匹配与多平台配置

做自动化测试这行的人&#xff0c;几乎都被 ChromeDriver 的版本问题绊过一跤。前阵子帮同事看一个跑了两年的采集脚本&#xff0c;报错只有一行SessionNotCreatedException: This version of ChromeDriver only supports Chrome version 114&#xff0c;但本机 Chrome 早就自动…

作者头像 李华