Jekyll Custom Ruby Plugin Development

Our company is engaged in the development, support and maintenance of sites of any complexity. From simple one-page sites to large-scale cluster systems built on micro services. Experience of developers is confirmed by certificates from vendors.
Development and maintenance of all types of websites:
Informational websites or web applications
Business card websites, landing pages, corporate websites, online catalogs, quizzes, promo websites, blogs, news resources, informational portals, forums, aggregators
E-commerce websites or web applications
Online stores, B2B portals, marketplaces, online exchanges, cashback websites, exchanges, dropshipping platforms, product parsers
Business process management web applications
CRM systems, ERP systems, corporate portals, production management systems, information parsers
Electronic service websites or web applications
Classified ads platforms, online schools, online cinemas, website builders, portals for electronic services, video hosting platforms, thematic portals

These are just some of the technical types of websites we work with, and each of them can have its own specific features and functionality, as well as be customized to meet the specific needs and goals of the client.

Our competencies:
Development stages
Latest works
  • image_web-applications_feedme_466_0.webp
    Development of a web application for FEEDME
    1161
  • image_ecommerce_furnoro_435_0.webp
    Development of an online store for the company FURNORO
    1041
  • image_crm_enviok_479_0.webp
    Development of a web application for Enviok
    822
  • image_crm_chasseurs_493_0.webp
    CRM development for Chasseurs
    847
  • image_website-sbh_0.png
    Website development for SBH Partners
    999
  • image_website-_0.png
    Website development for Red Pear
    451

Developing Custom Jekyll Plugins (Ruby)

Jekyll is written in Ruby and provides a full extension API through plugins. Plugins are Ruby classes that are embedded in the site generation pipeline. They can add custom Liquid tags, filters, page generators, format converters, and hooks. GitHub Pages doesn't run plugins (only a whitelist) — you need your own CI/CD.

Plugin types and when to use each

Type Superclass Use case
Generator Jekyll::Generator Create pages programmatically, aggregate data
Converter Jekyll::Converter New content formats (AsciiDoc, reStructuredText)
Command Jekyll::Command New CLI commands (jekyll mycommand)
Tag Liquid::Tag Custom tags {% mytag %}
Block Liquid::Block Tags with content {% block %}...{% endblock %}
Filter register in Liquid::Template.register_filter Custom filters {{ value | myfilter }}

Plugin structure

Plugins are placed in _plugins/:

_plugins/
├── image_optimizer.rb
├── reading_time.rb
├── related_posts.rb
└── generators/
    └── tag_pages.rb

Example 1: Custom filter

Filter for formatting a number in US format:

# _plugins/filters/number_format.rb
module NumberFormatFilter
  def en_number(number, decimals = 0)
    return number unless number.is_a?(Numeric)

    formatted = number.to_f.round(decimals)
    parts = formatted.to_s.split('.')
    integer_part = parts[0].gsub(/(\d)(?=(\d{3})+$)/, '\1,')

    if decimals > 0 && parts[1]
      "#{integer_part}.#{parts[1].ljust(decimals, '0')}"
    else
      integer_part
    end
  end

  def en_currency(number, currency = '$')
    "#{currency}#{en_number(number)}"
  end

  def reading_time(content)
    words = content.split.length
    minutes = (words / 200.0).ceil
    "#{minutes} min"
  end
end

Liquid::Template.register_filter(NumberFormatFilter)

Usage in template:

{{ 1234567 | en_number }}        → 1,234,567
{{ 9990.5 | en_currency }}       → $9,990
{{ page.content | reading_time }} → 5 min

Example 2: Custom tag with parameters

Tag for embedding video with lazy loading:

# _plugins/tags/video_embed.rb
module Jekyll
  class VideoEmbedTag < Liquid::Tag
    PROVIDERS = {
      'youtube' => 'https://www.youtube.com/embed/%s',
      'vimeo'   => 'https://player.vimeo.com/video/%s',
    }.freeze

    def initialize(tag_name, markup, tokens)
      super
      @params = {}
      markup.scan(/(\w+)="([^"]*)"/) do |key, value|
        @params[key] = value
      end
    end

    def render(context)
      provider = @params['provider'] || 'youtube'
      video_id = @params['id']
      title    = @params['title'] || 'Video'
      aspect   = @params['aspect'] || '16-9'

      return "<!-- video_embed: missing id -->" unless video_id

      url = format(PROVIDERS[provider], video_id)

      <<~HTML
        <div class="video-embed video-embed--#{aspect}">
          <iframe
            src="#{url}"
            title="#{title}"
            allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
            allowfullscreen
            loading="lazy"
          ></iframe>
        </div>
      HTML
    end
  end
end

Liquid::Template.register_tag('video_embed', Jekyll::VideoEmbedTag)

Usage:

{% video_embed provider="youtube" id="dQw4w9WgXcQ" title="Project demo" aspect="16-9" %}

Example 3: Generator for tag pages

Jekyll natively generates _site/tags/ only through third-party plugins. Implementation:

# _plugins/generators/tag_pages.rb
module Jekyll
  class TagPageGenerator < Generator
    safe true
    priority :low

    def generate(site)
      # Collect all tags from all posts
      all_tags = site.posts.docs.flat_map { |post|
        post.data['tags'] || []
      }.uniq.sort

      all_tags.each do |tag|
        site.pages << TagPage.new(site, site.source, tag)
      end

      # Create tag index page
      site.pages << TagIndexPage.new(site, site.source, all_tags)
    end
  end

  class TagPage < Page
    def initialize(site, base, tag)
      @site = site
      @base = base
      @dir  = File.join('tags', Jekyll::Utils.slugify(tag))
      @name = 'index.html'

      process(@name)
      read_yaml(File.join(base, '_layouts'), 'tag.html')

      self.data['tag']         = tag
      self.data['title']       = "Posts tagged: #{tag}"
      self.data['description'] = "All posts about #{tag}"

      # Get all posts with this tag
      self.data['tag_posts'] = site.posts.docs.select { |post|
        (post.data['tags'] || []).include?(tag)
      }.sort_by { |post| post.date }.reverse
    end
  end

  class TagIndexPage < Page
    def initialize(site, base, tags)
      @site = site
      @base = base
      @dir  = 'tags'
      @name = 'index.html'

      process(@name)
      read_yaml(File.join(base, '_layouts'), 'tags-index.html')

      self.data['title'] = 'All tags'
      self.data['tags_with_counts'] = tags.map { |tag|
        count = site.posts.docs.count { |post|
          (post.data['tags'] || []).include?(tag)
        }
        { 'name' => tag, 'slug' => Jekyll::Utils.slugify(tag), 'count' => count }
      }.sort_by { |t| -t['count'] }
    end
  end
end

Example 4: Hooks for post-processing

# _plugins/hooks/minify_html.rb
Jekyll::Hooks.register [:pages, :documents], :post_render do |doc|
  next unless doc.output_ext == '.html'
  next if doc.output.nil? || doc.output.empty?

  # Basic HTML minification (remove extra spaces between tags)
  doc.output = doc.output
    .gsub(/>\s+</, '><')
    .gsub(/\s{2,}/, ' ')
    .strip
end

# Hook after file write
Jekyll::Hooks.register :site, :post_write do |site|
  puts "  Site built: #{site.pages.length} pages, #{site.posts.docs.length} posts"
  puts "  Output directory: #{site.dest}"
end

Testing plugins

# spec/plugins/number_format_spec.rb
require 'jekyll'
require_relative '../../_plugins/filters/number_format'

RSpec.describe NumberFormatFilter do
  include NumberFormatFilter

  describe '#en_number' do
    it 'formats thousands with comma' do
      expect(en_number(1234567)).to eq('1,234,567')
    end

    it 'formats decimals' do
      expect(en_number(1234.5, 2)).to eq('1,234.50')
    end
  end

  describe '#reading_time' do
    it 'calculates reading time' do
      content = Array.new(400, 'word').join(' ')
      expect(reading_time(content)).to eq('2 min')
    end
  end
end

Distribution as gem

# myplugin.gemspec
Gem::Specification.new do |spec|
  spec.name        = "jekyll-myplugin"
  spec.version     = "1.0.0"
  spec.authors     = ["Your Name"]
  spec.summary     = "Plugin description"
  spec.files       = Dir["lib/**/*", "LICENSE"]
  spec.require_paths = ["lib"]
  spec.add_dependency "jekyll", ">= 4.0"
end

Timeline

Simple filter or tag — half day to 1 day. Generator for tag/author pages — 2–3 days. Complex plugin with image processing, external APIs, tests — 1–2 weeks.