Developing custom Ruby gems extends Jekyll's capabilities with seamless Cloudflare and GitHub integrations. Advanced gem development involves creating sophisticated plugins that handle API interactions, content transformations, and deployment automation while maintaining Ruby best practices. This guide explores professional gem development patterns that create robust, maintainable integrations between Jekyll, Cloudflare's edge platform, and GitHub's development ecosystem.
A well-architected gem separates concerns into logical modules while providing a clean API for users. The architecture should support extensibility, configuration management, and error handling across different integration points.
The gem structure combines Jekyll plugins, Cloudflare API clients, GitHub integration modules, and utility classes. Each component is designed as a separate module that can be used independently or together. Configuration management uses Ruby's convention-over-configuration pattern with sensible defaults and environment variable support.
# lib/jekyll-cloudflare-github/architecture.rb
module Jekyll
module CloudflareGitHub
# Main namespace module
VERSION = '1.0.0'
# Core configuration class
class Configuration
attr_accessor :cloudflare_api_token, :cloudflare_account_id,
:cloudflare_zone_id, :github_token, :github_repository,
:auto_deploy, :cache_purge_strategy
def initialize
@cloudflare_api_token = ENV['CLOUDFLARE_API_TOKEN']
@cloudflare_account_id = ENV['CLOUDFLARE_ACCOUNT_ID']
@cloudflare_zone_id = ENV['CLOUDFLARE_ZONE_ID']
@github_token = ENV['GITHUB_TOKEN']
@auto_deploy = true
@cache_purge_strategy = :selective
end
end
# Dependency injection container
class Container
def self.configure
yield(configuration) if block_given?
end
def self.configuration
@configuration ||= Configuration.new
end
def self.cloudflare_client
@cloudflare_client ||= Cloudflare::Client.new(configuration.cloudflare_api_token)
end
def self.github_client
@github_client ||= GitHub::Client.new(configuration.github_token)
end
end
# Error hierarchy
class Error < StandardError; end
class ConfigurationError < Error; end
class APIAuthenticationError < Error; end
class DeploymentError < Error; end
# Utility module for common operations
module Utils
def self.log(message, level = :info)
Jekyll.logger.send(level, "[JekyllCloudflareGitHub] #{message}")
end
def self.track_operation(name, &block)
start_time = Time.now
result = block.call
elapsed = ((Time.now - start_time) * 1000).round(2)
log("Operation #{name} completed in #{elapsed}ms", :debug)
result
rescue => e
log("Operation #{name} failed: #{e.message}", :error)
raise
end
end
end
end
A sophisticated Cloudflare Ruby SDK provides comprehensive API coverage with intelligent error handling, request retries, and response caching. The SDK should support all essential Cloudflare features including Pages, Workers, KV, R2, and Cache Purge.
# lib/jekyll-cloudflare-github/cloudflare/client.rb
module Jekyll
module CloudflareGitHub
module Cloudflare
class Client
BASE_URL = 'https://api.cloudflare.com/client/v4'
def initialize(api_token, account_id = nil)
@api_token = api_token
@account_id = account_id
@connection = build_connection
end
# Pages API
def create_pages_deployment(project_name, files, branch = 'main', env_vars = {})
endpoint = "/accounts/#{@account_id}/pages/projects/#{project_name}/deployments"
response = @connection.post(endpoint) do |req|
req.headers['Content-Type'] = 'multipart/form-data'
req.body = build_pages_payload(files, branch, env_vars)
end
handle_response(response)
end
def purge_cache(urls = [], tags = [], hosts = [])
endpoint = "/zones/#{@zone_id}/purge_cache"
payload = {}
payload[:files] = urls if urls.any?
payload[:tags] = tags if tags.any?
payload[:hosts] = hosts if hosts.any?
response = @connection.post(endpoint) do |req|
req.body = payload.to_json
end
handle_response(response)
end
# Workers KV operations
def write_kv(namespace_id, key, value, metadata = {})
endpoint = "/accounts/#{@account_id}/storage/kv/namespaces/#{namespace_id}/values/#{key}"
response = @connection.put(endpoint) do |req|
req.body = value
req.headers['Content-Type'] = 'text/plain'
metadata.each { |k, v| req.headers["#{k}"] = v.to_s }
end
response.success?
end
# R2 storage operations
def upload_to_r2(bucket_name, key, content, content_type = 'application/octet-stream')
endpoint = "/accounts/#{@account_id}/r2/buckets/#{bucket_name}/objects/#{key}"
response = @connection.put(endpoint) do |req|
req.body = content
req.headers['Content-Type'] = content_type
end
handle_response(response)
end
private
def build_connection
Faraday.new(url: BASE_URL) do |conn|
conn.request :retry, max: 3, interval: 0.05,
interval_randomness: 0.5, backoff_factor: 2
conn.request :authorization, 'Bearer', @api_token
conn.request :json
conn.response :json, content_type: /\bjson$/
conn.response :raise_error
conn.adapter Faraday.default_adapter
end
end
def build_pages_payload(files, branch, env_vars)
# Build multipart form data for Pages deployment
{
'files' => files.map { |f| Faraday::UploadIO.new(f, 'application/octet-stream') },
'branch' => branch,
'env_vars' => env_vars.to_json
}
end
def handle_response(response)
if response.success?
response.body
else
raise APIAuthenticationError, "Cloudflare API error: #{response.body['errors']}"
end
end
end
# Specialized cache manager
class CacheManager
def initialize(client, zone_id)
@client = client
@zone_id = zone_id
@purge_queue = []
end
def queue_purge(url)
@purge_queue << url
# Auto-purge when queue reaches threshold
if @purge_queue.size >= 30
flush_purge_queue
end
end
def flush_purge_queue
return if @purge_queue.empty?
@client.purge_cache(@purge_queue)
@purge_queue.clear
end
def selective_purge_for_jekyll(site)
# Identify changed URLs for selective cache purging
changed_urls = detect_changed_urls(site)
changed_urls.each { |url| queue_purge(url) }
flush_purge_queue
end
private
def detect_changed_urls(site)
# Compare current build with previous to identify changes
previous_manifest = load_previous_manifest
current_manifest = generate_current_manifest(site)
changed_files = compare_manifests(previous_manifest, current_manifest)
convert_files_to_urls(changed_files, site)
end
end
end
end
end
Jekyll plugins extend functionality through generators, converters, commands, and tags. Advanced plugins integrate seamlessly with Jekyll's lifecycle while providing powerful new capabilities.
# lib/jekyll-cloudflare-github/generators/deployment_generator.rb
module Jekyll
module CloudflareGitHub
class DeploymentGenerator < Generator
safe true
priority :low
def generate(site)
@site = site
@config = Container.configuration
return unless should_deploy?
prepare_deployment
deploy_to_cloudflare
post_deployment_cleanup
end
private
def should_deploy?
@config.auto_deploy &&
ENV['JEKYLL_ENV'] == 'production' &&
!ENV['SKIP_DEPLOYMENT']
end
def prepare_deployment
Utils.track_operation('prepare_deployment') do
# Optimize assets for deployment
optimize_assets
# Generate deployment manifest
generate_manifest
# Create deployment package
create_deployment_package
end
end
def deploy_to_cloudflare
Utils.track_operation('deploy_to_cloudflare') do
client = Container.cloudflare_client
# Create Pages deployment
deployment = client.create_pages_deployment(
@config.project_name,
deployment_files,
@config.branch,
environment_variables
)
# Monitor deployment status
monitor_deployment(deployment['id'])
# Update DNS if needed
update_dns_records if @config.update_dns
end
end
def deployment_files
# Package site directory for deployment
Dir.glob(File.join(@site.dest, '**/*')).map do |file|
next if File.directory?(file)
file
end.compact
end
def environment_variables
{
'JEKYLL_ENV' => 'production',
'BUILD_TIME' => Time.now.iso8601,
'GIT_COMMIT' => git_commit_sha,
'SITE_URL' => @site.config['url']
}
end
def monitor_deployment(deployment_id)
client = Container.cloudflare_client
max_attempts = 60
attempt = 0
while attempt < max_attempts
status = client.deployment_status(deployment_id)
case status['status']
when 'success'
Utils.log("Deployment #{deployment_id} completed successfully")
return true
when 'failed'
raise DeploymentError, "Deployment failed: #{status['error']}"
else
attempt += 1
sleep 5
end
end
raise DeploymentError, "Deployment timed out after #{max_attempts * 5} seconds"
end
end
# Asset optimization plugin
class AssetOptimizer < Generator
def generate(site)
@site = site
optimize_images
compress_text_assets
generate_webp_versions
end
def optimize_images
return unless defined?(ImageOptim)
image_optim = ImageOptim.new(
pngout: false,
svgo: false,
allow_lossy: true
)
Dir.glob(File.join(@site.source, 'assets/images/**/*.{jpg,jpeg,png,gif}')).each do |image_path|
next unless File.file?(image_path)
optimized = image_optim.optimize_image(image_path)
if optimized && optimized != image_path
FileUtils.mv(optimized, image_path)
Utils.log("Optimized #{image_path}", :debug)
end
end
end
end
# Custom Liquid filters for Cloudflare integration
module Filters
def cloudflare_cdn_url(input)
return input unless @context.registers[:site].config['cloudflare_cdn']
cdn_domain = @context.registers[:site].config['cloudflare_cdn_domain']
"#{cdn_domain}/#{input}"
end
def cloudflare_workers_url(path, worker_name = 'jekyll-assets')
worker_domain = @context.registers[:site].config['cloudflare_workers_domain']
"https://#{worker_name}.#{worker_domain}#{path}"
end
end
end
end
# Register Liquid filters
Liquid::Template.register_filter(Jekyll::CloudflareGitHub::Filters)
The gem provides GitHub Actions integration for automated workflows, including deployment, cache management, and synchronization between GitHub and Cloudflare.
# lib/jekyll-cloudflare-github/github/actions.rb
module Jekyll
module CloudflareGitHub
module GitHub
class Actions
def initialize(token, repository)
@client = Octokit::Client.new(access_token: token)
@repository = repository
end
def trigger_deployment_workflow(ref = 'main', inputs = {})
workflow_id = find_workflow_id('deploy.yml')
@client.create_workflow_dispatch(
@repository,
workflow_id,
ref,
inputs
)
end
def create_deployment_status(deployment_id, state, description = '')
@client.create_deployment_status(
@repository,
deployment_id,
state,
description: description,
environment_url: deployment_url(deployment_id)
)
end
def sync_to_cloudflare_pages(branch = 'main')
# Trigger Cloudflare Pages build via GitHub Actions
trigger_deployment_workflow(branch, {
environment: 'production',
skip_tests: false
})
end
def update_pull_request_deployment(pr_number, deployment_url)
comment = "## Deployment Preview\n\n" \
"🚀 Preview deployment ready: #{deployment_url}\n\n" \
"This deployment will be automatically updated with new commits."
@client.add_comment(@repository, pr_number, comment)
end
private
def find_workflow_id(filename)
workflows = @client.workflows(@repository)
workflow = workflows[:workflows].find { |w| w[:name] == filename }
workflow[:id] if workflow
end
end
# Webhook handler for GitHub events
class WebhookHandler
def self.handle_push(payload, config)
# Process push event for auto-deployment
if payload['ref'] == 'refs/heads/main'
deployer = DeploymentManager.new(config)
deployer.deploy(payload['after'])
end
end
def self.handle_pull_request(payload, config)
# Create preview deployment for PR
if payload['action'] == 'opened' || payload['action'] == 'synchronize'
pr_deployer = PRDeploymentManager.new(config)
pr_deployer.create_preview(payload['pull_request'])
end
end
end
end
end
end
# Rake tasks for common operations
namespace :jekyll do
namespace :cloudflare do
desc 'Deploy to Cloudflare Pages'
task :deploy do
require 'jekyll-cloudflare-github'
Jekyll::CloudflareGitHub::Deployer.new.deploy
end
desc 'Purge Cloudflare cache'
task :purge_cache do
require 'jekyll-cloudflare-github'
purger = Jekyll::CloudflareGitHub::Cloudflare::CachePurger.new
purger.purge_all
end
desc 'Sync GitHub content to Cloudflare KV'
task :sync_content do
require 'jekyll-cloudflare-github'
syncer = Jekyll::CloudflareGitHub::ContentSyncer.new
syncer.sync_all
end
end
end
Professional gem development requires comprehensive testing strategies including unit tests, integration tests, and end-to-end testing with real services.
# spec/spec_helper.rb
require 'jekyll-cloudflare-github'
require 'webmock/rspec'
require 'vcr'
RSpec.configure do |config|
config.before(:suite) do
# Setup test configuration
Jekyll::CloudflareGitHub::Container.configure do |c|
c.cloudflare_api_token = 'test-token'
c.cloudflare_account_id = 'test-account'
c.auto_deploy = false
end
end
config.around(:each) do |example|
# Use VCR for API testing
VCR.use_cassette(example.metadata[:vcr]) do
example.run
end
end
end
# spec/jekyll/cloudflare_git_hub/client_spec.rb
RSpec.describe Jekyll::CloudflareGitHub::Cloudflare::Client do
let(:client) { described_class.new('test-token', 'test-account') }
describe '#purge_cache' do
it 'purges specified URLs', vcr: 'cloudflare/purge_cache' do
result = client.purge_cache(['https://example.com/page1'])
expect(result['success']).to be true
end
end
describe '#create_pages_deployment' do
it 'creates a new deployment', vcr: 'cloudflare/create_deployment' do
files = [double('file', path: '_site/index.html')]
result = client.create_pages_deployment('test-project', files)
expect(result['id']).not_to be_nil
end
end
end
# spec/jekyll/generators/deployment_generator_spec.rb
RSpec.describe Jekyll::CloudflareGitHub::DeploymentGenerator do
let(:site) { double('site', config: {}, dest: '_site') }
let(:generator) { described_class.new }
before do
allow(generator).to receive(:site).and_return(site)
allow(ENV).to receive(:[]).with('JEKYLL_ENV').and_return('production')
end
describe '#generate' do
it 'prepares deployment when conditions are met' do
expect(generator).to receive(:should_deploy?).and_return(true)
expect(generator).to receive(:prepare_deployment)
expect(generator).to receive(:deploy_to_cloudflare)
generator.generate(site)
end
end
end
# Integration test with real Jekyll site
RSpec.describe 'Integration with Jekyll site' do
let(:source_dir) { File.join(__dir__, 'fixtures/site') }
let(:dest_dir) { File.join(source_dir, '_site') }
before do
@site = Jekyll::Site.new(Jekyll.configuration({
'source' => source_dir,
'destination' => dest_dir
}))
end
it 'processes site with Cloudflare GitHub plugin' do
expect { @site.process }.not_to raise_error
expect(File.exist?(File.join(dest_dir, 'index.html'))).to be true
end
end
# GitHub Actions workflow for gem CI/CD
# .github/workflows/test.yml
name: Test Gem
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
ruby: ['3.0', '3.1', '3.2']
steps:
- uses: actions/checkout@v4
- uses: ruby/setup-ruby@v1
with:
ruby-version: $
bundler-cache: true
- run: bundle exec rspec
- run: bundle exec rubocop
Proper gem distribution involves packaging, version management, and dependency handling with support for different Ruby and Jekyll versions.
# jekyll-cloudflare-github.gemspec
Gem::Specification.new do |spec|
spec.name = "jekyll-cloudflare-github"
spec.version = Jekyll::CloudflareGitHub::VERSION
spec.authors = ["Your Name"]
spec.email = ["your.email@example.com"]
spec.summary = "Advanced integration between Jekyll, Cloudflare, and GitHub"
spec.description = "Provides seamless deployment, caching, and synchronization between Jekyll sites, Cloudflare's edge platform, and GitHub workflows"
spec.homepage = "https://github.com/yourusername/jekyll-cloudflare-github"
spec.license = "MIT"
spec.required_ruby_version = ">= 2.7.0"
spec.required_rubygems_version = ">= 3.0.0"
spec.files = Dir["lib/**/*", "README.md", "LICENSE.txt", "CHANGELOG.md"]
spec.require_paths = ["lib"]
# Runtime dependencies
spec.add_runtime_dependency "jekyll", ">= 4.0", "< 5.0"
spec.add_runtime_dependency "faraday", "~> 2.0"
spec.add_runtime_dependency "octokit", "~> 5.0"
spec.add_runtime_dependency "rake", "~> 13.0"
# Optional dependencies
spec.add_development_dependency "rspec", "~> 3.11"
spec.add_development_dependency "webmock", "~> 3.18"
spec.add_development_dependency "vcr", "~> 6.1"
spec.add_development_dependency "rubocop", "~> 1.36"
spec.add_development_dependency "rubocop-rspec", "~> 2.13"
# Platform-specific dependencies
spec.add_development_dependency "image_optim", "~> 0.32", :platform => [:ruby]
# Metadata for RubyGems.org
spec.metadata = {
"bug_tracker_uri" => "#{spec.homepage}/issues",
"changelog_uri" => "#{spec.homepage}/blob/main/CHANGELOG.md",
"documentation_uri" => "#{spec.homepage}/blob/main/README.md",
"homepage_uri" => spec.homepage,
"source_code_uri" => spec.homepage,
"rubygems_mfa_required" => "true"
}
end
# Gem installation and setup instructions
module Jekyll
module CloudflareGitHub
class Installer
def self.run
puts "Installing jekyll-cloudflare-github..."
puts "Please set the following environment variables:"
puts " export CLOUDFLARE_API_TOKEN=your_api_token"
puts " export CLOUDFLARE_ACCOUNT_ID=your_account_id"
puts " export GITHUB_TOKEN=your_github_token"
puts ""
puts "Add to your Jekyll _config.yml:"
puts "plugins:"
puts " - jekyll-cloudflare-github"
puts ""
puts "Available Rake tasks:"
puts " rake jekyll:cloudflare:deploy # Deploy to Cloudflare Pages"
puts " rake jekyll:cloudflare:purge_cache # Purge Cloudflare cache"
end
end
end
end
# Version management and compatibility
module Jekyll
module CloudflareGitHub
class Compatibility
SUPPORTED_JEKYLL_VERSIONS = ['4.0', '4.1', '4.2', '4.3']
SUPPORTED_RUBY_VERSIONS = ['2.7', '3.0', '3.1', '3.2']
def self.check
check_jekyll_version
check_ruby_version
check_dependencies
end
def self.check_jekyll_version
jekyll_version = Gem::Version.new(Jekyll::VERSION)
supported = SUPPORTED_JEKYLL_VERSIONS.any? do |v|
jekyll_version >= Gem::Version.new(v)
end
unless supported
raise CompatibilityError,
"Jekyll #{Jekyll::VERSION} is not supported. " \
"Please use one of: #{SUPPORTED_JEKYLL_VERSIONS.join(', ')}"
end
end
end
end
end
This advanced Ruby gem provides a comprehensive integration between Jekyll, Cloudflare, and GitHub. It enables sophisticated deployment workflows, real-time synchronization, and performance optimizations while maintaining Ruby gem development best practices. The gem is production-ready with comprehensive testing, proper version management, and excellent developer experience.