Mike Slinn
Mike Slinn

Explanations and Examples of Jekyll Plugins

Published 2022-03-27. Last modified 2022-04-24.
Time to read: 6 minutes.

This site is categorized under Jekyll, Ruby.

This blog post discusses the various types of Jekyll plugins, including filters, generators, tags, block tags and hooks. It is intended to provide background information.

The follow-on blog post builds upon the information presented here. It discusses a GitHub project that provides many working examples of plugins that you can use as a starting point for your own plugins.

Plugin Types

Jekyll supports the following types of plugins. This blog post discusses and demonstrates templates for all of these types of plugins.

Debugging Plugins

The official Jekyll documentation does not provide much guidance about how to debug Jekyll plugins. Please read my blog post on Debugging Jekyll Plugins. In this post, and the follow-on post, I provide debugging scripts for the plugins shown. You can use those scripts on your Jekyll plugin projects.

Tag Parameter Parsing

Block tag and inline tag plugins often require parameter parsing. I have never seen good parameter parsing for Jekyll tag plugins. Most tag plugins either have no need to pass arguments, or the parameters are merely keywords (which are easy to parse), or Yet Another Awful Parser (YAAP) is built.

It is actually quite easy to perform proper parameter parsing once you know how. The GitHub project in the follow-on blog post uses the technique described here.

Here is an example of parameters that requires parsing:

{% my_tag_plugin param1=value1 param2='value 2' param3="value 3" %}

The name/value pairs for param1/value1, param2/value 2 and param3/value 3 are parsed and provided to your plugin without having to do anything special.

Shellwords

Let's assume that the parameters highlighted above are provided to the Jekyll tag plugin as a string called argument_string. First we tokenize name/value pairs by using Shellwords:

argv = Shellwords.split(argument_string)

Shellwords is part of the standard Ruby runtime library. It manipulates strings according to the word parsing rules of the UNIX Bourne shell. That just means that Shellwords creates an array of tokens from the command line string. Shellwords recognizes quoted strings, using either single quotes or double quotes.

Below is a small example demonstration program, followed by its output. The flavor of Ruby heredoc used, called a squiggly heredoc, strips leading whitespace between END_STRING delimiters, and does not require quotes to be escaped.

# frozen_string_literal: true

require "shellwords"

string = <<~END_STRING
  a b=c d='e f' g="h i j"
END_STRING
puts Shellwords.split(string)

Each token is displayed on a separate line. Let's run the program and see the output.

Shell
$ ruby shellwords.rb
a
b=c
d=e f
g=h i j 

KeyValueParser

For extracting key/value pairs from the tokens returned by Shellwords, we use KeyValueParser, an incredibly versatile yet easy to use Ruby gem. KeyValueParser accepts the array of tokens from Shellwords and returns a hash[Symbol, String], called params in the following code.

require 'key_value_parser'

# argv was returned by Shellwords, above
params = KeyValueParser.new.parse(argv)

# Example of obtaining the value of parameter param1
@param1 = params[:param1]

# The value of non-existant keys is nil,
# which often displays as the empty string
@param_x = params[:not_present]

The RSpec unit test for jekyll_plugin_template demonstrates how KeyValueParser is used in conjunction with Shellwords:

spec/jekyll_plugin_template_spec.rb
require 'key_value_parser'
require 'shellwords'

params = "param0 param1=value1 param2='value2' param3=\"value3's tricky\""
argv = Shellwords.split params
options = KeyValueParser.new.parse(argv)

expect(options[:param0]).to eq(true)
expect(options[:param1]).to eq("value1")
expect(options[:param2]).to eq("value2")
expect(options[:param3]).to eq("value3's tricky")
expect(options[:unknown]).to be_nil

Variables

Most of this section pertains to tag plugins, but this information can also be useful for most of the other types of plugins. Jekyll variables can be defined in various places. I believe that each of these places is what is meant by a scope... but because that term is never defined, I cannot be 100% sure.

  • In a page content, via a liquid assign or capture statement.
  • In the front matter of a page
  • In an include's parameters
  • In a layout
  • Jekyll also exposes a few internal variables, such as site and page. They can be retrieved from the render method using liquid_context.registers[:site] and liquid_context.registers[:page].

Additionally, name/value pairs are available from the YAML data in _config.yml and data.

When referencing a variable from an include, prepend the scope to the variable name. For example: layout.compress, include.param1, page.date, or my_var.

The Jekyll documentation does not provide any guidance about how a plugin would to evaluate a variable reference. Here is what I've learned:

  • Variables defined in the content of pages can be retrieved as hash values in from the render method's parameter, which is of type Liquid::Context. For example:
    my_tag.rb
    def render(liquid_context)
    my_var = liquid_context['my_var']
    end
  • Variables defined in front matter can be retrieved from the :page context, which has type Jekyll::Drops::DocumentDrop. For example:
    my_tag.rb
    def render(liquid_context)
    page = liquid_context.registers[:page]
    layout = @page['layout']
    end
  • Is this true? Variables passed from an include parameter can be fetched from the liquid_context within the register method like the following. Note that fetch provides for a default value, which is false in this example:
    my_tag.rb
    def render(liquid_context)
    do_not_escape = liquid_context['include'].fetch('do_not_escape', 'false')
    end
  • Variables defined in a layout template used by a page can be retrieved from within the register method like this:
    my_tag.rb
    def render(liquid_context)
    env = liquid_context.environments.first
    layout_hash = env['layout']
    layout_hash['compress']
    end

Plugins may receive variables as arguments, if they are enclosed in double curly braces.

---
front_matter_variable: Provided in front matter
---
{% assign page_variable = "page variable value" %}

{% block_tag_template param1="provided as string"
                      param2='{{page_variable}}'
                      param3="{{front_matter_variable}}"
                      param4='{{page.last_modified_at}}'
                      param5='{{layout.compress}}' %}
  This is the block_tag_template content.
  It includes {{front_matter_variable}} variable data.
{% endblock_tag_template %}

Bonus! You can examine all of the attributes of a page except content and next as follows. (You probably want to skip the content and next attributes because they will fill up your screen.)

MyTag.rb
def render(liquid_context)
  puts liquid_context.registers[:page]
          .sort
          .reject { |k, _| ["content", "next"].include? k }
          .map { |k, v| "#{k}=#{v}" }
          .join("\n  ")
end

Here is sample output:

categories=["Jekyll"]
collection=posts
date=2022-03-28 00:00:00 -0400
description=Just a draft test post I wrote
draft=true
excerpt=<h2>Hello, World!</h2>
ext=.html
front_matter_variable=Provided in front matter
id=/blog/2022/03/28/test2
last_modified_at=2022-04-12
layout=default
output=
path=_drafts/2022/2022-05-01-test2.html
previous=
relative_path=_drafts/2022/2022-05-01-test2.html
slug=test2
tags=[]
title=Test Draft Post
url=/blog/2022/03/28/test2.html

Scope Control

Some types of plugins, such as tags and filters, automatically have a restricted scope; they only act on their arguments. Other types of plugins, such as converters, generators, and hooks, have a much larger scope; they either act on the entire website, or all webpages within a certain category, for example all blog pages. I will show you some ways of efficiently restricting the scope of hooks so only selected webpages are processed, or even just selected portions of designated webpages.

Jekyll process Steps

The types of hooks exactly follow the major processing steps that Jekyll follows, as defined in Jekyll/site.rb in Site.process:

Site.process
# Public: Read, process, and write this Site to output.
#
# Returns nothing.
def process
  return profiler.profile_process if config["profile"]

  reset
  read
  generate
  render
  cleanup
  write
end

Generator Invocations

The generate method invokes all generators. It is called after read, but before render. generate looks like this:

jekyll/site.rb
# Run each of the Generators.
#
# Returns nothing.
def generate
  generators.each do |generator|
    start = Time.now
    generator.generate(self)
    Jekyll.logger.debug "Generating:",
                        "#{generator.class} finished in #{Time.now - start} seconds."
  end
  nil
end

This means that generators inherit the global state right after the reset and read hooks trigger, but before the render, cleanup and write hooks trigger. This also means that any changes that generators make to global state are only visible in the render, cleanup and write hooks.

Tag Invocations

Tags get called by the call sequence initiatated by Site.render, by the invocation of document.renderer.run in Site.render_regenerated, just before the :site :post_render triggers.

true
def render_regenerated(document, payload)
  return unless regenerator.regenerate?(document)

  document.renderer.payload = payload
  document.output = document.renderer.run
  document.trigger_hooks(:post_render)
end

Cleanup Invocations

No mention of the purpose of the :cleanup hook is provided in the official documentation. Looking at the Jekyll source code reveals that :cleanup is where unnecessary files are removed from the _site under construction, prior to writing out the site.

Site Payload

The site payload is defined in Jekyll/site.rb in Site.site_payload

Site.site_payload
# The Hash payload containing site-wide data.
#
# Returns the Hash: { "site" => data } where data is a Hash with keys:
#   "time"       - The Time as specified in the configuration or the
#                  current time if none was specified.
#   "posts"      - The Array of Posts, sorted chronologically by post date
#                  and then title.
#   "pages"      - The Array of all Pages.
#   "html_pages" - The Array of HTML Pages.
#   "categories" - The Hash of category values and Posts.
#                  See Site#post_attr_hash for type info.
#   "tags"       - The Hash of tag values and Posts.
#                  See Site#post_attr_hash for type info.
def site_payload
  Drops::UnifiedPayloadDrop.new self
end
alias_method :to_liquid, :site_payload

Hooks

This is the Jekyll method that is invoked by the 45 examples of hooks described next.

/lib/jekyll/hooks.rb
def self.register(owners, event, priority: DEFAULT_PRIORITY, &block)
  Array(owners).each do |owner|
    register_one(owner, event, priority_value(priority), &block)
  end
end
  • There are 45 valid combination of owners and event. I will introduce those parameters, provide code examples for all of them, discuss when they should be used, and offer suggestions of how to best use them. Jekyll internally defines additional hooks, for example :site :post_convert, but only the documented hooks are exposed to plugin developers.
  • Notice that the register method accepts a block. We can use a Ruby Proc to supply the block to consolidate code across multiple hooks. I provide examples of this in the follow-on blog post.

45 Plugin Hooks

Jekyll’s built-in hook ‘owners’s are :site, :pages, :documents, :posts, and :clean. The core events are :post_init, :pre_render, :post_convert, :post_render and :post_write.

There are 45 valid combinations of owners and event parameters to Jekyll::Hooks.register.

Jekyll’s hook event types vary between owners. The 5 core event types mentioned above pertain to 3 types of owners – :pages, :documents, and :posts. This core set of hooks is embellished for owner :site, and does not pertain to owner :clean.

The :site owner has event types that are similar, but not identical to other event owners. Owner :site has 6 combinations, 3 of which are unique. Owner :clean has only one combination. We will see the details in a moment.

Following is the source file containing short working examples all 45 of the standard Jekyll hooks, and an explanation of the key features.

# frozen_string_literal: true

require "jekyll_plugin_logger"
require_relative "jekyll_plugin_template/version"
require_relative "dumpers"

module JekyllPluginHooksName
  PLUGIN_NAME = "jekyll_plugin_hooks"
end

# The Jekyll processing steps are described in https://jekyllrb.com/tutorials/orderofinterpretation/
#
# 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:
#   JekyllPluginHooks: debug
#
# Jekyll::Hooks.register accepts an optional parameter:
#   :priority determines the load order for the hook plugins.
#     Valid values are: :lowest, :low, :normal, :high, and :highest.
#     Highest priority matches are applied first, lowest priority are applied last.
#     The default value is :normal
#
# Each hook, except the clean hook, can set a boolean flag, called `site.safe`, that informs Jekyll if this plugin may be safely executed in an environment
# where arbitrary code execution is not allowed. This is used by GitHub Pages to determine which
# core plugins may be used, and which are unsafe to run. If your plugin does not allow for arbitrary
# code execution, set this to true. GitHub Pages still will not load your plugin, but if you submit it
# for inclusion in core, it is best for this to be correct!
# Default value is false.
# The hooks for pages, posts and documents access safe via pages.site.safe, posts.site.safe and documents.site.safe, respectively.
module JekyllPluginHooks
  ########## :site hooks
  # These hooks influence the entire site

  # Called just after the site resets during regeneration
  # This is the first hook called, so you might think that this is the best place to define loggers.
  # However, this hook will not be called unless safe mode is OFF, so define loggers in the :site :after_init hook instead
  Jekyll::Hooks.register(:site, :after_reset, priority: :normal) do |site|
    @log_site ||= PluginMetaLogger.instance.new_logger(:SiteHooks, PluginMetaLogger.instance.config)
    @log_site.info { "Jekyll::Hooks.register(:site, :after_reset) invoked." }
    Dumpers.dump_site(@log_site, "Jekyll::Hooks.register(:site, :after_reset)", site)
  end

  # This hook is called just after the site initializes.
  # It is a good place to modify the configuration of the site.
  # This hook is triggered once per build / serve session.
  Jekyll::Hooks.register(:site, :after_init, priority: :normal) do |site|
    @log_clean = PluginMetaLogger.instance.new_logger(:CleanHook,     PluginMetaLogger.instance.config)
    @log_docs  = PluginMetaLogger.instance.new_logger(:DocumentHooks, PluginMetaLogger.instance.config)
    @log_pages = PluginMetaLogger.instance.new_logger(:PageHooks,     PluginMetaLogger.instance.config)
    @log_posts = PluginMetaLogger.instance.new_logger(:PostHooks,     PluginMetaLogger.instance.config)
    @log_site  ||= PluginMetaLogger.instance.new_logger(:SiteHooks,   PluginMetaLogger.instance.config)

    @log_site.info { "Loaded #{JekyllPluginHooksName::PLUGIN_NAME} v#{JekyllPluginTemplate::VERSION} plugin." }
    @log_site.info { "Jekyll::Hooks.register(:site, :after_init) invoked." }
    Dumpers.dump_site(@log_site, "Jekyll::Hooks.register(:site, :after_init)", site)
  end

  # Called after all source files have been read and loaded from disk.
  # This is a good hook for enriching posts;
  # for example, adding links to author pages or adding posts to author pages.
  Jekyll::Hooks.register(:site, :post_read, priority: :normal) do |site|
    @log_site.info { "Jekyll::Hooks.register(:site, :post_read) invoked." }
    Dumpers.dump_site(@log_site, "Jekyll::Hooks.register(:site, :post_read)", site)
  end

  # Called before rendering the whole site
  # This is the first hook in the site generation sequence where site['env'] has a value.
  # Consequently, this is the first hook that defines mode (production, development or test),
  # because it is derived from site['env']['JEKYLL_ENV']
  # @param payload [Hash] according to the docs, payload is a hash containing the variables available during rendering; the hash can be modified here.
  # However, the debugger shows payload has type Jekyll::UnifiedPayloadDrop
  Jekyll::Hooks.register(:site, :pre_render, priority: :normal) do |site, payload|
    @log_site.info { "Jekyll::Hooks.register(:site, :pre_render) invoked." }
    @log_site.debug { dump(":site, :pre_render payload", payload) }
    Dumpers.dump_site(@log_site, "Jekyll::Hooks.register(:site, :pre_render)", site)
    Dumpers.dump_payload(@log_site, "Jekyll::Hooks.register(:site, :pre_render)", payload)
  end

  # Called after rendering the whole site, but before writing any files.
  # Functionally, this hook is exactly the same as a Jekyll generator.
  # This hook is also similar to invoking the same method on the :post_render hooks for :documents and :pages:
  #   Jekyll::Hooks.register(:documents, :post_render, &my_method)
  #   Jekyll::Hooks.register(:pages, :post_render, &my_method)
  # ... with the difference that this hook will be called only once, for the entire site, so you will have to iterate over all of the
  # :documents and :pages, whereas the :pages and :documents hooks are called once for each page and document.
  # @param payload [Hash] contains final values of variables after rendering the entire site (useful for sitemaps, feeds, etc).
  Jekyll::Hooks.register(:site, :post_render, priority: :normal) do |site, payload|
    @log_site.info { "Jekyll::Hooks.register(:site, :post_render) invoked." }
    @log_site.debug { dump(":site, :post_render payload", payload) }
    Dumpers.dump_site(@log_site, "Jekyll::Hooks.register(:site, :post_render)", site)
    Dumpers.dump_payload(@log_site, "Jekyll::Hooks.register(:site, :post_render)", payload)
  end

  # Called after writing all of the rendered files to disk
  Jekyll::Hooks.register(:site, :post_write, priority: :normal) do |site|
    @log_site.info { "Jekyll::Hooks.register(:site, :post_write) invoked." }
    Dumpers.dump_site(@log_site, "Jekyll::Hooks.register(:site, :post_write)", site)
  end

  ########## :pages hooks
  # Pages are web pages that do not belong to a collection, such as posts or drafts.
  # These hooks provide fine-grained control over all pages in the site.

  # Called whenever a page is initialized
  Jekyll::Hooks.register(:pages, :post_init, priority: :normal) do |page|
    @log_pages.info { "Jekyll::Hooks.register(:pages, :post_init) invoked." }
    Dumpers.dump_page(@log_pages, "Jekyll::Hooks.register(:pages, :post_init)", page)
  end

  # Called just before rendering a page
  Jekyll::Hooks.register(:pages, :pre_render, priority: :normal) do |page, payload|
    @log_pages.info { "Jekyll::Hooks.register(:pages, :pre_render) invoked." }
    Dumpers.dump_page(@log_pages, "Jekyll::Hooks.register(:pages, :pre_render)", page)
    Dumpers.dump_payload(@log_pages, ":pages, :pre_render payload", payload)
  end

  # Called after converting the page content, but before rendering the page layout
  Jekyll::Hooks.register(:pages, :post_convert, priority: :normal) do |page|
    @log_pages.info { "Jekyll::Hooks.register(:pages, :post_convert) invoked." }
    Dumpers.dump_page(@log_pages, "Jekyll::Hooks.register(:pages, :post_convert)", page)
  end

  # Called after rendering a page, but before writing it to disk
  Jekyll::Hooks.register(:pages, :post_render, priority: :normal) do |page|
    page.site.safe = true
    @log_pages.info { "Jekyll::Hooks.register(:pages, :post_render) invoked." }
    Dumpers.dump_page(@log_pages, "Jekyll::Hooks.register(:pages, :post_render)", page)
  end

  # Called after writing a page to disk
  Jekyll::Hooks.register(:pages, :post_write, priority: :normal) do |page|
    @log_pages.info { "Jekyll::Hooks.register(:pages, :post_write) invoked." }
    Dumpers.dump_page(@log_pages, "Jekyll::Hooks.register(:pages, :post_write)", page)
  end

  ########## :documents hooks
  # Documents are web pages that belong to a collection, for example posts, drafts and custom collections.
  # These hooks provide fine-grained control over all documents in the site.
  # If you want to inspect or process all collections in the same way, use these hooks.
  # If you just want to process a custom collection, use these hooks and filter out the documents
  # that do not belong to that collection.

  # Called whenever any document is initialized.
  # Front matter data will not have been assigned yet to documents when this hook is invoked, for example:
  #   categories, description, last_modified_at, tags, title, and slug;
  # other document attributes that are not yet ready when this hook is invoked include
  # excerpt and ext (file extension).
  # The collection attribute will be set properly for this hook.
  Jekyll::Hooks.register(:documents, :post_init, priority: :normal) do |document|
    @log_docs.info { "Jekyll::Hooks.register(:documents, :post_init) invoked." }
    Dumpers.dump_document(@log_docs, "Jekyll::Hooks.register(:documents, :post_init)", document)
    "stop"
  end

  # Called just before rendering a document.
  # Front matter data will have been assigned when this hook is invoked.
  # Liquid variables are still embedded in the content.
  # If the document contains markdown (or some other markup),
  # it will not have been converted to HTML (or whatever the target format is) yet.
  Jekyll::Hooks.register(:documents, :pre_render, priority: :normal) do |document, payload|
    @log_docs.info { "Jekyll::Hooks.register(:documents, :pre_render) invoked." }
    Dumpers.dump_document(@log_docs, "Jekyll::Hooks.register(:documents, :pre_render)", document)
    Dumpers.dump_payload(@log_docs, ":documents, :pre_render payload", payload)
  end

  # Called after converting the document content to HTML (or whatever),
  # but before rendering the document using the layout.
  Jekyll::Hooks.register(:documents, :post_convert, priority: :normal) do |document|
    @log_docs.info { "Jekyll::Hooks.register(:documents, :post_convert) invoked." }
    Dumpers.dump_document(@log_docs, "Jekyll::Hooks.register(:documents, :post_convert)", document)
  end

  # Called after rendering a document using the layout, but before writing it to disk.
  # This is your last chance to modify the content.
  Jekyll::Hooks.register(:documents, :post_render, priority: :normal) do |document|
    @log_docs.info { "Jekyll::Hooks.register(:documents, :post_render) invoked." }
    Dumpers.dump_document(@log_docs, "Jekyll::Hooks.register(:documents, :post_render)", document)
  end

  # Called after writing a document to disk.
  # Useful for statistics regarding completed renderings.
  Jekyll::Hooks.register(:documents, :post_write, priority: :normal) do |document|
    @log_docs.info { "Jekyll::Hooks.register(:documents, :post_write) invoked." }
    Dumpers.dump_document(@log_docs, "Jekyll::Hooks.register(:documents, :post_write)", document)
  end

  ########## :posts hooks
  # These hooks provide fine-grained control over all posts **and drafts** in the site without affecting
  # documents in user-defined collections

  # Called whenever any post is initialized
  Jekyll::Hooks.register(:posts, :post_init, priority: :normal) do |post|
    @log_posts.info { "Jekyll::Hooks.register(:posts, :post_init) invoked." }
    Dumpers.dump_document(@log_posts, "Jekyll::Hooks.register(:posts, :post_init)", post)
  end

  # Called just before rendering a post
  Jekyll::Hooks.register(:posts, :pre_render, priority: :normal) do |post, payload|
    # post is a Jekyll::Document
    @log_posts.info { "Jekyll::Hooks.register(:posts, :pre_render) invoked." }
    Dumpers.dump_document(@log_posts, "Jekyll::Hooks.register(:posts, :pre_render)", post)
    Dumpers.dump_payload(@log_posts, ":posts, :pre_render payload", payload)
  end

  # Called after converting the post content, but before rendering the post layout.
  # This hook can be used to make edits to rendered pages,
  # regardless of whether they were originally written in markdown or HTML.
  #
  # Changes must modify post.output, as shown in this example:
  #   Jekyll::Hooks.register(:posts, :post_convert) do |post|
  #     post.output.gsub!('programming PHP', 'banging rocks together')
  #   end
  Jekyll::Hooks.register(:posts, :post_convert, priority: :normal) do |post|
    @log_posts.info { "Jekyll::Hooks.register(:posts, :post_convert) invoked." }
    Dumpers.dump_document(@log_posts, "Jekyll::Hooks.register(:posts, :post_convert)", post)
  end

  # Called after rendering a post, but before writing it to disk.
  # Changing `post.conent` has no effect on visible output.
  Jekyll::Hooks.register(:posts, :post_render, priority: :normal) do |post|
    @log_posts.info { "Jekyll::Hooks.register(:posts, :post_render) invoked." }
    Dumpers.dump_document(@log_posts, "Jekyll::Hooks.register(:posts, :post_render)", post)
  end

  # Called after writing a post to disk
  Jekyll::Hooks.register(:posts, :post_write, priority: :normal) do |post|
    @log_posts.info { "Jekyll::Hooks.register(:posts, :post_write) invoked." }
    Dumpers.dump_document(@log_posts, "Jekyll::Hooks.register(:posts, :post_write)", post)
  end

  ########## :clean hooks
  # These hooks provide fine-grained control on the list of obsolete files determined
  # to be deleted during the site's cleanup phase.

  # Called during the cleanup of a site's destination, before the site is built
  Jekyll::Hooks.register(:clean, :on_obsolete, priority: :normal) do |files|
    # files has type Array[String]
    @log_clean.info { "Jekyll::Hooks.register(:clean, :on_obsolete) invoked for #{files}." }
  end
end

Hook Call Order

The Jekyll documentation does not indicate the exact order that each of the 45 hooks gets called. However, the log output from this template makes that clear. Following is elided output; I removed duplicate log entries. All loggers were set to level info. Output will vary, depending on the Jekyll site that is processed and the log levels you set. :post_init, in particular, gets called many times. You should write your hooks so they are idempotent.

The following output is slightly incomplete; I intend to fill out the test data better and redo this at some point.

INFO Module: Jekyll::Hooks.register(:site, :after_reset) invoked.
INFO Module: Jekyll::Hooks.register(:pages, :post_init) invoked.
INFO Module: Jekyll::Hooks.register(:documents, :post_init) invoked.
INFO Module: Jekyll::Hooks.register(:posts, :post_init) invoked.
INFO Module: Jekyll::Hooks.register(:site, :post_read) invoked.
INFO Module: Jekyll::Hooks.register(:pages, :post_init) invoked.
INFO Module: Jekyll::Hooks.register(:site, :pre_render) invoked.
INFO Module: Jekyll::Hooks.register(:pages, :pre_render) invoked.
INFO Module: Jekyll::Hooks.register(:pages, :post_convert) invoked.
INFO Module: Jekyll::Hooks.register(:pages, :post_render) invoked.
INFO Module: Jekyll::Hooks.register(:site, :post_render) invoked.
INFO Module: Jekyll::Hooks.register(:clean, :on_obsolete) invoked for [].
INFO Module: Jekyll::Hooks.register(:pages, :post_write) invoked.