Mike Slinn
Mike Slinn

Nugem: Custom Rails & Jekyll Plugins

Published 2022-03-28. Last modified 2023-07-10.
Time to read: 26 minutes.

This page is part of the jekyll collection.
Early Access
The material on this web page and the GitHub project it links to are still in the development and heavy editing stages. Do not rely on anything working yet. If you are interested in watching this project evolve, please revisit this page at a later time.

This article builds upon Explanations and Examples of Jekyll Plugins and discusses a code generator that you can use to start writing your next Jekyll plugin or Ruby on Rails plugin.

Nugem

Igor Jancev originally wrote Creategem to generate Ruby on Rails gems. The project uses thor for code generation, which I wrote about in Ruby Gem Scaffold Generation With Thor. After the project had no updates for 7 years, I forked and updated it, then added the capability to generate Jekyll gems. Igor did not respond when I asked if he was interested in including my work in his gem, so I republished the new version under the name nugem.

Rails support includes Rails engine plugins, including mountable engines. Jekyll plugin support includes filters, generators, tags and block tags.

All generated plugins:

  • Are structured and implemented as Ruby gems. It is surprisingly easy to publish Ruby gems.
  • Gems are set up for publishing to RubyGems.org or to a private geminabox repository.
  • Have Rubocop and Visual Studio Code support baked in.

Generated Jekyll plugins also:

  • Set up their own custom loggers, as described in Custom Logging in Jekyll Plugins. Separate loggers are predefined for the tag plugins, as well as five loggers for the five major categories of Jekyll hooks: clean, documents, pages, posts, and site.

Generated Jekyll tags and block tags also:

  • Use jekyll_plugin_support to accept parameters, using standardized and convenient parameter parsing.
  • Provide Jekyll site, page and mode variables to scopes that need those variables. The generated code is structured to help you avoid wasting time on undocumented or under-documented details.

Installing Nugem

You could type along with the remainder of this article if you install nugem:

Shell
$ gem install nugem
Successfully installed nugem-0.8.0
Parsing documentation for nugem-0.8.0
Done installing documentation for nugem after 0 seconds
1 gem installed 

If you are using rbenv to manage Ruby instances, type:

Shell
$ rbenv rehash

Syntax

Here is the top-level help message for nugem:

Shell
$ nugem help
  nugem help [COMMAND]  # Describe available commands or one specific command
  nugem jekyll NAME     # Creates a new Jekyll plugin scaffold.
  nugem plain NAME      # Creates a new plain Ruby gem scaffold.
  nugem rails NAME      # Creates a new Rails plugin scaffold.
  [--executable], [--no-executable]  # Include an executable for the gem.
  [--host=HOST]                      # Repository host.
                                     # Default: github
                                     # Possible values: bitbucket, github
  [--private], [--no-private]        # Publish the gem on a private repository.
  [--quiet], [--no-quiet]  # Suppress detailed messages.
                           # Default: true
  [--todos], [--no-todos]  # Generate TODO: messages in generated code.
                           # Default: true 

Here is the help message for creating plain gems using nugem:

Shell
$ nugem help plain
    [--host=HOST]                      # Repository host.
  # Default: github
  # Possible values: bitbucket, github
[--private], [--no-private]        # Publish the gem in a private repository.
[--executable], [--no-executable]  # Include an executable for the gem.
[--host=HOST]                      # Repository host.
  # Default: github
  # Possible values: bitbucket, github
[--private], [--no-private]        # Publish the gem on a private repository.
[--quiet], [--no-quiet]  # Suppress detailed messages.
# Default: true
[--todos], [--no-todos]  # Generate TODO: messages in generated code.
# Default: true
Creates a new plain gem scaffold with the given NAME, by default hosted by GitHub and published on RubyGems.

Following is the help message for creating Ruby on Rails plugin gems using nugem. This article will not mention Rails again.

Shell
$ nugem help rails
  [--engine], [--no-engine]          # Create a gem containing a Rails engine.
  [--test-framework=TEST_FRAMEWORK]  # Use rspec or minitest for the test framework (default is minitest).
                                     # Default: minitest
                                     # Possible values: minitest, rspec
  [--executable], [--no-executable]  # Include an executable for the gem.
  [--host=HOST]                      # Repository host.
                                     # Default: github
                                     # Possible values: bitbucket, github
  [--mountable], [--no-mountable]    # Create a gem containing a mountable Rails engine.
  [--private], [--no-private]        # Publish the gem on a private repository.
  [--quiet], [--no-quiet]  # Suppress detailed messages.
                           # Default: true
  [--todos], [--no-todos]  # Generate TODO: messages in generated code.
                           # Default: true
  Creates a new Rails scaffold with the given NAME, by default hosted by GitHub and published on RubyGems. 

Here is the help message for creating Jekyll plugin gems using nugem:

Shell
$ nugem help jekyll
  [--block=BLOCK]                    # Specifies the name of a Jekyll block tag.
  [--blockn=BLOCKN]                  # Specifies the name of a Jekyll no-arg block tag.
  [--filter=FILTER]                  # Specifies the name of a Jekyll/Liquid filter module.
  [--generator=GENERATOR]            # Specifies a Jekyll generator.
  [--hooks=HOOKS]                    # Specifies Jekyll hooks.
  [--tag=TAG]                        # Specifies the name of a Jekyll tag.
  [--tagn=TAGN]                      # Specifies the name of a Jekyll no-arg tag.
  [--test-framework=TEST_FRAMEWORK]  # Use rspec or minitest for the test framework (default is rspec).
                                     # Default: rspec
                                     # Possible values: minitest, rspec
  [--executable], [--no-executable]  # Include an executable for the gem.
  [--host=HOST]                      # Repository host.
                                     # Default: github
                                     # Possible values: bitbucket, github
  [--private], [--no-private]        # Publish the gem on a private repository.
  [--quiet], [--no-quiet]  # Suppress detailed messages.
                           # Default: true
  [--todos], [--no-todos]  # Generate TODO: messages in generated code.
                           # Default: true
  Creates a new Jekyll plugin scaffold with the given NAME, by default hosted by GitHub and published on RubyGems. 

Generated Jekyll Plugin Code

The generated code that I added to nugem was adapted from github.com/mslinn/jekyll_plugin_template. That project also has additional Jekyll plugin code examples, some of which are shown below.

Output

Jekyll plugins are easier to write when using a template, and they are easier to manage when distributed as Ruby gems. Nugem does a good job of creating working scaffolds of Jekyll plugins, customized for your needs.

Following is a quick tour through the generated files that make up your new gem. Each type of Jekyll plugin will have a few different files; those will be described under the sections for each type of plugin, below.

.bundle/config
Configures bundler to look for Ruby development executables in the binstub directory.
README.md
This markdown file includes a badge that automatically displays the most recently published version of the gem. The version will be shown as question marks on GitHub until you publish the first version of your gem on RubyGems.org. Standard installation instructions are provided, and a placeholder is provided for you to write a description of the Jekyll tag plugin.
.git/
This is the local copy of your newly created git repository, and by default, the generated version is automatically committed.
.gitignore
This is set up for Jekyll gem development. Do not delete the .jekyll-cache/ entry, or you will introduce a huge security risk.
.rspec
Configures the rspec test facility.
.rubocop.yml
Configures the Rubocop Ruby linter.
.vscode/
Defines standard debug launch configurations, suggested VSCode extensions, extension settings, and code snippets for developing Jekyll gems.
CHANGELOG.md
You should update this file each time you release a new version of the gem.
Gemfile
This is a standard setup for developing Jekyll gems; do not define runtime dependencies in this file.
your_gem_name.gemspec
Define runtime dependencies in this file. Also update the description of your new gem here. Pay close attention to the contents of this file; it defines your gem.
LICENCE.txt
Standard MIT license terms. If you want to use another license, modify this file, and update the spec.license entry in .gemspec file to match.
Rakefile
Standard setup for Ruby gem development, you probably do not need to modify this file.
bin/
This directory contain useful scripts for Jekyll gem development: attach, console, rake, and setup. Take a look at them.
demo/
This directory contains a small but complete Jekyll website, set up to exercise your Jekyll plugin. See demo/README.md for more information.
lib/
Contains the logic for your Jekyll plugin:
  • lib/your_gem_name.rb – entry point, set up to require all the other Ruby source files in the lib/ directory. You probably do not need to modify it.
  • your_gem_name/version.rb – update the version number in this file each time you publish a new release.
  • your_plugin_name.rb – the logic for your Jekyll tag, block, filter, generator, or hook goes in this file. It is well commented.
    • nugem can create Jekyll plugins that define more than one tag, and/or block tag, and/or filter, and/or generator, and/or hooks. A Ruby source file will be created for each.
    • For Jekyll tags and block tags, the contents subclass the appropriate class from the jekyll_plugin_support gem.
spec/
This directory contains a scaffold for rspec tests for your Jekyll gem and includes some test fixtures. Jekyll filters are easy to test because they are just Ruby module methods. Setting up tests for other types of Jekyll plugins is a black art, however, because almost nothing is documented. Tags, block tags, generators and hooks can be extremely difficult to test due to the difficulty of setting up fixtures. The demo/ is provided, so you can debug your Jekyll plugins in situ.

Jekyll Filter Plugins

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.

Filters should not accept page parameters because Jekyll will recursively re-evaluate all the properties of the page, and blow up when recursing through excerpt. This means the following syntax should not be used:

Shell
{% include early_access.html %}


This article builds upon {% href match jekyll-plugin-background.html Explanations and Examples of Jekyll Plugins %} and discusses a code generator that you can use to start writing your next {% href follow https://jekyllrb.com/docs/plugins/ Jekyll plugin %} or {% href https://guides.rubyonrails.org/v3.2/plugins.html Ruby on Rails plugin %}.

Nugem

{% href https://github.com/igorj Igor Jancev %} originally wrote Creategem to generate Ruby on Rails gems. The project uses thor for code generation, which I wrote about in {% href /ruby/6700-thor.html Ruby Gem Scaffold Generation With Thor %}. After the project had no updates for 7 years, I forked and updated it, then added the capability to generate Jekyll gems. Igor did not respond when I asked if he was interested in including my work in his gem, so I republished the new version under the name {% href url="/ruby/6800-nugem.html" nugem %}.

Rails support includes Rails engine plugins, including mountable engines. Jekyll plugin support includes filters, generators, tags and block tags.

All generated plugins:

  • Are structured and implemented as {% href https://rubygems.org Ruby gems %}. It is surprisingly easy to publish Ruby gems.
  • Gems are set up for publishing to {% href RubyGems.org %} or to a private {% href https://github.com/mslinn/geminabox geminabox %} repository.
  • Have Rubocop and Visual Studio Code support baked in.

Generated Jekyll plugins also:

  • Set up their own custom loggers, as described in {% href match custom-logging-in-jekyll-plugins.html Custom Logging in Jekyll Plugins %}. Separate loggers are predefined for the tag plugins, as well as five loggers for the five major categories of Jekyll hooks: clean, documents, pages, posts, and site.

Generated Jekyll tags and block tags also:

  • Use {% href /jekyll_plugins/jekyll_plugin_support.html jekyll_plugin_support %} to accept parameters, using standardized and convenient parameter parsing.
  • Provide Jekyll site, page and mode variables to scopes that need those variables. The generated code is structured to help you avoid wasting time on undocumented or under-documented details.

Installing Nugem

You could type along with the remainder of this article if you install nugem:

{% pre copyButton shell %} {% noselect %}gem install nugem {% noselect Successfully installed nugem-0.8.0 Parsing documentation for nugem-0.8.0 Done installing documentation for nugem after 0 seconds 1 gem installed %} {% endpre %}

If you are using {% href https://github.com/rbenv/rbenv rbenv %} to manage Ruby instances, type:

{% pre copyButton shell %} {% noselect %}rbenv rehash {% endpre %}

Syntax

Here is the top-level help message for nugem:

{% pre dedent copyButton shell %} {% noselect %}nugem help {% noselect   nugem help [COMMAND] # Describe available commands or one specific command nugem jekyll NAME # Creates a new Jekyll plugin scaffold. nugem plain NAME # Creates a new plain Ruby gem scaffold. nugem rails NAME # Creates a new Rails plugin scaffold. [--executable], [--no-executable] # Include an executable for the gem. [--host=HOST] # Repository host. # Default: github # Possible values: bitbucket, github [--private], [--no-private] # Publish the gem on a private repository. [--quiet], [--no-quiet] # Suppress detailed messages. # Default: true [--todos], [--no-todos] # Generate TODO: messages in generated code. # Default: true %} {% endpre %}

Here is the help message for creating plain gems using nugem:

{% pre copyButton shell %} {% noselect %}nugem help plain {% noselect    [--host=HOST] # Repository host. # Default: github # Possible values: bitbucket, github [--private], [--no-private] # Publish the gem in a private repository. [--executable], [--no-executable] # Include an executable for the gem. [--host=HOST] # Repository host. # Default: github # Possible values: bitbucket, github [--private], [--no-private] # Publish the gem on a private repository. [--quiet], [--no-quiet] # Suppress detailed messages. # Default: true [--todos], [--no-todos] # Generate TODO: messages in generated code. # Default: true Creates a new plain gem scaffold with the given NAME, by default hosted by GitHub and published on RubyGems. %} {% endpre %}

Following is the help message for creating Ruby on Rails plugin gems using nugem. This article will not mention Rails again.

{% pre dedent copyButton shell %} {% noselect %}nugem help rails {% noselect   [--engine], [--no-engine] # Create a gem containing a Rails engine. [--test-framework=TEST_FRAMEWORK] # Use rspec or minitest for the test framework (default is minitest). # Default: minitest # Possible values: minitest, rspec [--executable], [--no-executable] # Include an executable for the gem. [--host=HOST] # Repository host. # Default: github # Possible values: bitbucket, github [--mountable], [--no-mountable] # Create a gem containing a mountable Rails engine. [--private], [--no-private] # Publish the gem on a private repository. [--quiet], [--no-quiet] # Suppress detailed messages. # Default: true [--todos], [--no-todos] # Generate TODO: messages in generated code. # Default: true Creates a new Rails scaffold with the given NAME, by default hosted by GitHub and published on RubyGems. %} {% endpre %}

Here is the help message for creating Jekyll plugin gems using nugem:

{% pre dedent copyButton shell %} {% noselect %}nugem help jekyll {% noselect   [--block=BLOCK] # Specifies the name of a Jekyll block tag. [--blockn=BLOCKN] # Specifies the name of a Jekyll no-arg block tag. [--filter=FILTER] # Specifies the name of a Jekyll/Liquid filter module. [--generator=GENERATOR] # Specifies a Jekyll generator. [--hooks=HOOKS] # Specifies Jekyll hooks. [--tag=TAG] # Specifies the name of a Jekyll tag. [--tagn=TAGN] # Specifies the name of a Jekyll no-arg tag. [--test-framework=TEST_FRAMEWORK] # Use rspec or minitest for the test framework (default is rspec). # Default: rspec # Possible values: minitest, rspec [--executable], [--no-executable] # Include an executable for the gem. [--host=HOST] # Repository host. # Default: github # Possible values: bitbucket, github [--private], [--no-private] # Publish the gem on a private repository. [--quiet], [--no-quiet] # Suppress detailed messages. # Default: true [--todos], [--no-todos] # Generate TODO: messages in generated code. # Default: true Creates a new Jekyll plugin scaffold with the given NAME, by default hosted by GitHub and published on RubyGems. %} {% endpre %}

Generated Jekyll Plugin Code

The generated code that I added to nugem was adapted from {% href wbr follow github.com/mslinn/jekyll_plugin_template %}. That project also has additional Jekyll plugin code examples, some of which are shown below.

Output

Jekyll plugins are easier to write when using a template, and they are easier to manage when distributed as Ruby gems. Nugem does a good job of creating working scaffolds of Jekyll plugins, customized for your needs.

Following is a quick tour through the generated files that make up your new gem. Each type of Jekyll plugin will have a few different files; those will be described under the sections for each type of plugin, below.

.bundle/config
Configures {% href https://bundler.io/ bundler %} to look for Ruby development executables in the binstub directory.
README.md
This markdown file includes a badge that automatically displays the most recently published version of the gem. The version will be shown as question marks on GitHub until you publish the first version of your gem on {% href RubyGems.org %}. Standard installation instructions are provided, and a placeholder is provided for you to write a description of the Jekyll tag plugin.
.git/
This is the local copy of your newly created git repository, and by default, the generated version is automatically committed.
.gitignore
This is set up for Jekyll gem development. Do not delete the .jekyll-cache/ entry, or you will introduce a {% href /jekyll/1000-jekyll-setup.html huge security risk %}.
.rspec
Configures the rspec test facility.
.rubocop.yml
Configures the Rubocop Ruby linter.
.vscode/
Defines standard debug launch configurations, suggested VSCode extensions, extension settings, and code snippets for developing Jekyll gems.
CHANGELOG.md
You should update this file each time you release a new version of the gem.
Gemfile
This is a standard setup for developing Jekyll gems; do not define runtime dependencies in this file.
your_gem_name.gemspec
Define runtime dependencies in this file. Also update the description of your new gem here. Pay close attention to the contents of this file; it defines your gem.
LICENCE.txt
Standard MIT license terms. If you want to use another license, modify this file, and update the spec.license entry in .gemspec file to match.
Rakefile
Standard setup for Ruby gem development, you probably do not need to modify this file.
bin/
This directory contain useful scripts for Jekyll gem development: attach, console, rake, and setup. Take a look at them.
demo/
This directory contains a small but complete Jekyll website, set up to exercise your Jekyll plugin. See demo/README.md for more information.
lib/
Contains the logic for your Jekyll plugin:
  • lib/your_gem_name.rb – entry point, set up to require all the other Ruby source files in the lib/ directory. You probably do not need to modify it.
  • your_gem_name/version.rb – update the version number in this file each time you publish a new release.
  • your_plugin_name.rb – the logic for your Jekyll tag, block, filter, generator, or hook goes in this file. It is well commented.
    • nugem can create Jekyll plugins that define more than one tag, and/or block tag, and/or filter, and/or generator, and/or hooks. A Ruby source file will be created for each.
    • For Jekyll tags and block tags, the contents subclass the appropriate class from the {% href /jekyll_plugins/jekyll_plugin_support.html jekyll_plugin_support %} gem.
spec/
This directory contains a scaffold for rspec tests for your Jekyll gem and includes some test fixtures. Jekyll filters are easy to test because they are just Ruby module methods. Setting up tests for other types of Jekyll plugins is a black art, however, because almost nothing is documented. Tags, block tags, generators and hooks can be extremely difficult to test due to the difficulty of setting up fixtures. The demo/ is provided, so you can debug your Jekyll plugins {% href https://en.wikipedia.org/wiki/In_situ in situ %}.

Jekyll Filter Plugins

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.

Filters should not accept page parameters because Jekyll will recursively re-evaluate all the properties of the page, and blow up when recursing through excerpt. This means the following syntax should not be used:

{% pre copyButton my_page.html %} {{ page | my_filter }} {% endpre %}

The workaround is to write a Jekyll inline tag instead.

Example Jekyll Filter

The following code includes a method called my_filter_template. That method becomes a filter when wrapped as shown:

{% flexible_include download show_copy_button file='$jekyll_plugin_template/lib/jekyll_filter_template.rb' %}

Input / Output Filter Transformation

Given this markup in an HTML file:

{% pre copyButton %}{% raw %}Search for {{ "joy" | my_filter_template }}{% endraw %}{% endpre %}

This is what is rendered to the web page after being passed through the above filter:

{% pre copyButton %}Search for {{ "joy" | my_filter_template }}{% endpre %}

Forwarding Singleton Methods to Instance Methods

Liquid only registers instance methods as filters

If you want 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.

Singleton methods cannot be used as Liquid filters. When you use a {% href https://www.rubydoc.info/stdlib/core/Module:module_function module_function %} statement, module functions can be invoked from other modules; you are actually converting those 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.

Method forwarding is a way to wrap a singleton method within an instance method. The following example of method forwarding was taken from my {% href https://github.com/mslinn/jekyll_draft/blob/master/lib/jekyll_draft.rb jekyll_draft %} plugin. The Jekyll::Draft::draft? singleton method is invoked from the Liquid filter method Jekyll::DraftFilter::is_draft.

{% pre copyButton %} module Jekyll module Draft # Define this method outside of the filter module so they can be invoked externally def draft?(doc) # blah blah end module_function :draft? end module DraftFilter def is_draft(doc) Draft::draft?(doc) # method forwarding end Liquid::Template.register_filter(DraftFilter) end end {% endpre %}

Jekyll Tag Plugins

Jekyll tag plugins are easier to write when using a template, and they are easier to manage when distributed as Ruby gems. Nugem does a good job of creating a working scaffold of Jekyll tags, customized for your needs.

Following is a demonstration of how to use nugem to create a new Jekyll plugin that defines one Jekyll tag. The gem that contains the tag will be called jekyll_highlight_tag, and the Jekyll tag will be called highlight. This plugin would be better implemented as a filter.

{% pre copyButton shell %} {% noselect %}nugem jekyll jekyll_highlight_tag --tag highlight {% noselect Please list the names of the options for the highlight Jekyll/Liquid tag: %} text fg_color bg_color {% noselect What is the type of text? (tab autocompletes) [boolean, string, numeric] (string) What is the type of fg_color? (tab autocompletes) [boolean, string, numeric] (string) What is the type of bg_color? (tab autocompletes) [boolean, string, numeric] (string) Initialized empty Git repository in /mnt/c/work/ruby/nugem/generated/jekyll_highlight_tag/.git/ Do you want to create a repository on GitHub named jekyll_highlight_tag? (y/N) %} y {% noselect Enumerating objects: 66, done. Counting objects: 100% (66/66), done. Delta compression using up to 12 threads Compressing objects: 100% (57/57), done. Writing objects: 100% (66/66), 461.38 KiB | 445.00 KiB/s, done. Total 66 (delta 0), reused 0 (delta 0), pack-reused 0 To github.com:mslinn/jekyll_highlight_tag.git * [new branch] master -> master branch 'master' set up to track 'origin/master'. %} {% endpre %}

Lets see what was written to the generated/ directory. The following shows only the first 2 levels of directories, without files.

{% pre copyButton dedent shell %} {% noselect %}tree -adL 2 generated/ {% noselect generated/ └── jekyll_highlight_tag ├── .bundle ├── .git ├── .vscode ├── bin ├── demo ├── lib ├── spec └── test
9 directories %} {% endpre %}

The above shows that the new Jekyll plugin, ready to be made into a gem, is stored in generated/jekyll_highlight_tag. {% href https://github.com/mslinn/jekyll_highlight_tag A public git repository was created on GitHub %}, and the contents of the directory were committed.

I use the generated/ directory as a scratch area for experimentation. Before we go any further, you might want to move the generated/jekyll_highlight_tag directory somewhere permanent. I moved it to the directory pointed to by $work, and made that directory current.

{% pre copyButton shell %} {% noselect %}mv generated/jekyll_highlight_tag $work/ {% noselect %}cd $work/jekyll_highlight_tag {% endpre %}

Regular readers of this blog will know that I use environment variables to point to directories; this allows me to address the contents of $work on every machine, even though the environment variable might point to /mnt/f/work on one machine, and /data/work on another.

Next, I launched the project using Visual Studio Code:

{% pre copyButton shell %} {% noselect %}code . {% endpre %}

Jekyll Block Tag Plugins

Jekyll block tag plugins are just like tag plugins, plus they also have a content body.

The following is a demonstration of how to use nugem to create a new Jekyll plugin that defines one Jekyll block tag. The gem that contains the tag will be called jekyll_highlight_block, and the Jekyll tag will be called highlight2. This plugin would be better implemented as a filter.

{% pre copyButton shell %} {% noselect %}nugem jekyll jekyll_highlight_block --block highlight2 {% noselect Please list the names of the options for the highlight2 Jekyll/Liquid tag: %} color bg_color {% noselect What is the type of fg_color? (tab autocompletes) [boolean, string, numeric] (string) What is the type of bg_color? (tab autocompletes) [boolean, string, numeric] (string) Initialized empty Git repository in /mnt/c/work/ruby/nugem/generated/jekyll_highlight_tag/.git/ Do you want to create a repository on GitHub named jekyll_highlight_block? (y/N) %} y {% noselect Enumerating objects: 66, done. Counting objects: 100% (66/66), done. Delta compression using up to 12 threads Compressing objects: 100% (57/57), done. Writing objects: 100% (66/66), 461.38 KiB | 445.00 KiB/s, done. Total 66 (delta 0), reused 0 (delta 0), pack-reused 0 To github.com:mslinn/jekyll_highlight_block.git * [new branch] master -> master branch 'master' set up to track 'origin/master'. %} {% endpre %}

The structure of the generated code for Jekyll block tags is identical to that of regular Jekyll tags.

The new Jekyll plugin, ready to be made into a gem, is stored in generated/jekyll_highlight_block. {% href https://github.com/mslinn/jekyll_highlight_block A public git repository was created on GitHub %}, and the contents of the directory were committed.

As with the preceding tag plugin, you might want to move the generated/jekyll_highlight_block directory somewhere permanent. I moved it to the directory pointed to by $work, and made that directory current.

{% pre copyButton shell %} {% noselect %}mv generated/jekyll_highlight_block $work/ {% noselect %}cd $work/jekyll_highlight_block {% endpre %}

Next, I launched the project using Visual Studio Code:

{% pre copyButton shell %} {% noselect %}code . {% endpre %}

Usage

Given this markup in an HTML file:

{% pre copyButton %}{% raw %}{ % highlight2 %} Hello, world! { % endhighlight2 %}{% endraw %}{% endpre %}

The rendered HTML from the block tag looks like this:

{% pre %} <span style='color: black; background: yellow; padding: 2px;'>Hello, world!</span> {% endpre %}

Here is another example:

{ % highlight2 fg_color="yellow" bg_color="green" % } Hello, world! { % endhighlight2 % }

The generated HTML from the block tag is as follows:

{% pre %} <span style='color: yellow; background: green; padding: 2px;'>Hello, world!</span> {% endpre %}

Jekyll Hook Plugins

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:

{% pre copyButton %} module JekyllPluginHookExamples Jekyll::Hooks.register(:documents, :post_render) do |doc| doc.output.gsub!('Jekyll', 'Awesome') end end {% endpre %}

To also modify web pages that are not in a collection (for example, /index.html), add the following into the above module JekyllPluginHooks:

{% pre copyButton %} Jekyll::Hooks.register(:pages, :post_render) do |page| page.output.gsub!('Jekyll', 'Awesome') end {% endpre %}

Notice that both of the hook invocations have 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:

{% pre copyButton %} 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 {% endpre %}

The demo/index.html web page now looks like the following:

{% img src="/blog/jekyll/plugins/awesome.webp" %}

If you want to translate web pages into other languages or dialects, for example, {% href https://www.dictionary.com/e/pig-latin/ Pig Latin %} or {% href http://talklikeapirate.com/wordpress/how-to/ Pirate Talk %}, or even {% href https://rubygems.org/gems/ruby-spellchecker 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. This is an example of a Jekyll hook plugin.

{% pre copyButton %} 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) {% endpre %}

Here is the output of one of the demo web pages:

{% pre copyButton Original HTML %} <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>. {% endpre %}

Notice that 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.

{% pre copyButton HTML translated to Pirate Talk %} <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> {% endpre %}

The translated HTML renders in a web browser like this:

{% img src="/blog/jekyll/plugins/pirate_html.webp" %}

Selecting Pages to Translate

The above pirate_translator plugin modifies every page on the website. 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 the hooks for :documents, :pages, and :posts.

Let's modify the hook, so it checks for the existence of a front matter variable called pirate_talk. If present, and it has a value that is not false, that page will be translated into Pirate Talk; otherwise, it will not be modified. Here is the modified version:

{% pre copyButton lib/jekyll_hook_examples.rb %} 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 {% endpre %}

{% href https://github.com/mslinn/jekyll_plugin_template/blob/master/demo/_posts/2022/2022-01-01-test.html demo/_posts/2022/2022-01-01-test.html %} looks like this:

{% flexible_include file='$jekyll_plugin_template/demo/_posts/2022/2022-01-01-test.html' escape download show_copy_button %}

Jekyll Generator Plugins

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:

You can create a generator when you need Jekyll to create additional content based on your own rules.

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 {% href https://github.com/jekyll/jekyll/blob/master/lib/jekyll/page.rb Jekyll::Page %} and are available via site.pages. Static files become instances of {% href https://github.com/jekyll/jekyll/blob/master/lib/jekyll/static_file.rb Jekyll::StaticFile %} and are available via site.static_files. See the {% href https://jekyllrb.com/docs/variables/ Variables documentation page %} and {% href https://github.com/jekyll/jekyll/blob/master/lib/jekyll/site.rb Jekyll::Site %} for details.
{% flexible_include file='$jekyll_plugin_template/lib/category_index_generator.rb' download show_copy_button %} {% comment %}

Jekyll Sub-Commands

{% href https://jekyllrb.com/docs/plugins/your-first-plugin/#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 {% href http://jekyllrb.com/docs/usage/ default Jekyll subcommands %}, which include jekyll build, jekyll clean, jekyll new, and jekyll 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 {% href https://rubyapi.org/3.1/o/gem/specification#method-i-executable gem executables %}. 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: {% pre %}Gem::Specification.new do |spec|
    spec.name = "jekyll-hello"
    end{% endpre %} This is consistent with the {% href https://guides.rubygems.org/name-your-gem/ Ruby Gem Naming Conventions %}, because Jekyll sub-commands of course extend the Jekyll gem.

    Use Dashes for Extensions

    If you’re adding functionality to another gem, use a dash. This usually corresponds to a / in the require statement (and therefore your gem’s directory structure) and a :: in the name of your main class or module.
    Now we know why the implementation of your gem must be defined within a directory called 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 {% href $jekyll_hello/lib/jekyll/hello.rb jekyll hello %} sub-command. Follow this pattern closely.

{% flexible_include file='$jekyll_hello/lib/jekyll/hello.rb' label='lib/jekyll/hello.rb' copyButton download %}

Some comments about the above code:

  • class << self opens up self’s singleton class, so that methods can be redefined for the current self 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 for prog, which has type {% href https://github.com/jekyll/mercenary/blob/master/lib/mercenary/program.rb Mercenary::Program %}. {% img src="/blog/jekyll/plugins/mercenary_program.webp" size="quartersize" style="margin-left: 1em;" %}
  • The name of the subcommand (hello) is specified as a symbol and passed to prog.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:

{% pre shell copyButton %} {% noselect %}bundle exec rake install && (cd demo; jekyll hello k tx bye) {% noselect 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={} %} {% endpre %}

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:

{% flexible_include file='$jekyll_hello/.vscode/launch.json' label='.vscode/launch.json' download copyButton %}

Nail down the desired Jekyll version to the exe/ subdirectory:

{% pre shell copyButton %} {% noselect %}bundle binstubs jekyll --path exe {% endpre %}

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:

{% pre Gemfile copyButton %} group :jekyll_plugins do gem 'jekyll-hello', path: '../' end {% endpre %}

The {% href https://github.com/mslinn/jekyll-hello/blob/master/bin/attach bin/attach %} script launches Jekyll under control of a debugger.

  1. Tell the script the directory containing the Jekyll site. {% pre copyButton %}{% noselect %}attach demo
    {% noselect ... 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 %}{% endpre %}
  2. Set breakpoints in lib/jekyll/hello.rb.
  3. 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 the program property as the source path for the gem, above.
  • Launch the configuration called Debug hello sub-command
😁

Easy!

{% endcomment %}

The workaround is to write a Jekyll inline tag instead.

Example Jekyll Filter

The following code includes a method called my_filter_template. That method becomes a filter when wrapped as shown:

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)

Input / Output Filter Transformation

Given this markup in an HTML file:

Shell
Search for {{ "joy" | my_filter_template }}

This is what is rendered to the web page after being passed through the above filter:

Shell
Search for joy

Forwarding Singleton Methods to Instance Methods

Liquid only registers instance methods as filters

If you want 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.

Singleton methods cannot be used as Liquid filters. When you use a module_function statement, module functions can be invoked from other modules; you are actually converting those 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.

Method forwarding is a way to wrap a singleton method within an instance method. The following example of method forwarding was taken from my jekyll_draft plugin. The Jekyll::Draft::draft? singleton method is invoked from the Liquid filter method Jekyll::DraftFilter::is_draft.

Shell
module Jekyll
  module Draft
    # Define this method outside of the filter module so they can be invoked externally
    def draft?(doc)
      # blah blah
    end
    module_function :draft?
  end

  module DraftFilter
    def is_draft(doc)
      Draft::draft?(doc) # method forwarding
    end

    Liquid::Template.register_filter(DraftFilter)
  end
end

Jekyll Tag Plugins

Jekyll tag plugins are easier to write when using a template, and they are easier to manage when distributed as Ruby gems. Nugem does a good job of creating a working scaffold of Jekyll tags, customized for your needs.

Following is a demonstration of how to use nugem to create a new Jekyll plugin that defines one Jekyll tag. The gem that contains the tag will be called jekyll_highlight_tag, and the Jekyll tag will be called highlight. This plugin would be better implemented as a filter.

Shell
$ nugem jekyll jekyll_highlight_tag --tag highlight
Please list the names of the options for the highlight Jekyll/Liquid tag:  text fg_color bg_color
What is the type of text? (tab autocompletes) [boolean, string, numeric] (string)
What is the type of fg_color? (tab autocompletes) [boolean, string, numeric] (string)
What is the type of bg_color? (tab autocompletes) [boolean, string, numeric] (string)
Initialized empty Git repository in /mnt/c/work/ruby/nugem/generated/jekyll_highlight_tag/.git/
Do you want to create a repository on GitHub named jekyll_highlight_tag? (y/N)  y
Enumerating objects: 66, done.
Counting objects: 100% (66/66), done.
Delta compression using up to 12 threads
Compressing objects: 100% (57/57), done.
Writing objects: 100% (66/66), 461.38 KiB | 445.00 KiB/s, done.
Total 66 (delta 0), reused 0 (delta 0), pack-reused 0
To github.com:mslinn/jekyll_highlight_tag.git
  * [new branch]      master -> master
branch 'master' set up to track 'origin/master'. 

Lets see what was written to the generated/ directory. The following shows only the first 2 levels of directories, without files.

Shell
$ tree -adL 2 generated/
generated/
  └── jekyll_highlight_tag
      ├── .bundle
      ├── .git
      ├── .vscode
      ├── bin
      ├── demo
      ├── lib
      ├── spec
      └── test
9 directories

The above shows that the new Jekyll plugin, ready to be made into a gem, is stored in generated/jekyll_highlight_tag. A public git repository was created on GitHub, and the contents of the directory were committed.

I use the generated/ directory as a scratch area for experimentation. Before we go any further, you might want to move the generated/jekyll_highlight_tag directory somewhere permanent. I moved it to the directory pointed to by $work, and made that directory current.

Shell
$ mv generated/jekyll_highlight_tag $work/

$ cd $work/jekyll_highlight_tag

Regular readers of this blog will know that I use environment variables to point to directories; this allows me to address the contents of $work on every machine, even though the environment variable might point to /mnt/f/work on one machine, and /data/work on another.

Next, I launched the project using Visual Studio Code:

Shell
$ code .

Jekyll Block Tag Plugins

Jekyll block tag plugins are just like tag plugins, plus they also have a content body.

The following is a demonstration of how to use nugem to create a new Jekyll plugin that defines one Jekyll block tag. The gem that contains the tag will be called jekyll_highlight_block, and the Jekyll tag will be called highlight2. This plugin would be better implemented as a filter.

Shell
$ nugem jekyll jekyll_highlight_block --block highlight2
Please list the names of the options for the highlight2 Jekyll/Liquid tag:  color bg_color
What is the type of fg_color? (tab autocompletes) [boolean, string, numeric] (string)
What is the type of bg_color? (tab autocompletes) [boolean, string, numeric] (string)
Initialized empty Git repository in /mnt/c/work/ruby/nugem/generated/jekyll_highlight_tag/.git/
Do you want to create a repository on GitHub named jekyll_highlight_block? (y/N)  y
Enumerating objects: 66, done.
Counting objects: 100% (66/66), done.
Delta compression using up to 12 threads
Compressing objects: 100% (57/57), done.
Writing objects: 100% (66/66), 461.38 KiB | 445.00 KiB/s, done.
Total 66 (delta 0), reused 0 (delta 0), pack-reused 0
To github.com:mslinn/jekyll_highlight_block.git
  * [new branch]      master -> master
branch 'master' set up to track 'origin/master'. 

The structure of the generated code for Jekyll block tags is identical to that of regular Jekyll tags.

The new Jekyll plugin, ready to be made into a gem, is stored in generated/jekyll_highlight_block. A public git repository was created on GitHub, and the contents of the directory were committed.

As with the preceding tag plugin, you might want to move the generated/jekyll_highlight_block directory somewhere permanent. I moved it to the directory pointed to by $work, and made that directory current.

Shell
$ mv generated/jekyll_highlight_block $work/

$ cd $work/jekyll_highlight_block

Next, I launched the project using Visual Studio Code:

Shell
$ code .

Usage

Given this markup in an HTML file:

Shell
{ % highlight2 %}
Hello, world!
{ % endhighlight2 %}

The rendered HTML from the block tag looks like this:

Shell
<span style='color: black; background: yellow; padding: 2px;'>Hello, world!</span>

Here is another example:

{ % highlight2 fg_color="yellow" bg_color="green" % } Hello, world! { % endhighlight2 % }

The generated HTML from the block tag is as follows:

Shell
<span style='color: yellow; background: green; padding: 2px;'>Hello, world!</span>

Jekyll Hook Plugins

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:

Shell
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:

Shell
Jekyll::Hooks.register(:pages, :post_render) do |page|
  page.output.gsub!('Jekyll', 'Awesome')
end

Notice that both of the hook invocations have 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:

Shell
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. This is an example of a Jekyll hook plugin.

Shell
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:

Shell
<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 that 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.

Shell
<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 on the website. 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 the hooks for :documents, :pages, and :posts.

Let's modify the hook, so it checks for the existence of a front matter variable called pirate_talk. If present, and it has a value that is not false, that page will be translated into Pirate Talk; otherwise, it will not be modified. Here is the modified version:

Shell
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 Generator Plugins

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:

You can create a generator when you need Jekyll to create additional content based on your own rules.

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


* indicates a required field.

Please select the following to receive Mike Slinn’s newsletter:

You can unsubscribe at any time by clicking the link in the footer of emails.

Mike Slinn uses Mailchimp as his marketing platform. By clicking below to subscribe, you acknowledge that your information will be transferred to Mailchimp for processing. Learn more about Mailchimp’s privacy practices.