:bulb: This is the first part of the article. The next part can be found here.

Introduction

Actually the main purpose of this post is to provide a detailed step-by-step guide for building a Jekyll plugin from scratch. So the use scope of the plugin by itself is not so important.

Even though Jekyll does have some documentation about plugins (The general overview and installation guide and Some basic knowledge about plugin types and internals) the existing documentation lacks on details a bit.

And even more in: the official documentation there is a phrase like:

For tips on creating a gem … look through the source code of an existing plugin such as jekyll-feed`

I agree with the fact that this plugin is a good way to discover the best practices and a common structural patterns of a Jekyll plugin in general, and it is worth to looking into it. But the thing is that there’re really not enought details and the basic how-to-start building a plugin tutorial is actually missing.

Anyway all of these materials are worth reading to get to get a general idea of what’s going on. And it won’t take you more than 20-40 minutes approximately.

What we’re going to build

Keeping an eye on your visitors is extremely useful. It allows you to analyze which subjects are interesting for your audience, how comfortable is the navigation and even more.

Actually there are tons of different plugins, trackers and web applications which can be used for these purposes.

We’re going to use two of the world’s most popular digital analytics systems: Google Analytics and Yandex Metrica.

We will also only deal with page views counters to not make our examples overloaded.

Obtaining codes (cheat-sheet)

That’s just a basic cheat-sheet. You can find a more detailed how-to guides here:

Getting tracking code for Google Analytics

Admin :arrow_right: Property :arrow_right: Tracking Info :arrow_right: Tracking Code

Will result to where the value XX-XXXXXXXXX-X will be your tracking code for Google:

<!-- Global site tag (gtag.js) - Google Analytics -->
<script async src="https://www.googletagmanager.com/gtag/js?id=XX-XXXXXXXXX-X"></script>
<script>
  window.dataLayer = window.dataLayer || [];
  function gtag(){dataLayer.push(arguments);}
  gtag('js', new Date());

  gtag('config', 'XX-XXXXXXXXX-X');
</script>
<!-- End Global site tag (gtag.js) - Google Analytics -->

Getting tracking code for Yandex Metrica

Add tag :arrow_right: Go to the tag :arrow_right: Settings :arrow_right: Code snippet (in the bottom of the page)

Will result to where the value XXXXXXXX will be your tracking code for Yandex:

<!-- Yandex.Metrika counter -->
<script type="text/javascript" >
   (function(m,e,t,r,i,k,a){m[i]=m[i]||function(){(m[i].a=m[i].a||[]).push(arguments)};
   m[i].l=1*new Date();k=e.createElement(t),a=e.getElementsByTagName(t)[0],k.async=1,k.src=r,a.parentNode.insertBefore(k,a)})
   (window, document, "script", "https://mc.yandex.ru/metrika/tag.js", "ym");

   ym(XXXXXXXX, "init", {
        clickmap:true,
        trackLinks:true,
        accurateTrackBounce:true
   });
</script>
<noscript><div><img src="https://mc.yandex.ru/watch/XXXXXXXX" style="position:absolute; left:-9999px;" alt="" /></div></noscript>
<!-- /Yandex.Metrika counter -->

First steps

Creating a test project

Let’s start with a small step at first.

First of all, you’ll need to have an existing Jekill site installed. Let’s create a test project for it:

$ jekyll new test_project
Running bundle install in /home/somename/test_project...
  Bundler: Fetching gem metadata from https://rubygems.org/..........
  Bundler: Fetching gem metadata from https://rubygems.org/.
  Bundler: Resolving dependencies...
  ...
New jekyll site installed in /home/vanya/Projects/zinovyev/tutorials/test_project.

Now let’s enter the new project directory and list all the files there:


$ cd test_project
$ ls
$ 404.html  about.markdown  _config.yml  Gemfile  Gemfile.lock  index.markdown  _posts

Start your jekyll app with this command:

jekyll serve --watch --verbose --port 15000

Your application should be listening on the 127.0.0.1:15000 now.

If you want to know more about all the flags that I’ve used just type jekyll serve --help and you’ll get the description for all of them.

Start to build the plugin

In your site source root, make a _plugins directory. Now you can place all your plugins here and they will be available in your application right away.

Let’s create a file _plugins/test_project.rb with the following content:

test_project/_plugins/test_project.rb
# Main metrics plugin class

class JekyllMetrics
  attr_accessor :page

  def initialize(page)
    @page = page
    yield(@page, self) if block_given? && injectable?
  end

  def injectable?
    ['Jekyll::Document', 'Jekyll::Page'].include?(page.class.name) || page.write? &&
      (page.output_ext == '.html' || page.permalink&.end_with?('/'))
  end

  def inject_scripts
    page.output.gsub!(%r{<\/head>}, prepare_scripts_for(:head))
  end

  def prepare_scripts_for(closing_tag)
    [load_scripts, "</#{closing_tag}>"].compact.join("\n")
  end

  def load_scripts
    <<~SCRIPT
      <script>
        alert('Hello World!')
      </script>
    SCRIPT
  end

  def config
    site.config
  end

  def site
    page.site
  end
end

# Register the hook
Jekyll::Hooks.register [:documents, :pages], :post_render do |page|
  JekyllMetrics.new(page) { |_page, metrics| metrics.inject_scripts }
end

What we’ve done so far:

  1. We’ve created a hook plugin which will register itself on the post_render event for the pages of types documents and pages;
  2. The #injectable? method checks if the current page is suitable for injection of the code;
  3. The #inject_scripts -> #prepare_scripts_for -> #load_scripts chain prepends the </head> tag with the content of the simple Hello World! script which we’ve just hardcoded for now;

As you refresh the page you should see the Hello World! alerting window.

Moving the scripts to a separate file

Now let’s get rid of the hardcoded script inside of our plugin module. Let’s create another directory called _includes and the file _includes/metrics.html with the following content:

test_project/_includes/metrics.html
<script>
  alert('Hello World from file!')
</script>

And we’ll change the plugin class to be able to read from include:

test_project/_plugins/test_project.rb
# Main metrics plugin class
class JekyllMetrics
  attr_accessor :page

  def initialize(page)
    @page = page
    yield(@page, self) if block_given? && injectable?
  end

  def injectable?
    ['Jekyll::Document', 'Jekyll::Page'].include?(page.class.name) || page.write? &&
      (page.output_ext == '.html' || page.permalink&.end_with?('/'))
  end

  def inject_scripts
    page.output.gsub!(%r{<\/head>}, load_scripts)
  end

  def prepare_scripts_for(closing_tag)
    [load_scripts, "</#{closing_tag}>"].compact.join("\n")
  end

  def load_scripts
    File.read(metrics_template)
  end

  def metrics_template
    root_path.join('_includes/metrics.html')
  end

  def root_path
    Pathname.new(site.source)
  end

  def config
    site.config
  end

  def site
    page.site
  end
end

# Register the hook
Jekyll::Hooks.register [:documents, :pages], :post_render do |page|
  JekyllMetrics.new(page) { |_page, metrics| metrics.inject_scripts }
end

What we’ve changed here is the #load_scripts method which will read the file from the _includes/metrics.html template for now.

Try to reload the page at 127.0.0.1:15000 to check that everything is working fine. You should see the new alerting window: Hello World from file!

Adding some configuration options

Now we’ll add some configuration options for our script.

Open the _config.yml file in the root of your project and add the following content there:

test_project/_config.yml
test_project:
  template: _includes/metrics.html
  google_analytics_id: 11-111111111-1
  yandex_metrica_id: 22222222

Update the content of _includes/metrics.html:

test_project/_includes/metrics.html
<script>
  alert('Your Yandex Metrica ID: {{ yandex_metrica_id }}')
</script>

<script>
  alert('Your Google Analytics ID: {{ google_analytics_id }}')
</script>

And we will modify our plugin code again to make it work with the config:

test_project/_plugins/test_project.rb
# Main metrics plugin class
class JekyllMetrics
  CONFIG_NAME = 'test_project'.freeze
  DEFAULT_CONFIG = {
    'template' => '_includes/metrics.html',
    'google_analytics_id' => 'XX-XXXXXXXXX-X',
    'yandex_metrica_id' => 'XXXXXXXX'
  }.freeze

  attr_accessor :page

  def initialize(page)
    @page = page
    yield(@page, self) if block_given? && injectable?
  end

  def injectable?
    ['Jekyll::Document', 'Jekyll::Page'].include?(page.class.name) || page.write? &&
      (page.output_ext == '.html' || page.permalink&.end_with?('/'))
  end

  def inject_scripts
    page.output.gsub!(%r{<\/head>}, load_scripts)
  end

  def prepare_scripts_for(closing_tag)
    [load_scripts, "</#{closing_tag}>"].compact.join("\n")
  end

  def load_scripts
    render_template(File.read(metrics_template))
  end

  def render_template(file)
    Liquid::Template.parse(file).render(plugin_config)
  end

  def metrics_template
    root_path.join(plugin_config['template'])
  end

  def root_path
    Pathname.new(site.source)
  end

  def plugin_config
    @plugin_config ||= DEFAULT_CONFIG.merge(config[CONFIG_NAME].to_h)
  end

  def config
    site.config
  end

  def site
    page.site
  end
end

# Register the hook
Jekyll::Hooks.register [:documents, :pages], :post_render do |page|
  JekyllMetrics.new(page) { |_page, metrics| metrics.inject_scripts }
end

What we’ve added now:

  1. The #plugin_config method reads the specific plugin configuration that we’ve entered to the _config.yml file;
  2. The #metrics_template will now use the file name from the configuration with the possibility to fallback to the default path;
  3. The #render_template method will preprocess the Liquid template and populate it with variables from our plugin configuration;

The following two alerts should appear:

  • Your Yandex Metrica ID: 22222222
  • Your Google Analytics ID: 11-111111111-1

If everything went well, let’s now add the real trackers code to our template _includes/metrics.html:

test_project/_includes/metrics.html
<!-- Global site tag (gtag.js) - Google Analytics -->
<script async src="https://www.googletagmanager.com/gtag/js?id="></script>
<script>
  window.dataLayer = window.dataLayer || [];
  function gtag(){dataLayer.push(arguments);}
  gtag('js', new Date());

  gtag('config', '');
</script>
<!-- End Global site tag (gtag.js) - Google Analytics -->

<!-- Yandex.Metrika counter -->
<script type="text/javascript" >
   (function(m,e,t,r,i,k,a){m[i]=m[i]||function(){(m[i].a=m[i].a||[]).push(arguments)};
   m[i].l=1*new Date();k=e.createElement(t),a=e.getElementsByTagName(t)[0],k.async=1,k.src=r,a.parentNode.insertBefore(k,a)})
   (window, document, "script", "https://mc.yandex.ru/metrika/tag.js", "ym");

   ym(, "init", {
        clickmap:true,
        trackLinks:true,
        accurateTrackBounce:true
   });
</script>
<noscript><div><img src="https://mc.yandex.ru/watch/" style="position:absolute; left:-9999px;" alt="" /></div></noscript>
<!-- /Yandex.Metrika counter -->

Now as you reload the page and open the source code of it, you will see the boths counters included with the proper ID substitutions from the configuration file.

And that’s it for now!

Continue reading on the next part of the article to find out how to convert the plugin to a separate gem.

The source code of the plugin

The full source code of the plugin may be found on github. It may slightly differ from the one described in the article. Please follow commits to find out what changed.