1+ # Avatar Downloader Plugin for Jekyll
2+ # ==================================
3+ #
4+ # This plugin automates the process of downloading GitHub avatar images for
5+ # contributors listed in the site's data file. It downloads the images during
6+ # the Jekyll build process and stores them in the assets/img/avatars directory.
7+ #
8+ # Process Flow:
9+ # ------------
10+ # ```
11+ # +-----------------+ +------------------+ +-----------------------+
12+ # | Read contributor| | For each | | Check if avatar exists|
13+ # | data from |--->| contributor with |--->|or check ETag/Modified |
14+ # | site.data | | GitHub ID | | headers for changes |
15+ # +-----------------+ +------------------+ +-----------------------+
16+ # |
17+ # v
18+ # +-----------------+ +------------------+ +-----------------------+
19+ # | Update site | | Save avatar | | Download avatar only |
20+ # | configuration |<---| to assets/img/ |<---| if changed or new |
21+ # | with avatar path| | avatars | | (using HTTP headers) |
22+ # +-----------------+ +------------------+ +-----------------------+
23+ # ```
24+ #
25+ # Benefits:
26+ # --------
27+ # 1. No manual downloading of avatar images required
28+ # 2. Images stay fresh but are only downloaded when actually changed
29+ # 3. Images are included in the built site automatically
30+ # 4. Faster page loads since images are served from the same domain
31+ #
32+ # Usage:
33+ # ------
34+ # In templates: <img src="/assets/img/avatars/{{ github_username }}.jpg">
35+ # The plugin automatically runs during Jekyll build process.
36+ #
37+ # Configuration:
38+ # --------------
39+ # - Requires contributor data with 'githubId' field
40+ # - Uses HTTP ETag/Last-Modified headers to only download changed images
41+
42+ require 'net/http'
43+ require 'fileutils'
44+ require 'digest'
45+ require 'yaml'
46+
47+ module Jekyll
48+ class AvatarDownloader < Generator
49+ safe true
50+ priority :high
51+
52+ def generate ( site )
53+ # Store avatars in the source assets directory so they're included in the build
54+ avatar_dir = File . join ( site . source , 'assets' , 'img' , 'avatars' )
55+ FileUtils . mkdir_p ( avatar_dir ) unless Dir . exist? ( avatar_dir )
56+
57+ # Create metadata directory for ETag/Last-Modified storage
58+ metadata_dir = File . join ( site . source , 'assets' , 'img' , 'avatars' , '.metadata' )
59+ FileUtils . mkdir_p ( metadata_dir ) unless Dir . exist? ( metadata_dir )
60+
61+ # Load previous metadata if it exists
62+ metadata_file = File . join ( metadata_dir , 'metadata.yml' )
63+ avatar_metadata = { }
64+ if File . exist? ( metadata_file )
65+ begin
66+ avatar_metadata = YAML . load_file ( metadata_file ) || { }
67+ rescue => e
68+ puts "Error loading avatar metadata: #{ e . message } "
69+ avatar_metadata = { }
70+ end
71+ end
72+
73+ # Get contributors from site data
74+ contributors = site . data [ 'contributors' ] || [ ]
75+
76+ # Download each avatar
77+ contributors . each do |contributor |
78+ next unless contributor [ 'githubId' ]
79+
80+ github_id = contributor [ 'githubId' ]
81+ avatar_url = "https://avatars.githubusercontent.com/#{ github_id } "
82+ local_path = File . join ( avatar_dir , "#{ github_id } .jpg" )
83+
84+ # Get stored ETag and Last-Modified values if they exist
85+ user_metadata = avatar_metadata [ github_id ] || { }
86+ stored_etag = user_metadata [ 'etag' ]
87+ stored_last_modified = user_metadata [ 'last_modified' ]
88+
89+ download_needed = false
90+ headers = { }
91+
92+ # Add conditional headers if we have previous values
93+ if stored_etag
94+ headers [ 'If-None-Match' ] = stored_etag
95+ end
96+
97+ if stored_last_modified
98+ headers [ 'If-Modified-Since' ] = stored_last_modified
99+ end
100+
101+ # Check if we need to download: file doesn't exist or we need to check if it's changed
102+ if !File . exist? ( local_path ) || stored_etag || stored_last_modified
103+ begin
104+ uri = URI ( avatar_url )
105+
106+ # Make a HEAD request first to check headers
107+ Net ::HTTP . start ( uri . host , uri . port , use_ssl : true ) do |http |
108+ request = Net ::HTTP ::Head . new ( uri , headers )
109+ response = http . request ( request )
110+
111+ case response . code
112+ when '200'
113+ # Resource changed or first download
114+ download_needed = true
115+ # Store new ETag and Last-Modified for future requests
116+ avatar_metadata [ github_id ] = {
117+ 'etag' => response [ 'ETag' ] ,
118+ 'last_modified' => response [ 'Last-Modified' ]
119+ }
120+ when '304'
121+ # Resource not modified
122+ puts "Avatar for #{ github_id } is up-to-date (HTTP 304)"
123+ download_needed = false
124+ else
125+ puts "Unexpected response for #{ github_id } : #{ response . code } "
126+ download_needed = false
127+ end
128+ end
129+
130+ # Only download if needed
131+ if download_needed
132+ puts "Downloading avatar for #{ github_id } ..."
133+ Net ::HTTP . start ( uri . host , uri . port , use_ssl : true ) do |http |
134+ request = Net ::HTTP ::Get . new ( uri )
135+ response = http . request ( request )
136+
137+ if response . code == '200'
138+ File . open ( local_path , 'wb' ) do |file |
139+ file . write ( response . body )
140+ end
141+ puts "Avatar saved for #{ github_id } "
142+
143+ # Update metadata
144+ avatar_metadata [ github_id ] = {
145+ 'etag' => response [ 'ETag' ] ,
146+ 'last_modified' => response [ 'Last-Modified' ]
147+ }
148+ else
149+ puts "Failed to download avatar for #{ github_id } : #{ response . code } "
150+ end
151+ end
152+ end
153+ rescue => e
154+ puts "Error processing avatar for #{ github_id } : #{ e . message } "
155+ end
156+ end
157+ end
158+
159+ # Save metadata for next build
160+ File . open ( metadata_file , 'w' ) do |file |
161+ file . write ( avatar_metadata . to_yaml )
162+ end
163+
164+ # Add site.avatar_path for use in templates
165+ site . config [ 'avatar_path' ] = '/assets/img/avatars'
166+ end
167+ end
168+ end
0 commit comments