_src/_plugins/avatar_downloader.rb (92 lines of code) (raw):
# Avatar Downloader Plugin for Jekyll
# ==================================
#
# This plugin automates the process of downloading GitHub avatar images for
# contributors listed in the site's data file. It downloads the images during
# the Jekyll build process and stores them in the assets/img/avatars directory.
#
# Process Flow:
# ------------
# ```
# +-----------------+ +------------------+ +-----------------------+
# | Read contributor| | For each | | Check if avatar exists|
# | data from |--->| contributor with |--->|or check ETag/Modified |
# | site.data | | GitHub ID | | headers for changes |
# +-----------------+ +------------------+ +-----------------------+
# |
# v
# +-----------------+ +------------------+ +-----------------------+
# | Update site | | Save avatar | | Download avatar only |
# | configuration |<---| to assets/img/ |<---| if changed or new |
# | with avatar path| | avatars | | (using HTTP headers) |
# +-----------------+ +------------------+ +-----------------------+
# ```
#
# Benefits:
# --------
# 1. No manual downloading of avatar images required
# 2. Images stay fresh but are only downloaded when actually changed
# 3. Images are included in the built site automatically
# 4. Faster page loads since images are served from the same domain
#
# Usage:
# ------
# In templates: <img src="/assets/img/avatars/{{ github_username }}.jpg">
# The plugin automatically runs during Jekyll build process.
#
# Configuration:
# --------------
# - Requires contributor data with 'githubId' field
# - Uses HTTP ETag/Last-Modified headers to only download changed images
require 'net/http'
require 'fileutils'
require 'digest'
require 'yaml'
module Jekyll
class AvatarDownloader < Generator
safe true
priority :high
def generate(site)
# Store avatars in the source assets directory so they're included in the build
avatar_dir = File.join(site.source, 'assets', 'img', 'avatars')
FileUtils.mkdir_p(avatar_dir) unless Dir.exist?(avatar_dir)
# Create metadata directory for ETag/Last-Modified storage
metadata_dir = File.join(site.source, 'assets', 'img', 'avatars', '.metadata')
FileUtils.mkdir_p(metadata_dir) unless Dir.exist?(metadata_dir)
# Load previous metadata if it exists
metadata_file = File.join(metadata_dir, 'metadata.yml')
avatar_metadata = {}
if File.exist?(metadata_file)
begin
avatar_metadata = YAML.load_file(metadata_file) || {}
rescue => e
puts "Error loading avatar metadata: #{e.message}"
avatar_metadata = {}
end
end
# Get contributors from site data
contributors = site.data['contributors'] || []
# Download each avatar
contributors.each do |contributor|
next unless contributor['githubId']
github_id = contributor['githubId']
avatar_url = "https://avatars.githubusercontent.com/#{github_id}"
local_path = File.join(avatar_dir, "#{github_id}.jpg")
# Get stored ETag and Last-Modified values if they exist
user_metadata = avatar_metadata[github_id] || {}
stored_etag = user_metadata['etag']
stored_last_modified = user_metadata['last_modified']
download_needed = false
headers = {}
# Add conditional headers if we have previous values
if stored_etag
headers['If-None-Match'] = stored_etag
end
if stored_last_modified
headers['If-Modified-Since'] = stored_last_modified
end
# Check if we need to download: file doesn't exist or we need to check if it's changed
if !File.exist?(local_path) || stored_etag || stored_last_modified
begin
uri = URI(avatar_url)
# Make a HEAD request first to check headers
Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |http|
request = Net::HTTP::Head.new(uri, headers)
response = http.request(request)
case response.code
when '200'
# Resource changed or first download
download_needed = true
# Store new ETag and Last-Modified for future requests
avatar_metadata[github_id] = {
'etag' => response['ETag'],
'last_modified' => response['Last-Modified']
}
when '304'
# Resource not modified
puts "Avatar for #{github_id} is up-to-date (HTTP 304)"
download_needed = false
else
puts "Unexpected response for #{github_id}: #{response.code}"
download_needed = false
end
end
# Only download if needed
if download_needed
puts "Downloading avatar for #{github_id}..."
Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |http|
request = Net::HTTP::Get.new(uri)
response = http.request(request)
if response.code == '200'
File.open(local_path, 'wb') do |file|
file.write(response.body)
end
puts "Avatar saved for #{github_id}"
# Update metadata
avatar_metadata[github_id] = {
'etag' => response['ETag'],
'last_modified' => response['Last-Modified']
}
else
puts "Failed to download avatar for #{github_id}: #{response.code}"
end
end
end
rescue => e
puts "Error processing avatar for #{github_id}: #{e.message}"
end
end
end
# Save metadata for next build
File.open(metadata_file, 'w') do |file|
file.write(avatar_metadata.to_yaml)
end
# Add site.avatar_path for use in templates
site.config['avatar_path'] = '/assets/img/avatars'
end
end
end