You love Jekyll's simplicity but need dynamic features like personalization, A/B testing, or form handling. Cloudflare Workers offer edge computing capabilities, but integrating them with your Jekyll workflow feels disconnected. You're writing Workers in JavaScript while your site is in Ruby/Jekyll, creating context switching and maintenance headaches. The solution is using Ruby gems that bridge this gap, allowing you to develop, test, and deploy Workers using Ruby while seamlessly integrating them with your Jekyll site.

In This Article

Understanding Workers and Jekyll Synergy

Cloudflare Workers run JavaScript at Cloudflare's edge locations worldwide, allowing you to modify requests and responses. When combined with Jekyll, you get the best of both worlds: Jekyll handles content generation during build time, while Workers handle dynamic aspects at runtime, closer to users. This architecture is called "dynamic static sites" or "Jamstack with edge functions."

The synergy is powerful: Workers can personalize content, handle forms, implement A/B testing, add authentication, and more—all without requiring a backend server. Since Workers run at the edge, they add negligible latency. For Jekyll users, this means you can keep your simple static site workflow while gaining dynamic capabilities. Ruby gems make this integration smoother by providing tools to develop, test, and deploy Workers as part of your Ruby-based Jekyll workflow.

Workers Capabilities for Jekyll Sites

Worker Function Benefit for Jekyll Ruby Integration Approach
Personalization Show different content based on visitor attributes Ruby gem generates Worker config from analytics data
A/B Testing Test content variations without rebuilding Ruby manages test variations and analyzes results
Form Handling Process forms without third-party services Ruby gem generates form handling Workers
Authentication Protect private content or admin areas Ruby manages user accounts and permissions
API Composition Combine multiple APIs into single response Ruby defines API schemas and response formats
Edge Caching Logic Smart caching beyond static files Ruby analyzes traffic patterns to optimize caching
Bot Detection Block malicious bots before they reach site Ruby updates bot signatures and rules

Ruby Gems for Workers Development

Several gems facilitate Workers development in Ruby:

1. cloudflare-workers - Official Ruby SDK

gem 'cloudflare-workers'

# Configure client
client = CloudflareWorkers::Client.new(
  account_id: ENV['CF_ACCOUNT_ID'],
  api_token: ENV['CF_API_TOKEN']
)

# Create a Worker
worker = client.workers.create(
  name: 'jekyll-personalizer',
  script:  ~JS
    addEventListener('fetch', event => {
      event.respondWith(handleRequest(event.request))
    })
    
    async function handleRequest(request) {
      // Your Worker logic here
    }
  JS
)

# Deploy to route
client.workers.routes.create(
  pattern: 'yourdomain.com/*',
  script: 'jekyll-personalizer'
)

2. wrangler-ruby - Wrangler CLI Wrapper

gem 'wrangler-ruby'

# Run wrangler commands from Ruby
wrangler = Wrangler::CLI.new(
  config_path: 'wrangler.toml',
  environment: 'production'
)

# Build and deploy
wrangler.build
wrangler.publish

# Manage secrets
wrangler.secret.set('API_KEY', ENV['SOME_API_KEY'])
wrangler.kv.namespace.create('jekyll_data')
wrangler.kv.key.put('trending_posts', trending_posts_json)

3. workers-rs - Write Workers in Rust via Ruby FFI

While not pure Ruby, you can compile Rust Workers and deploy via Ruby:

gem 'workers-rs'

# Build Rust Worker
worker = WorkersRS::Builder.new('src/worker.rs')
worker.build

# The Rust code (compiles to WebAssembly)
# #[wasm_bindgen]
# pub fn handle_request(req: Request) -> Result {
#     // Rust logic here
# }

# Deploy via Ruby
worker.deploy_to_cloudflare

4. ruby2js - Write Workers in Ruby, Compile to JavaScript

gem 'ruby2js'

# Write Worker logic in Ruby
ruby_code =  ~RUBY
  add_event_listener('fetch') do |event|
    event.respond_with(handle_request(event.request))
  end
  
  def handle_request(request)
    # Ruby logic here
    if request.headers['CF-IPCountry'] == 'US'
      # Personalize for US visitors
    end
    
    fetch(request)
  end
RUBY

# Compile to JavaScript
js_code = Ruby2JS.convert(ruby_code, filters: [:functions, :es2015])

# Deploy
client.workers.create(name: 'ruby-worker', script: js_code)

Jekyll Specific Workers Integration

Create tight integration between Jekyll and Workers:

# _plugins/workers_integration.rb
module Jekyll
  class WorkersGenerator < Generator
    def generate(site)
      # Generate Worker scripts based on site content
      generate_personalization_worker(site)
      generate_ab_testing_workers(site)
      generate_form_handlers(site)
    end
    
    def generate_personalization_worker(site)
      # Analyze site structure for personalization opportunities
      personalized_pages = site.pages.select do |page|
        page.data['personalizable'] || 
        page.url.include?('blog/') ||
        page.data['layout'] == 'post'
      end
      
      # Generate Worker that injects personalization
      worker_script =  ~JS
        addEventListener('fetch', event => {
          event.respondWith(handleRequest(event.request))
        })
        
        async function handleRequest(request) {
          const response = await fetch(request)
          const country = request.headers.get('CF-IPCountry')
          
          // Clone response to modify
          const newResponse = new Response(response.body, response)
          
          // Add personalization header for CSS/JS to use
          newResponse.headers.set('X-Visitor-Country', country)
          
          return newResponse
        }
      JS
      
      # Write to file
      File.write('_workers/personalization.js', worker_script)
      
      # Add to site data for deployment
      site.data['workers'] ||= []
      site.data['workers']   {
        name: 'personalization',
        script: '_workers/personalization.js',
        routes: ['yourdomain.com/*']
      }
    end
    
    def generate_form_handlers(site)
      # Find all forms in site
      forms = []
      
      site.pages.each do |page|
        content = page.content
        if content.include?(' {
          if (event.request.method === 'POST') {
            event.respondWith(handleFormSubmission(event.request))
          } else {
            event.respondWith(fetch(event.request))
          }
        })
        
        async function handleFormSubmission(request) {
          const formData = await request.formData()
          const data = {}
          
          // Extract form data
          for (const [key, value] of formData.entries()) {
            data[key] = value
          }
          
          // Send to external service (e.g., email, webhook)
          await sendToWebhook(data)
          
          // Redirect to thank you page
          return Response.redirect('${form[:page]}/thank-you', 303)
        }
        
        async function sendToWebhook(data) {
          // Send to Discord, Slack, email, etc.
          await fetch('https://discord.com/api/webhooks/...', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({
              content: \`New form submission from \${data.email || 'anonymous'}\`
            })
          })
        }
      JS
    end
  end
end

Implementing Edge Side Includes with Workers

ESI allows dynamic content injection into static pages:

# lib/workers/esi_generator.rb
class ESIGenerator
  def self.generate_esi_worker(site)
    # Identify dynamic sections in static pages
    dynamic_sections = find_dynamic_sections(site)
    
    worker_script =  ~JS
      import { HTMLRewriter } from 'https://gh.workers.dev/v1.6.0/deno.land/x/html_rewriter@v0.1.0-beta.12/index.js'
      
      addEventListener('fetch', event => {
        event.respondWith(handleRequest(event.request))
      })
      
      async function handleRequest(request) {
        const response = await fetch(request)
        const contentType = response.headers.get('Content-Type')
        
        if (!contentType || !contentType.includes('text/html')) {
          return response
        }
        
        return new HTMLRewriter()
          .on('esi-include', {
            element(element) {
              const src = element.getAttribute('src')
              if (src) {
                // Fetch and inject dynamic content
                element.replace(fetchDynamicContent(src, request), { html: true })
              }
            }
          })
          .transform(response)
      }
      
      async function fetchDynamicContent(src, originalRequest) {
        // Handle different ESI types
        switch(true) {
          case src.startsWith('/trending'):
            return await getTrendingPosts()
          case src.startsWith('/personalized'):
            return await getPersonalizedContent(originalRequest)
          case src.startsWith('/weather'):
            return await getWeather(originalRequest)
          default:
            return '
Dynamic content unavailable
' } } async function getTrendingPosts() { // Fetch from KV store (updated by Ruby script) const trending = await JEKYLL_KV.get('trending_posts', 'json') return trending.map(post => \`
  • \${post.title}
  • \` ).join('') } JS File.write('_workers/esi.js', worker_script) end def self.find_dynamic_sections(site) # Look for ESI comments or markers site.pages.flat_map do |page| content = page.content # Find patterns content.scan(//).flatten end.uniq end end # In Jekyll templates, use:

    Workers for Dynamic Content Injection

    Inject dynamic content based on real-time data:

    # lib/workers/dynamic_content.rb
    class DynamicContentWorker
      def self.generate_worker(site)
        # Generate Worker that injects dynamic content
        
        worker_template =  ~JS
          addEventListener('fetch', event => {
            event.respondWith(injectDynamicContent(event.request))
          })
          
          async function injectDynamicContent(request) {
            const url = new URL(request.url)
            const response = await fetch(request)
            
            // Only process HTML pages
            const contentType = response.headers.get('Content-Type')
            if (!contentType || !contentType.includes('text/html')) {
              return response
            }
            
            let html = await response.text()
            
            // Inject dynamic content based on page type
            if (url.pathname.includes('/blog/')) {
              html = await injectRelatedPosts(html, url.pathname)
              html = await injectReadingTime(html)
              html = await injectTrendingNotice(html)
            }
            
            if (url.pathname === '/') {
              html = await injectPersonalizedGreeting(html, request)
              html = await injectLatestContent(html)
            }
            
            return new Response(html, response)
          }
          
          async function injectRelatedPosts(html, currentPath) {
            // Get related posts from KV store
            const allPosts = await JEKYLL_KV.get('blog_posts', 'json')
            const currentPost = allPosts.find(p => p.path === currentPath)
            
            if (!currentPost) return html
            
            const related = allPosts
              .filter(p => p.id !== currentPost.id)
              .filter(p => hasCommonTags(p.tags, currentPost.tags))
              .slice(0, 3)
            
            if (related.length === 0) return html
            
            const relatedHtml = related.map(post => 
              \`
    \${post.title}

    \${post.excerpt}

    \` ).join('') return html.replace( '', \`\` ) } async function injectPersonalizedGreeting(html, request) { const country = request.headers.get('CF-IPCountry') const timezone = request.headers.get('CF-Timezone') let greeting = 'Welcome' let extraInfo = '' if (country) { const countryName = await getCountryName(country) greeting = \`Welcome, visitor from \${countryName}\` } if (timezone) { const hour = new Date().toLocaleString('en-US', { timeZone: timezone, hour: 'numeric' }) extraInfo = \` (it's \${hour} o'clock there)\` } return html.replace( '', \`
    \${greeting}\${extraInfo}
    \` ) } JS # Write Worker file File.write('_workers/dynamic_injection.js', worker_template) # Also generate Ruby script to update KV store generate_kv_updater(site) end def self.generate_kv_updater(site) updater_script = ~RUBY # Update KV store with latest content require 'cloudflare' def update_kv_store cf = Cloudflare.connect( account_id: ENV['CF_ACCOUNT_ID'], api_token: ENV['CF_API_TOKEN'] ) # Update blog posts blog_posts = site.posts.docs.map do |post| { id: post.id, path: post.url, title: post.data['title'], excerpt: post.data['excerpt'], tags: post.data['tags'] || [], published_at: post.data['date'].iso8601 } end cf.workers.kv.write( namespace_id: ENV['KV_NAMESPACE_ID'], key: 'blog_posts', value: blog_posts.to_json ) # Update trending posts (from analytics) trending = get_trending_posts_from_analytics() cf.workers.kv.write( namespace_id: ENV['KV_NAMESPACE_ID'], key: 'trending_posts', value: trending.to_json ) end # Run after each Jekyll build Jekyll::Hooks.register :site, :post_write do |site| update_kv_store end RUBY File.write('_plugins/kv_updater.rb', updater_script) end end

    Testing and Deployment Workflow

    Create a complete testing and deployment workflow:

    # Rakefile
    namespace :workers do
      desc "Build all Workers"
      task :build do
        puts "Building Workers..."
        
        # Generate Workers from Jekyll site
        system("jekyll build")
        
        # Minify Worker scripts
        Dir.glob('_workers/*.js').each do |file|
          minified = Uglifier.compile(File.read(file))
          File.write(file.gsub('.js', '.min.js'), minified)
        end
        
        puts "Workers built successfully"
      end
      
      desc "Test Workers locally"
      task :test do
        require 'workers_test'
        
        # Test each Worker
        WorkersTest.run_all_tests
        
        # Integration test with Jekyll output
        WorkersTest.integration_test
      end
      
      desc "Deploy Workers to Cloudflare"
      task :deploy do
        require 'cloudflare-workers'
        
        client = CloudflareWorkers::Client.new(
          account_id: ENV['CF_ACCOUNT_ID'],
          api_token: ENV['CF_API_TOKEN']
        )
        
        # Deploy each Worker
        Dir.glob('_workers/*.min.js').each do |file|
          worker_name = File.basename(file, '.min.js')
          script = File.read(file)
          
          puts "Deploying #{worker_name}..."
          
          begin
            # Update or create Worker
            client.workers.create_or_update(
              name: worker_name,
              script: script
            )
            
            # Deploy to routes (from site data)
            routes = site.data['workers'].find { |w| w[:name] == worker_name }[:routes]
            
            routes.each do |route|
              client.workers.routes.create(
                pattern: route,
                script: worker_name
              )
            end
            
            puts "✅ #{worker_name} deployed successfully"
          rescue => e
            puts "❌ Failed to deploy #{worker_name}: #{e.message}"
          end
        end
      end
      
      desc "Full build and deploy workflow"
      task :full do
        Rake::Task['workers:build'].invoke
        Rake::Task['workers:test'].invoke
        Rake::Task['workers:deploy'].invoke
        
        puts "🚀 All Workers deployed successfully"
      end
    end
    
    # Integrate with Jekyll build
    task :build do
      # Build Jekyll site
      system("jekyll build")
      
      # Build and deploy Workers
      Rake::Task['workers:full'].invoke
    end

    Advanced Workers Use Cases for Jekyll

    Implement sophisticated edge functionality:

    1. Real-time Analytics with Workers Analytics Engine

    # Worker to collect custom analytics
    gem 'cloudflare-workers-analytics'
    
    analytics_worker =  ~JS
      export default {
        async fetch(request, env) {
          // Log custom event
          await env.ANALYTICS.writeDataPoint({
            blobs: [
              request.url,
              request.cf.country,
              request.cf.asOrganization
            ],
            doubles: [1],
            indexes: ['pageview']
          })
          
          // Continue with request
          return fetch(request)
        }
      }
    JS
    
    # Ruby script to query analytics
    def get_custom_analytics
      client = CloudflareWorkers::Analytics.new(
        account_id: ENV['CF_ACCOUNT_ID'],
        api_token: ENV['CF_API_TOKEN']
      )
      
      data = client.query(
        query: {
          query: "
            SELECT 
              blob1 as url,
              blob2 as country,
              SUM(_sample_interval) as visits
            FROM jekyll_analytics
            WHERE timestamp > NOW() - INTERVAL '1' DAY
            GROUP BY url, country
            ORDER BY visits DESC
            LIMIT 100
          "
        }
      )
      
      data['result']
    end

    2. Edge Image Optimization

    # Worker to optimize images on the fly
    image_worker =  ~JS
      import { ImageWorker } from 'cloudflare-images'
      
      export default {
        async fetch(request) {
          const url = new URL(request.url)
          
          // Only process image requests
          if (!url.pathname.match(/\.(jpg|jpeg|png|webp)$/i)) {
            return fetch(request)
          }
          
          // Parse optimization parameters
          const width = url.searchParams.get('width')
          const format = url.searchParams.get('format') || 'webp'
          const quality = url.searchParams.get('quality') || 85
          
          // Fetch and transform image
          const imageResponse = await fetch(request)
          const image = await ImageWorker.load(imageResponse)
          
          if (width) {
            image.resize({ width: parseInt(width) })
          }
          
          image.format(format)
          image.quality(parseInt(quality))
          
          return image.response()
        }
      }
    JS
    
    # Ruby helper to generate optimized image URLs
    def optimized_image_url(original_url, width: nil, format: 'webp')
      uri = URI(original_url)
      params = {}
      params[:width] = width if width
      params[:format] = format
      
      uri.query = URI.encode_www_form(params)
      uri.to_s
    end

    3. Edge Caching with Stale-While-Revalidate

    # Worker for intelligent caching
    caching_worker =  ~JS
      export default {
        async fetch(request, env) {
          const cache = caches.default
          const url = new URL(request.url)
          
          // Try cache first
          let response = await cache.match(request)
          
          if (response) {
            // Cache hit - check if stale
            const age = response.headers.get('age') || 0
            
            if (age < 3600) { // Less than 1 hour old
              return response
            } else {
              // Stale but usable - revalidate in background
              event.waitUntil(revalidate(request))
              return response
            }
          }
          
          // Cache miss - fetch and cache
          response = await fetch(request)
          
          // Clone response to cache
          const responseToCache = response.clone()
          
          // Determine cache TTL based on content type
          let ttl = 3600 // Default 1 hour
          const contentType = response.headers.get('Content-Type')
          
          if (contentType?.includes('text/html')) {
            ttl = 300 // 5 minutes for HTML
          } else if (contentType?.includes('image')) {
            ttl = 2592000 // 30 days for images
          } else if (contentType?.includes('css') || contentType?.includes('js')) {
            ttl = 86400 // 1 day for assets
          }
          
          // Store in cache
          const cacheResponse = new Response(responseToCache.body, responseToCache)
          cacheResponse.headers.append('Cache-Control', \`public, max-age=\${ttl}\`)
          
          event.waitUntil(cache.put(request, cacheResponse))
          
          return response
        }
      }
    JS

    Start integrating Workers gradually. Begin with a simple personalization Worker that adds visitor country headers. Then implement form handling for your contact form. As you become comfortable, add more sophisticated features like A/B testing and dynamic content injection. Within months, you'll have a Jekyll site with the dynamic capabilities of a full-stack application, all running at the edge with minimal latency.