Розробка кастомного плагіна Jekyll (Ruby)
Jekyll написаний на Ruby і надає повноцінний API розширення через плагіни. Плагіни — це Ruby-класи, які вбудовуються в pipeline генерації сайту. Через них можна додати нові теги Liquid, фільтри, генератори сторінок, конвертори форматів, гуки. GitHub Pages не запускає плагіни (тільки білий список) — потрібен власний CI/CD.
Типи плагінів та коли які використовувати
| Тип | Суперклас | Застосування |
|---|---|---|
| Generator | Jekyll::Generator |
Створення сторінок програмно, агрегація даних |
| Converter | Jekyll::Converter |
Нові формати контенту (AsciiDoc, reStructuredText) |
| Command | Jekyll::Command |
Нові CLI-команди (jekyll mycommand) |
| Tag | Liquid::Tag |
Кастомні теги {% mytag %} |
| Block | Liquid::Block |
Теги з контентом {% block %}...{% endblock %} |
| Filter | включення в Liquid::Template.register_filter |
Кастомні фільтри {{ value | myfilter }} |
Структура плагіна
Плагіни розташовуються в _plugins/:
_plugins/
├── image_optimizer.rb
├── reading_time.rb
├── related_posts.rb
└── generators/
└── tag_pages.rb
Приклад 1: Кастомний фільтр
Фільтр для форматування числа в англійський формат:
# _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} хв"
end
end
Liquid::Template.register_filter(NumberFormatFilter)
Використання в шаблоні:
{{ 1234567 | en_number }} → 1,234,567
{{ 9990.5 | en_currency }} → $9,990
{{ page.content | reading_time }} → 5 хв
Приклад 2: Кастомний тег з параметрами
Тег для вставки відео з 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'] || 'Відео'
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)
Використання:
{% video_embed provider="youtube" id="dQw4w9WgXcQ" title="Демо проекту" aspect="16-9" %}
Приклад 3: Generator для сторінок тегів
Jekyll нативно генерує _site/tags/ тільки через сторонні плагіни. Реалізація:
# _plugins/generators/tag_pages.rb
module Jekyll
class TagPageGenerator < Generator
safe true
priority :low
def generate(site)
# Зібрати всі теги зі всіх постів
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
# Створити індексну сторінку всіх тегів
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'] = "Пости з тегом: #{tag}"
self.data['description'] = "Усі матеріали по темі «#{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'] = 'Усі теги'
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
Приклад 4: Гуки для постобробки
# _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?
# Базова мініфікація HTML (убрати лишні пробіли між тегами)
doc.output = doc.output
.gsub(/>\s+</, '><')
.gsub(/\s{2,}/, ' ')
.strip
end
# Гук після запису файлу
Jekyll::Hooks.register :site, :post_write do |site|
puts " Сайт зібраний: #{site.pages.length} сторінок, #{site.posts.docs.length} постів"
puts " Вихідна директорія: #{site.dest}"
end
Тестування плагіна
# 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 'форматує тисячі з комою' do
expect(en_number(1234567)).to eq('1,234,567')
end
it 'форматує десяткові дроби' do
expect(en_number(1234.5, 2)).to eq('1,234.50')
end
end
describe '#reading_time' do
it 'обчислює час читання' do
content = Array.new(400, 'слово').join(' ')
expect(reading_time(content)).to eq('2 хв')
end
end
end
Розповсюдження як gem
# myplugin.gemspec
Gem::Specification.new do |spec|
spec.name = "jekyll-myplugin"
spec.version = "1.0.0"
spec.authors = ["Ваше ім'я"]
spec.summary = "Опис плагіна"
spec.files = Dir["lib/**/*", "LICENSE"]
spec.require_paths = ["lib"]
spec.add_dependency "jekyll", ">= 4.0"
end
Терміни
Простий фільтр або тег — полдня — 1 день. Generator для сторінок тегів/авторів — 2–3 дні. Складний плагін з обробкою зображень, зовнішніми API, тестами — 1–2 тижні.







