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.

In This Guide

Gem Architecture and Modular Design Patterns

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

Cloudflare API Integration and Ruby SDK Development

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

Advanced Jekyll Plugin Development with Custom Generators

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)

GitHub Actions Integration and Automation Hooks

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

Comprehensive Gem Testing and CI/CD Integration

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

Gem Distribution and Dependency Management

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.