You just published an important update to your Jekyll blog, but visitors are still seeing the old cached version for hours. Manually purging Cloudflare cache through the dashboard is tedious and error-prone. This cache lag problem undermines the immediacy of static sites and frustrates both you and your audience. The solution lies in automating cache management using specialized Ruby gems that integrate directly with your Jekyll workflow.
Cloudflare caches static assets at its edge locations worldwide. For Jekyll sites, this includes HTML pages, CSS, JavaScript, and images. The default cache behavior depends on file type and cache headers. HTML files typically have shorter cache durations (a few hours) while assets like CSS and images cache longer (up to a year). This is problematic when you need instant updates across all cached content.
Cloudflare offers several cache purging methods: purge everything (entire zone), purge by URL, purge by tag, or purge by host. For Jekyll sites, understanding when to use each method is crucial. Purging everything is heavy-handed and affects all visitors. Purging by URL is precise but requires knowing exactly which URLs changed. The ideal approach combines selective purging with intelligent detection of changed files during the Jekyll build process.
| File Type | Default Cache TTL | Recommended Purging Strategy |
|---|---|---|
| HTML Pages | 2-4 hours | Purge specific changed pages |
| CSS Files | 1 month | Purge on any CSS change |
| JavaScript | 1 month | Purge on JS changes |
| Images (JPG/PNG) | 1 year | Purge only changed images |
| WebP/AVIF Images | 1 year | Purge originals and variants |
| XML Sitemaps | 24 hours | Always purge on rebuild |
Several Ruby gems can automate Cloudflare cache management. The most comprehensive is `cloudflare` gem:
# Add to Gemfile
gem 'cloudflare'
# Basic usage
require 'cloudflare'
cf = Cloudflare.connect(key: ENV['CF_API_KEY'], email: ENV['CF_EMAIL'])
zone = cf.zones.find_by_name('yourdomain.com')
# Purge entire cache
zone.purge_cache
# Purge specific URLs
zone.purge_cache(files: [
'https://yourdomain.com/about/',
'https://yourdomain.com/css/main.css'
])
For Jekyll-specific integration, create a custom gem or Rake task:
# lib/jekyll/cloudflare_purger.rb
module Jekyll
class CloudflarePurger
def initialize(site)
@site = site
@changed_files = detect_changed_files
end
def purge!
return if @changed_files.empty?
require 'cloudflare'
cf = Cloudflare.connect(
key: ENV['CLOUDFLARE_API_KEY'],
email: ENV['CLOUDFLARE_EMAIL']
)
zone = cf.zones.find_by_name(@site.config['url'])
urls = @changed_files.map { |f| File.join(@site.config['url'], f) }
zone.purge_cache(files: urls)
puts "Purged #{urls.count} URLs from Cloudflare cache"
end
private
def detect_changed_files
# Compare current build with previous build
# Implement git diff or file mtime comparison
end
end
end
# Hook into Jekyll build process
Jekyll::Hooks.register :site, :post_write do |site|
CloudflarePurger.new(site).purge! if ENV['PURGE_CLOUDFLARE_CACHE']
end
Selective purging is more efficient than purging everything. Implement a smart purging system:
Use git to detect what changed between builds:
def changed_files_since_last_build
# Get commit hash of last successful build
last_build_commit = File.read('.last_build_commit') rescue nil
if last_build_commit
`git diff --name-only #{last_build_commit} HEAD`.split("\n")
else
# First build, assume everything changed
`git ls-files`.split("\n")
end
end
# Save current commit after successful build
File.write('.last_build_commit', `git rev-parse HEAD`.strip)
Different file types need different purging strategies:
def purge_strategy_for_file(file)
case File.extname(file)
when '.css', '.js'
# CSS/JS changes affect all pages
:purge_all_pages
when '.html', '.md'
# HTML changes affect specific pages
:purge_specific_page
when '.yml', '.yaml'
# Config changes might affect many pages
:purge_related_pages
else
:purge_specific_file
end
end
Track which pages depend on which assets:
# _data/asset_dependencies.yml
about.md:
- /css/layout.css
- /js/navigation.js
- /images/hero.jpg
blog/index.html:
- /css/blog.css
- /js/comments.js
- /_posts/*.md
When an asset changes, purge all pages that depend on it.
Purging cache creates a performance penalty for the next visitor. Implement cache warming:
Implementation with Ruby:
def warm_cache(urls)
require 'net/http'
require 'uri'
threads = []
urls.each do |url|
threads Thread.new do
uri = URI.parse(url)
Net::HTTP.get_response(uri)
puts "Warmed: #{url}"
end
end
threads.each(&:join)
end
# Warm top 10 pages after purge
top_pages = get_top_pages_from_analytics(limit: 10)
warm_cache(top_pages)
Use Cloudflare Analytics to monitor cache performance:
# Fetch cache analytics via API
def cache_hit_ratio
require 'cloudflare'
cf = Cloudflare.connect(key: ENV['CF_API_KEY'], email: ENV['CF_EMAIL'])
data = cf.analytics.dashboard(
zone_id: ENV['CF_ZONE_ID'],
since: '-43200', # Last 12 hours
until: '0',
continuous: true
)
{
hit_ratio: data['totals']['requests']['cached'].to_f / data['totals']['requests']['all'],
bandwidth_saved: data['totals']['bandwidth']['cached'],
origin_requests: data['totals']['requests']['uncached']
}
end
Ideal cache hit ratio for Jekyll sites: 90%+. Lower ratios indicate cache configuration issues.
Serve different content variants with proper caching:
# Use Cloudflare Workers to vary cache by cookie
addEventListener('fetch', event => {
const cookie = event.request.headers.get('Cookie')
const variant = cookie.includes('variant=b') ? 'b' : 'a'
// Cache separately for each variant
const cacheKey = `${event.request.url}?variant=${variant}`
event.respondWith(handleRequest(event.request, cacheKey))
})
Serve stale content while updating in background:
# Configure in Cloudflare dashboard or via API
cf.zones.settings.cache_level.edit(
zone_id: zone.id,
value: 'aggressive' # Enables stale-while-revalidate
)
Tag content for granular purging:
# Add cache tags via HTTP headers
response.headers['Cache-Tag'] = 'post-123,category-tech,author-john'
# Purge by tag
cf.zones.purge_cache.tags(
zone_id: zone.id,
tags: ['post-123', 'category-tech']
)
Here's a complete Rakefile implementation:
# Rakefile
require 'cloudflare'
namespace :cloudflare do
desc "Purge cache for changed files"
task :purge_changed do
require 'jekyll'
# Initialize Jekyll
site = Jekyll::Site.new(Jekyll.configuration)
site.process
# Detect changed files
changed_files = `git diff --name-only HEAD~1 HEAD 2>/dev/null`.split("\n")
changed_files = site.static_files.map(&:relative_path) if changed_files.empty?
# Filter to relevant files
relevant_files = changed_files.select do |file|
file.match?(/\.(html|css|js|xml|json|md)$/i) ||
file.match?(/^_(posts|pages|drafts)/)
end
# Generate URLs to purge
urls = relevant_files.map do |file|
# Convert file paths to URLs
url_path = file
.gsub(/^_site\//, '')
.gsub(/\.md$/, '')
.gsub(/index\.html$/, '')
.gsub(/\.html$/, '/')
"#{site.config['url']}/#{url_path}"
end.uniq
# Purge via Cloudflare API
if ENV['CLOUDFLARE_API_KEY'] && !urls.empty?
cf = Cloudflare.connect(
key: ENV['CLOUDFLARE_API_KEY'],
email: ENV['CLOUDFLARE_EMAIL']
)
zone = cf.zones.find_by_name(site.config['url'].gsub(/https?:\/\//, ''))
begin
zone.purge_cache(files: urls)
puts "✅ Purged #{urls.count} URLs from Cloudflare cache"
# Log the purge
File.open('_data/cache_purges.yml', 'a') do |f|
f.write({
'timestamp' => Time.now.iso8601,
'urls' => urls,
'count' => urls.count
}.to_yaml.gsub(/^---\n/, ''))
end
rescue => e
puts "❌ Cache purge failed: #{e.message}"
end
end
end
desc "Warm cache for top pages"
task :warm_cache do
require 'net/http'
require 'uri'
# Get top pages from analytics or sitemap
top_pages = [
'/',
'/blog/',
'/about/',
'/contact/'
]
puts "Warming cache for #{top_pages.count} pages..."
top_pages.each do |path|
url = URI.parse("https://yourdomain.com#{path}")
Thread.new do
3.times do |i| # Hit each page 3 times for different cache layers
Net::HTTP.get_response(url)
sleep 0.5
end
puts " Warmed: #{path}"
end
end
# Wait for all threads
Thread.list.each { |t| t.join if t != Thread.current }
end
end
# Deployment task that combines everything
task :deploy do
puts "Building site..."
system("jekyll build")
puts "Purging Cloudflare cache..."
Rake::Task['cloudflare:purge_changed'].invoke
puts "Deploying to GitHub..."
system("git add . && git commit -m 'Deploy' && git push")
puts "Warming cache..."
Rake::Task['cloudflare:warm_cache'].invoke
puts "✅ Deployment complete!"
end
Stop fighting cache issues manually. Implement the basic purge automation this week. Start with the simple Rake task, then gradually add smarter detection and warming features. Your visitors will see updates instantly, and you'll save hours of manual cache management each month.