Published 2022-03-28.
Last modified 2023-02-13.
Time to read: 7 minutes.
This blog post builds upon the previous post, and discusses several templates that you can use to start writing your next Jekyll plugin in Ruby. Templates are provided for custom Jekyll filters, generators, tags and block tags. These templates:
-
Are structured and implemented as Ruby gems,
which are the easiest way to work with Jekyll plugins.
It is suprisingly easy to publish Ruby gems.
You can publish to
RubyGems.org
or to a private repository. -
Provide an interactive
run_this_first
installation script, which sets up your new Jekyll plugin and a git repo for it, so you can immediately start to work with it. - For plugins that can accept parameters, the templates provide convenient parameter parsing that is really easy to work with.
-
Set up their own custom loggers as described in
this post.
Separate loggers are predefined for the tag plugins, as well as 5 loggers for the 5 major categories of Jekyll hooks:
clean
,documents
,pages
,posts
, andsite
. -
Provides Jekyll
site
,page
andmode
variables to scopes that need those variables when writing Jekyll plugins. This can dramatically reduce the amount of time developing Jekyll plugins. I have wasted oh, so many hours trying figure out how to provide a variable to a scope when working on Jekyll plugins. These templates are structured to help you avoid wasting time on undocumented or underdocumented details.
GitHub Project and RubyGem
More information is available about this plugin from its GitHub project at
github.com/mslinn/jekyll_plugin_template
.
category_index_generator.rb
Generators are only invoked once during the website build process, when all the pages have been scanned and the site structure is available for processing. It is common for generators to include code that loops through various collections of pages.
Functionally, a Jekyll generator is the same as a :site
:pre_render
hook.
The choice of whether to write a generator class, which subclasses Jekyll::Generator
,
or writing a :site
:pre_render
hook is arbitrary.
Flip a coin to decide.
Generators can create files containing web pages in any directory, and they can modify front matter and content of existing files. Generators usually log information to the console whenever a problem occurs, or progress needs to be shown. Here is the official documentation:
A generator is a subclass of
Jekyll::Generator
that defines a generate
method,
which receives an instance of Jekyll::Site
.
The return value of generate
is ignored.
Generators run after Jekyll has made an inventory of the existing content, and before the site is generated. Pages with front matter are stored as instances of
Jekyll::Page
and are available via site.pages
.
Static files become instances of
Jekyll::StaticFile
and are available via site.static_files
.
See the Variables documentation page and Jekyll::Site
for details.
# Inspired by the badly broken example on https://jekyllrb.com/docs/plugins/generators/, and completely redone so it works. module CategoryIndexGenerator # Creates an index page for each catagory, plus a main index, all within a directory called _site/categories. class CategoryGenerator < Jekyll::Generator safe true # Only generates content in development mode # rubocop:disable Style/StringConcatenation, Metrics/AbcSize def generate(site) # This plugin is disabled unless _config.yml contains an entry for category_generator_enable and the value is not false return if site.config['category_generator_enable'] return if site.config['env']['JEKYLL_ENV'] == 'production' index = Jekyll::PageWithoutAFile.new(site, site.source, 'categories', 'index.html') index.data['layout'] = 'default' index.data['title'] = 'Post Categories' index.content = '<p>' site.categories.each do |category, posts| new_page = Jekyll::PageWithoutAFile.new(site, site.source, 'categories', "#{category}.html") new_page.data['layout'] = 'default' new_page.data['title'] = "Category #{category} Posts" new_page.content = '<p>' + posts.map do |post| "<a href='#{post.url}'>#{post.data['title']}</a><br>" end.join("\n") + "</p>\n" site.pages << new_page index.content += "<a href='#{category}.html'>#{category}</a><br>\n" end index.content += '</p>' site.pages << index end # rubocop:enable Style/StringConcatenation, Metrics/AbcSize end PluginMetaLogger.instance.logger.info { "Loaded CategoryGenerator v#{JekyllPluginTemplateVersion::VERSION} plugin." } end
jekyll_filter_template.rb
Filters are the easiest type of Jekyll plugin to write.
All that is required is a module with methods in it, and to register the module with Liquid::Template.register_filter
.
All of the methods in the module become filters.
No subclassing is required.
require 'jekyll_plugin_logger' # @author Copyright 2020 {https://www.mslinn.com Michael Slinn} # Template for Jekyll filters. module JekyllFilterTemplate class << self attr_accessor :logger end self.logger = PluginMetaLogger.instance.new_logger(self, PluginMetaLogger.instance.config) # This Jekyll filter returns the URL to search Google for the contents of the input string. # @param input_string [String]. # @return [String] empty string if input_string has no contents except whitespace. # @example Use. # {{ 'joy' | my_filter_template }} => <a href='https://www.google.com/search?q=joy' target='_blank' rel='nofollow'>joy</a> def my_filter_template(input_string) # @context[Liquid::Context] is available here to look up variables defined in front matter, templates, page, etc. JekyllFilterTemplate.logger.debug do 'Defined filters are: ' + self.class # rubocop:disable Style/StringConcatenation .class_variable_get('@@global_strainer') .filter_methods.instance_variable_get('@hash') .map { |k, _v| k } .sort end input_string.strip! JekyllFilterTemplate.logger.debug "input_string=#{input_string}" if input_string.empty? '' else "<a href='https://www.google.com/search?q=#{input_string}' target='_blank' rel='nofollow'>#{input_string}</a>" end end PluginMetaLogger.instance.logger.info { "Loaded JekyllFilterTemplate v#{JekyllPluginTemplateVersion::VERSION} plugin." } end Liquid::Template.register_filter(JekyllFilterTemplate)
Output
Given this markup in an HTML file:
Search for {{ "joy" | my_filter_template }}
This is what is rendered to the web page after being passed through the filter:
Search for joy
Using module_function Properly
If you want to to define a method that can be called as a Jekyll filter, and be used by other plugins as well, then you should read this section.
Liquid only registers instance methods as filters, not singleton methods.
module_function
statements convert instance methods into singleton methods.
If you mention a filter method in a module_function
statement,
an insidious bug will be introduced into your plugin, which can be difficult to understand at first.
Adding a module_function statement into a module that defines Jekyll filters does the following:
- The filters are never called
- The unfiltered value is returned
- No warnings or errors are issued.
You might want to define a module_function
so it can be invoked from another module.
The following example was taken from my jekyll_draft
plugin:
module Jekyll # Define this method outside of the filter module so they can be invoked externally module Draft def draft?(doc) # blah blah end end module DraftFilter def is_draft(doc) Draft::draft?(doc) end Liquid::Template.register_filter(DraftFilter) end end
Tag Plugins
😁
Here is a :smiley:
emoji, created by
{% tag_template name='smiley' align='left' size='2em' %}
This is the code for the tag plugin that creates the emojis:
require 'jekyll_plugin_support' module JekyllPluginTagTemplate PLUGIN_NAME = 'tag_template'.freeze end # This Jekyll tag plugin creates an emoji of the desired size and alignment. # # @example Float Smiley emoji right, sized 3em # {% tag_template name='smile' align='right' size='5em' %} # The above results in the following HTML: # <span style="float: right; font-size: 5em;">😁</span> # # @example Defaults # {% tag_template name='smile' %} # The above results in the following HTML: # <span style="font-size: 3em;">😁</span> # # The Jekyll log level defaults to :info, which means all the Jekyll.logger statements below will not generate output. # You can control the log level when you start Jekyll. # To set the log level to :debug, write an entery into _config.yml, like this: # plugin_loggers: # MyTag: debug module JekyllTagPlugin # This class implements the Jekyll tag functionality class MyTag < JekyllSupport::JekyllTag include JekyllPluginTemplateVersion # Supported emojis (GitHub symbol, hex code) - see https://gist.github.com/rxaviers/7360908 and # https://www.quackit.com/character_sets/emoji/emoji_v3.0/unicode_emoji_v3.0_characters_all.cfm @@emojis = { 'angry' => '😠', 'boom' => '💥', # used when requested emoji is not recognized 'grin' => '😀', 'horns' => '😈', 'kiss' => '😙', 'open' => '😃', 'poop' => '💩', 'sad' => '😢', 'scream' => '😱', 'smiley' => '😁', # default emoji 'smirk' => '😏', 'two_hearts' => '💕', }.sort_by { |k, _v| [k] }.to_h # @param tag_name [String] is the name of the tag, which we already know. # @param argument_string [String] the arguments from the web page. # @param tokens [Liquid::ParseContext] tokenized command line # @return [void] def render_impl @emoji_name = @helper.parameter_specified?('name') || 'smiley' # Ignored if `list` is specified @emoji_align = @helper.parameter_specified?('align') || 'inline' # Allowable values are: inline, right or left @emoji_size = @helper.parameter_specified?('size') || '3em' @emoji_and_name = @helper.parameter_specified?('emoji_and_name') @list = @helper.parameter_specified?('list') @emoji_hex_code = @@emojis[@emoji_name] if @emoji_name || @@emojis['boom'] # variables defined in pages are stored as hash values in liquid_context # _assigned_page_variable = @liquid_context['assigned_page_variable'] @layout_hash = @page['layout'] @logger.debug do <<~HEREDOC liquid_context.scopes=#{@liquid_context.scopes} mode="#{@mode}" page attributes: #{@page.sort .reject { |k, _| REJECTED_ATTRIBUTES.include? k } .map { |k, v| "#{k}=#{v}" } .join("\n ")} HEREDOC end # Return the value of this Jekyll tag if @list list else assemble_emoji(@emoji_name, @emoji_hex_code) end end private def assemble_emoji(emoji_name, emoji_hex_code) case @emoji_align when 'inline' align = '' when 'right' align = ' float: right; margin-left: 5px;' when 'left' align = ' float: left; margin-right: 5px;' else @logger.error { "Invalid emoji alignment #{@emoji_align}" } align = '' end name = " <code>#{emoji_name}</code>" if @emoji_and_name "<span style='font-size: #{@emoji_size};#{align}'>#{emoji_hex_code}</span>#{name}" end def list items = @@emojis.map do |ename, hex_code| " <li>#{assemble_emoji(ename, hex_code)}</li>" end <<~END_RESULT <ul class='emoji_list'> #{items.join("\n ")} </ul> END_RESULT end JekyllPluginHelper.register(self, JekyllPluginTagTemplate::PLUGIN_NAME) end end
Supported emojis for this example tag plugin are:
- 😠
angry
- 💥
boom
- 😀
grin
- 😈
horns
- 😙
kiss
- 😃
open
- 💩
poop
- 😢
sad
- 😱
scream
- 😁
smiley
- 😏
smirk
- 💕
two_hearts
If you specify an emoji name that does not exist, the
undefined
emoji is shown.
Go ahead and implement more!
Block Tag Plugins
Following is the source file containing a Jekyll block tag plugin, and an explanation of the key features of the template.
require 'jekyll_plugin_support' module JekyllPluginBlockTagTemplate PLUGIN_NAME = 'block_tag_template'.freeze end # This is the module-level description. # # @example Heading for this example # Describe what this example does # {% block_tag_template 'parameter' %} # Hello, world! # {% endblock_tag_template %} # # The Jekyll log level defaults to :info, which means all the Jekyll.logger statements below will not generate output. # You can control the log level when you start Jekyll. # To set the log level to :debug, write an entery into _config.yml, like this: # plugin_loggers: # MyBlock: debug module JekyllBlockTagPlugin # This class implements the Jekyll block tag functionality class MyBlock < JekyllSupport::JekyllBlock include JekyllPluginTemplateVersion REJECTED_ATTRIBUTES = %w[content excerpt next previous].freeze # Method prescribed by the Jekyll support plugin. # @return [String] def render_impl(content) @helper.gem_file __FILE__ # This enables attribution @param1 = @helper.keys_values['param1'] # Obtain the value of parameter param1 @param2 = @helper.keys_values['param2'] @param3 = @helper.keys_values['param3'] @param4 = @helper.keys_values['param4'] @param5 = @helper.keys_values['param5'] @param_x = @helper.keys_values['not_present'] # The value of parameters that are present is nil, but displays as the empty string @logger.debug do <<~HEREDOC tag_name = '#{@helper.tag_name}' argument_string = '#{@helper.argument_string}' @param1 = '#{@param1}' @param2 = '#{@param2}' @param3 = '#{@param3}' @param4 = '#{@param4}' @param5 = '#{@param5}' @param_x = '#{@param_x}' params = #{@helper.keys_values.map { |k, v| "#{k} = #{v}" }.join("\n ")} HEREDOC end @layout_hash = @envs['layout'] @logger.debug do <<~HEREDOC mode="#{@mode}" page attributes: #{@page.sort .reject { |k, _| REJECTED_ATTRIBUTES.include? k } .map { |k, v| "#{k}=#{v}" } .join("\n ")} HEREDOC end # Compute the return value of this Jekyll tag <<~HEREDOC <p style='color: green; background-color: yellow; padding: 1em; border: solid thin grey;'> #{content} #{@param1} #{@helper.attribute if @helper.attribution} </p> HEREDOC rescue StandardError => e @logger.error { "#{self.class} died with a #{e.full_message}" } exit 3 end JekyllPluginHelper.register(self, JekyllPluginBlockTagTemplate::PLUGIN_NAME) end end
Usage
Given this markup in an HTML file:
{% block_tag_template param1="Today is a wonderful day!" %} Hello, world! {% endblock_tag_template %}
The rendered HTML from the block tag looks like this:
Hello, world! Today is a wonderful day!
Console output looks like this, when the plugin's log level is set to debug
:
DEBUG MyBlock: tag_name = 'block_tag_template' argument_string = 'param1="Today is a wonderful day!" ' @param1 = 'Today is a wonderful day!' @param_x = '' params = param1 = Today is a wonderful day! param2 = param3 = DEBUG MyBlock: mode="" page.path="_posts/2022-03-28-jekyll-plugin-template-collection.html" page.url="/blog/2022/03/28/jekyll-plugin-template-collection.html"
Output
Following is the output when starting the demo Jekyll server.
$ demo/_bin/debug -r INFO PluginMetaLogger: Loaded block_tag_template v0.1.2 plugin. Configuration file: /mnt/f/work/jekyll/my_plugins/jekyll_plugin_template/demo/_config.yml INFO PluginMetaLogger: Loaded jekyll_plugin_logger v2.1.0 plugin. Source: /mnt/f/work/jekyll/my_plugins/jekyll_plugin_template/demo Destination: /mnt/f/work/jekyll/my_plugins/jekyll_plugin_template/demo/_site Incremental build: enabled Generating... INFO PluginMetaLogger: Loaded jekyll_plugin_logger v2.1.0 plugin. INFO PostHooks: Jekyll::Hooks.register(:posts, :post_init) invoked. INFO PostHooks: Jekyll::Hooks.register(:posts, :post_init) page: path = /mnt/f/work/jekyll/my_plugins/jekyll_plugin_template/demo/_posts/2022/2022-01-01-test.html extname = .html collection = 'posts' collection within '_posts' Directory: /mnt/f/work/jekyll/my_plugins/jekyll_plugin_template/demo/_posts Does the directory exist and is it not a symlink if in safe mode? true Collection_dir: /mnt/f/work/jekyll/my_plugins/jekyll_plugin_template/demo/_posts Metadata: {"output"=>true, "permalink"=>"/blog/:year/:month/:day/:title:output_ext"} Static files: [] Filtered entries: ["2022/2022-01-01-test.html"] type = posts content not dumped because it would likely be too long site not dumped also INFO DocumentHooks: Jekyll::Hooks.register(:documents, :post_init) invoked. INFO PageHooks: Jekyll::Hooks.register(:documents, :post_init) page: path = /mnt/f/work/jekyll/my_plugins/jekyll_plugin_template/demo/_posts/2022/2022-01-01-test.html extname = .html collection = 'posts' collection within '_posts' Directory: /mnt/f/work/jekyll/my_plugins/jekyll_plugin_template/demo/_posts Does the directory exist and is it not a symlink if in safe mode? true Collection_dir: /mnt/f/work/jekyll/my_plugins/jekyll_plugin_template/demo/_posts Metadata: {"output"=>true, "permalink"=>"/blog/:year/:month/:day/:title:output_ext"} Static files: [] Filtered entries: ["2022/2022-01-01-test.html"] type = posts content not dumped because it would likely be too long site not dumped also INFO PostHooks: Jekyll::Hooks.register(:posts, :post_init) invoked. INFO PostHooks: Jekyll::Hooks.register(:posts, :post_init) page: path = /mnt/f/work/jekyll/my_plugins/jekyll_plugin_template/demo/_drafts/2022/2022-05-01-test2.html extname = .html collection = 'posts' collection within '_posts' Directory: /mnt/f/work/jekyll/my_plugins/jekyll_plugin_template/demo/_posts Does the directory exist and is it not a symlink if in safe mode? true Collection_dir: /mnt/f/work/jekyll/my_plugins/jekyll_plugin_template/demo/_posts Metadata: {"output"=>true, "permalink"=>"/blog/:year/:month/:day/:title:output_ext"} Static files: [] Filtered entries: ["2022/2022-01-01-test.html"] type = posts content not dumped because it would likely be too long site not dumped also INFO DocumentHooks: Jekyll::Hooks.register(:documents, :post_init) invoked. INFO PageHooks: Jekyll::Hooks.register(:documents, :post_init) page: path = /mnt/f/work/jekyll/my_plugins/jekyll_plugin_template/demo/_drafts/2022/2022-05-01-test2.html extname = .html collection = 'posts' collection within '_posts' Directory: /mnt/f/work/jekyll/my_plugins/jekyll_plugin_template/demo/_posts Does the directory exist and is it not a symlink if in safe mode? true Collection_dir: /mnt/f/work/jekyll/my_plugins/jekyll_plugin_template/demo/_posts Metadata: {"output"=>true, "permalink"=>"/blog/:year/:month/:day/:title:output_ext"} Static files: [] Filtered entries: ["2022/2022-01-01-test.html"] type = posts content not dumped because it would likely be too long site not dumped also INFO PageHooks: Jekyll::Hooks.register(:pages, :post_init) invoked. INFO PageHooks: Jekyll::Hooks.register(:pages, :post_init) page at /blog/: basename = blogsByDate ext = .html name = blogsByDate.html output = pager = Is it HTML? true; is it an index? false Permalink: URL: /blog/blogsByDate.html content not dumped because it would likely be too long site not dumped also Excerpt: "" data: description = Blog posts sorted by date layout = default reading_time = false subtitle = Blog Posts, Listed Newest to Oldest title-override = Blog Posts, Listed Newest to Oldest INFO PageHooks: Jekyll::Hooks.register(:pages, :post_init) invoked. INFO PageHooks: Jekyll::Hooks.register(:pages, :post_init) page at /blog/: basename = index ext = .html name = index.html output = pager = Is it HTML? true; is it an index? true Permalink: URL: /blog/ content not dumped because it would likely be too long site not dumped also Excerpt: "" data: canonical_url = https://bogus.jekylldemo.com/blog/index.html description = Blog posts by category layout = default reading_time = false subtitle = Blog Posts by Category title-override = Blog Posts by Category INFO PageHooks: Jekyll::Hooks.register(:pages, :post_init) invoked. INFO PageHooks: Jekyll::Hooks.register(:pages, :post_init) page at /: basename = index ext = .html name = index.html output = pager = Is it HTML? true; is it an index? true Permalink: URL: / content not dumped because it would likely be too long site not dumped also Excerpt: "" data: description = Jekyll Plugin Template Demonstration layout = default subtitle = Demonstration title-override = Jekyll Plugin Template Demonstration INFO CleanHook: Jekyll::Hooks.register(:clean, :on_obsolete) invoked for []. done in 0.291 seconds. Auto-regeneration may not work on some Windows versions. Please see: https://github.com/Microsoft/BashOnWindows/issues/216 If it does not work, please upgrade Bash on Windows or run Jekyll with --no-watch. Auto-regeneration: enabled for '/mnt/f/work/jekyll/my_plugins/jekyll_plugin_template/demo' LiveReload address: http://0.0.0.0:35721 Server address: http://0.0.0.0:4444 Server running... press ctrl-c to stop.
demo/index.html
renders as:

Example Hooks
Modifying Pages Across the Entire Jekyll Site
You can modify the generated HTML for the entire Jekyll website. This is easy to do.
The very last hook that gets called before writing posts to disk is :post_render
.
We can modify the output
property of the document at the
:documents :post_render
hook to make edits to rendered web pages in collections,
regardless of whether they were originally written in Markdown or HTML:
module JekyllPluginHookExamples Jekyll::Hooks.register(:documents, :post_render) do |doc| doc.output.gsub!('Jekyll', 'Awesome') end end
To also modify web pages that are not in a collection (for example, /index.html
),
add the following into the above module JekyllPluginHooks
:
Jekyll::Hooks.register(:pages, :post_render) do |page| page.output.gsub!('Jekyll', 'Awesome') end
Notice that both of the hook invocations has duplicate code.
If we want all web pages to be modified, we can rewrite the above and extract the common code to a new method called modify_output
:
module JekyllPluginHookExamples def modify_output Proc.new do |webpage| webpage.output.gsub!('Jekyll', 'Awesome') end end module_function :modify_output Jekyll::Hooks.register(:documents, :post_render, &modify_output) Jekyll::Hooks.register(:pages, :post_render, &modify_output) end
The demo/index.html
web page now looks like the following:

If you want to translate web pages into other languages or dialects, for example
Pig Latin or
Pirate Talk, or even
spelling and grammar autocorrection,
just rewrite modify_output
to suit.
Talk Like a Pirate Translator
I could not help myself, and wrote a quick Pirate Talk translator for Jekyll sites.
require "active_support" require "active_support/inflector" require "nokogiri" require "talk_like_a_pirate" def pirate_translator proc do |webpage| html = Nokogiri.HTML(webpage.output) html.css("p").each do |node| node.content = TalkLikeAPirate.translate(node.content) end webpage.output = html end end module_function :pirate_translator Jekyll::Hooks.register(:documents, :post_render, &pirate_translator) Jekyll::Hooks.register(:pages, :post_render, &pirate_translator)
Here is the output of one of the demo web pages:
<h2>Don't Worry, Be Happy</h2> <p> If you do not worry, someone else will. That is their problem. Enjoy life, it comes at you fast. </p> <p class="copyright" id="copyright" xmlns:dct="http://purl.org/dc/terms/" xmlns:vcard="http://www.w3.org/2001/vcard-rdf/3.0#"> T' tha extent possible under law, Michael Slinn has waived all copyright n' related or neighborin' rights t' Jekyll Plugin Template Collection. This duty is published from Great North. </p> <p class="copyright" id="copyright" xmlns:dct="http://purl.org/dc/terms/" xmlns:vcard="http://www.w3.org/2001/vcard-rdf/3.0#"> <a rel="license" style="float: left; margin-right: 1em; padding-top: 9px; padding-bottom: 2em;" href="http://creativecommons.org/publicdomain/zero/1.0/"> <img src="http://i.creativecommons.org/p/zero/1.0/88x31.png" style="border-style: none;" alt="CC0" /> </a> To the extent possible under law, <a rel="dct:publisher" href="https://www.mslinn.com/blog/2022/03/28/jekyll-plugin-template-collection.html"> <span property="dct:title">Michael Slinn</span></a> has waived all copyright and related or neighboring rights to <span property="dct:title">Jekyll Plugin Template Collection</span>. This work is published from <span property="vcard:Country" datatype="dct:ISO3166" content="CA" about="https://www.mslinn.com/blog/2022/03/28/jekyll-plugin-template-collection.html"> Canada</span>.
Notice the copyright has had all the inner HTML removed by my simple translator. With more work (and more code) some of the inner HTML could be retained.
<h2>Don't Worry, Be Happy</h2> <p> If ye d' not worry, someone else will. That is their problem. Enjoy life, it comes at ye fast. </p> <p class="copyright" id="copyright" xmlns:dct="http://purl.org/dc/terms/" xmlns:vcard="http://www.w3.org/2001/vcard-rdf/3.0#"> T' tha extent possible under law, Michael Slinn has waived all copyright n' related or neighborin' rights t' Jekyll Plugin Template Collection. This duty is published from Great North. </p>
The translated HTML renders in a web browser like this:

Selecting Pages to Translate
The above pirate_translator
plugin modifies every page in the web site.
If you want to only translate certain pages, you could take advantage of the fact that page data
,
including front matter variables,
is available to all of the hooks for :documents
, :pages
, and :posts
.
Let's modify the hook so it checks for the existance of a front matter variable called pirate_talk
.
If present, and has a value, and that value is not false
,
that page will be translated into Pirate Talk, otherwise it will not be modified.
Here is the modified version:
def pirate_translator proc do |webpage| return unless webpage.data['pirate_talk'] html = Nokogiri.HTML(webpage.output) html.css("p").each do |node| node.content = TalkLikeAPirate.translate(node.content) end webpage.output = html end end
demo/_posts/2022/2022-01-01-test.html
looks like this:
--- categories: [Jekyll, Ruby] description: Test post. date: 2022-03-28 last_modified_at: 2022-04-01 layout: default title: Test Post pirate_talk: true --- <h2>Don't Worry, Be Happy</h2> <p> If you do not worry, someone else will. That is their problem. Enjoy life, it comes at you fast. </p>
Jekyll Sub-Commands
Jekyll Commands
extend the jekyll
executable with subcommands.
The official Jekyll documentation is quite brief.
Undocumented But Important
Some important details that the official Jekyll documentation does not mention:
-
Unlike the default Jekyll subcommands,
which include
jekyll build
,jekyll clean
,jekyll new
, andjekyll serve
, Jekyll subcommands defined by a plugin are only available within Jekyll projects that declare the plugin as a dependency. -
Jekyll subcommands implemented by a plugin are not
gem executable
s. Do not bother reading up on how to make a command-line program in Ruby, because Jekyll subcommands are just Ruby code that Jekyll calls for you. -
When packaged into a gem, the name of the gem is significant.
If the gem's name does not start with
jekyll-
, any Jekyll sub-command within will not be found. This means your.gemspec
file must have a line that looks like this:This is consistent with the Ruby Gem Naming Conventions, because Jekyll sub-commands of course extend the Jekyll gem.Gem::Specification.new do |spec|
spec.name = "jekyll-hello"
endUse Dashes for Extensions
If you’re adding functionality to another gem, use a dash. This usually corresponds to a/
in therequire
statement (and therefore your gem’s directory structure) and a::
in the name of your main class or module.lib/jekyll
. -
Jekyll provides support for subcommands via the Mercenary gem.
Contrary to what the Jekyll plugin documentation says,
plugin authors do not care about that implementation detail.
Do not waste your time trying to figure out how Jekyll uses Mercenary to implement subcommands.
All you need to know is that the entry point for your subcommand is a method called
init_with_program
. -
Hooks can be registered in a Jekyll command,
thereby initiating a causal chain that injects variables, such as the
site
. Jekyll::Hooks.register(:site, :post_read) do |site| end Jekyll::Hooks.register(:site, :post_read) should be the first hook that could be called after all files are read, and their front matter is parsed.
The Code
Below is the implementation of my
jekyll hello
sub-command.
Follow this pattern closely.
require_relative 'hello/version' # See https://www.mslinn.com/jekyll/10400-jekyll-plugin-template-collection.html#cmds class Hello < Jekyll::Command class << self # @param prog [Mercenary::Program] def init_with_program(prog) prog.command(:hello) do |c| c.action do |args, options| Jekyll::Hooks.register(:site, :post_read) do |_site| Jekyll.logger.info "Hello! args=#{args}; options=#{options}" # Your custom code goes here, site is available # Register another hook if you need other variables end end end end end end
Some comments about the above code:
-
class << self
opens upself’s
singleton class, so that methods can be redefined for the currentself
object (which inside a class or module body is the class or module itself). As is usually the case, this techique is used here to define class/module (“static”) methods. -
Jekyll will call your entry point (
init_with_program
) and pass in a value forprog
, which has typeMercenary::Program
. -
The name of the subcommand (
hello
) is specified as a symbol and passed toprog.command
. If this name does not match the gem suffix, the sub-command will fail.
Building and Running
Here is an example of building the gem and running the subcommand within:
$ bundle exec rake install && (cd demo; jekyll hello k tx bye) jekyll-hello 0.1.0 built to pkg/jekyll-hello-0.1.0.gem. jekyll-hello (0.1.0) installed. Hello! args=["k", "tx", "bye"]; options={}
Debugging
There are undoubtedly many ways to debug a Jekyll sub-command. Following are two ways that I use.
Here is the Visual Studio Code launch.json
that I set up for debugging this Jekyll plugin:
{ // Use IntelliSense to learn about possible attributes. // Hover to view descriptions of existing attributes. // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 "version": "0.2.0", "configurations": [ { "args": ["hello"], "cwd": "${workspaceFolder}/demo", "type": "Ruby", "name": "Debug hello sub-command", "program": "${workspaceRoot}/main.rb", "request": "launch", }, { "cwd": "${workspaceRoot}", "name": "Attach rdebug-ide", "request": "attach", "remoteHost": "localhost", "remotePort": "1234", "remoteWorkspaceRoot": "/", "showDebuggerOutput": true, "type": "Ruby", }, { "cwd": "${workspaceRoot}", "name": "Attach rdebug-ide", "request": "attach", "remoteHost": "localhost", "remotePort": "1234", "remoteWorkspaceRoot": "/", "showDebuggerOutput": true, "type": "Ruby", }, { "args": [ "-I", "${workspaceRoot}" ], "cwd": "${workspaceRoot}", "name": "RSpec - all", "program": "${workspaceRoot}/exe/rspec", "request": "launch", "showDebuggerOutput": false, "type": "Ruby", "useBundler": true, }, { "args": [ "-I", "${workspaceRoot}", "${file}" ], "cwd": "${workspaceRoot}", "name": "RSpec - active spec file only", "program": "${workspaceRoot}/exe/rspec", "request": "launch", "showDebuggerOutput": false, "type": "Ruby", "useBundler": true, } ] }
Nail down the desired Jekyll version to the exe/
subdirectory:
$ bundle binstubs jekyll --path exe
Debug Source
Use this technique when developing code.
You do not need to install the gem you are working on when using this technique.
A tiny Jekyll site is provided in the demo/
directory.
That site's Gemfile
references the gem source code in the parent directory, like this:
group :jekyll_plugins do
gem 'jekyll-hello', path: '../'
end
The bin/attach
script launches Jekyll under control of a debugger.
-
Tell the script the directory containing the Jekyll site.
$ attach demo
... lots of output ...
Fast Debugger (ruby-debug-ide 0.7.3, debase 0.2.4.1, file filtering is supported) listens on 0.0.0.0:1234 -
Set breakpoints in
lib/jekyll/hello.rb
. -
Now switch to the Visual Studio Code debugging view and launch the configuration called
Attach rdebug-ide
.
Easy!
Debug Installed Gems
Use this technique for a quick inspection of gems that have been installed.
- Create a new Visual Studio Code project.
-
Load the source from the gem into a Visual Studio Code editor pane.
(in my case the code was at
~/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/jekyll-hello/
) - Switch to the Visual Studio Code debugging view.
- Set your breakpoints in those files.
-
Examine the run configuration for
Debug hello sub-command
has the same value for theprogram
property as the source path for the gem, above. -
Launch the configuration called
Debug hello sub-command
Easy!