Tinkering
Lightweight Photo Management for Jekyll Site
I recently added a Photos section to my personal site and needed an easier way to move images from Google Photos into Jekyll without bloating my GitHub repo. I wanted something simple: pick images, import them, store the originals once, and serve transformed versions on demand.
All of my personal photos automatically backup to Google Photos. Pointing the pages in my site to Google Photos directly seemed janky, so I used:
- A Google Cloud Project to expose and handle authentication to the Google Picker API.
- A simple Python utility for transferring selected images from Google Photos to Azure Blob Storage.
- Cloudlfare Images for dynamic image transformation
High-Level Overview
[Google Photos] --------> [Google Picker Front End]
^ |
| | write metadata
| v
| [Azure Storage]
| |
| download |
| originals v
+-------------------- [Python Import Script]
|
| upload originals
v
[Azure Blob Storage]
|
v
[Cloudflare Image Transformations]
|
v
[Jekyll Site]
|
v
[Visitors]
Google Picker Front End
A simple private webpage loads Google Picker (using a Google Cloud project with OAuth enabled). I pick photos visually, and the page writes basic metadata—IDs, filenames, URLs—into Azure Storage. No images are downloaded at this stage.
The flow is entirely client-side JavaScript:
// 1. Create a Picker session
const response = await fetch('https://photospicker.googleapis.com/v1/sessions', {
method: 'POST',
headers: {
'Authorization': `Bearer ${accessToken}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({})
});
const session = await response.json();
// 2. Open the picker in a popup
window.open(session.pickerUri, 'GooglePhotosPicker', 'width=800,height=600');
// 3. Poll for user selection
const pollInterval = setInterval(async () => {
const status = await fetch(`https://photospicker.googleapis.com/v1/sessions/${session.id}`, {
headers: { 'Authorization': `Bearer ${accessToken}` }
});
const data = await status.json();
if (data.mediaItemsSet) {
// User finished selecting!
clearInterval(pollInterval);
// 4. Save session metadata to Azure for processing
await uploadToAzure({
sessionId: session.id,
accessToken: accessToken
});
}
}, 2000);
Python Import Script
A small Python script reads the stored metadata, downloads each photo from Google Photos, and uploads the originals to Azure Blob Storage. Once imported, the metadata is cleared so items aren’t duplicated.
Here is the core logic that bridges the two services:
def process_session(session_id, access_token):
# 1. Get media items from the Google Picker session
headers = {'Authorization': f'Bearer {access_token}'}
response = requests.get(
'https://photospicker.googleapis.com/v1/mediaItems',
headers=headers,
params={'sessionId': session_id}
)
media_items = response.json().get('mediaItems', [])
for item in media_items:
# 2. Download the high-res original
# The '=d' parameter requests the download URL
download_url = item['mediaFile']['baseUrl'] + '=d'
image_data = requests.get(download_url, headers=headers).content
# 3. Upload to Azure Blob Storage
blob_client = blob_service_client.get_blob_client(
container='images',
blob=item['mediaFile']['filename']
)
blob_client.upload_blob(image_data, overwrite=True)
Cloudflare Image Transformations
Images stay in Azure Blob, but all requests go through Cloudflare, which handles resizing and optimization on the fly. The URL structure allows me to specify transformation options directly in the path. Example:
https://assets.jessefitz.me/cdn-cgi/image/width=800,format=auto,quality=85/images/
- Domain:
assets.jessefitz.me(proxied through Cloudflare) - Transformation Path:
/cdn-cgi/image/ - Options:
width=800,format=auto,quality=85(resizes to 800px, auto-selects WebP/AVIF, sets quality) - Source:
/images/(path to the original file in Azure Blob Storage)
This gives me lightweight delivery without storing resized versions anywhere.
Using Images in Jekyll
Jekyll only references the Cloudflare-transformed URLs. The repo stays small, and I never manually export or resize images.
I created a simple layout _layouts/photo-page.html that takes a photo front matter variable and generates the optimized image tag:
{% if page.photo %}
<div class="post-image-container">
<a
href="https://assets.jessefitz.me/images/{{ page.photo }}"
target="_blank"
rel="noopener noreferrer"
>
<img
src="https://assets.jessefitz.me/cdn-cgi/image/width=800,format=auto,quality=85/images/{{ page.photo }}"
alt="{{ page.title | escape }}"
class="post-image"
/>
</a>
</div>
{% endif %}
Why This Works
- No images in the GitHub repo
- Visual selection from Google Photos
- Automatic import and storage
- On-the-fly image resizing via Cloudflare
- Simple to maintain and extend
It’s not a full photo system—it’s just enough structure to keep adding new photography to the site without friction.