Static Eleventy site with Cloudflare R2 photo gallery support and Cloudflare Worker-based image resizing.
- Eleventy-based static site generator
- Responsive image handling
- Automatic gallery discovery from R2
- Cloudflare-backed image delivery for thumbnails and full-size viewing
- GitHub Actions build and deploy pipeline
- Node.js
- npm
- Cloudflare R2 credentials
- Cloudflare account with Workers and R2 enabled
- GitHub repo with Pages enabled (for deploy)
The site stores a single high-resolution original image in R2 and serves resized variants through a Cloudflare Worker.
Recommended URL patterns:
- Original:
https://cdn.elew.is/photos/Film/000022590008.jpg - Thumbnail:
https://cdn.elew.is/thumb/photos/Film/000022590008.jpg - Full-size display:
https://cdn.elew.is/full/photos/Film/000022590008.jpg - Custom width:
https://cdn.elew.is/w1200/photos/Film/000022590008.jpg - Percent-based variant:
https://cdn.elew.is/p60/photos/Film/000022590008.jpg
For high-resolution originals, the most practical workflow is to keep one source image in R2 and let the Worker request a resized version from Cloudflare at the edge.
thumb→ 300px wide, cropped to a consistent thumbnail shapefull→ large display size for viewing pagesw####→ exact width in pixels, scaled down to preserve aspect ratiop##→ percentage-based variant for large originals, such asp60
For example:
thumbis best for grid tilesw800is useful for small previewsw1200is a good general-purpose gallery view sizew2400is useful for very high-resolution film scans
-
Install dependencies
npm install @11ty/eleventy @11ty/eleventy-img slugify @aws-sdk/client-s3
-
Set required environment variables
export R2_ACCOUNT_ID=<your-account-id> export R2_ACCESS_KEY=<your-access-key> export R2_SECRET_KEY=<your-secret-key> export R2_BUCKET=<bucket-name> export R2_PUBLIC_URL=https://cdn.example.com export IMAGE_ORIGIN_URL=https://origin.example.com
R2_PUBLIC_URLis the public CDN hostname used by the website.IMAGE_ORIGIN_URLshould point to the image origin that the Worker reads from. This can be a Cloudflare R2 custom domain, a dedicated origin hostname, or any origin route that serves the original image files. -
Run locally
npm run dev
-
Open browser
Visit
http://localhost:8080
Generate the static site into dist:
npm run buildOutput is written to dist.
Photos are stored in Cloudflare R2 under photos/<gallery-name>/.
Eleventy automatically discovers galleries and generates /photos/<slug>/index.html.
The photo shortcode handles responsive images with lazy loading.
Example R2 structure:
photos/
└── Gallery/
├── img1.jpg
├── img2.jpg
└── img3.jpg
Example image usage in templates:
imageUrl('photos/Film/000022590008.jpg', 'thumb')
imageUrl('photos/Film/000022590008.jpg', 'full')
imageUrl('photos/Film/000022590008.jpg', 'w1200')Example frontend helper:
function imageUrl(path, size = 'thumb') {
return `https://cdn.elew.is/${size}/${path}`;
}Create an R2 bucket and upload the original high-resolution images into it.
Keep the originals in a predictable key structure, such as:
photos/Film/000022590008.jpg
photos/Family/000000120003.jpg
The Worker needs an origin URL for the source images. The simplest pattern is:
- keep the canonical originals in R2
- expose them through a dedicated origin hostname
- point the Worker at that origin via
IMAGE_ORIGIN_URL
The public CDN hostname should be the one your website uses, while the origin hostname is what the Worker fetches from.
Image resizing must be enabled for the zone that serves the image origin and Worker route.
Recommended transformations:
- thumbnail crop for grid views
- scale-down for full-size views
- automatic format conversion where available
Attach the Worker to the image CDN hostname, for example:
cdn.elew.is/*
Use wrangler or the Cloudflare dashboard to deploy the Worker.
name = "image-resizer"
main = "src/worker.js"
compatibility_date = "2026-04-17"
[vars]
IMAGE_ORIGIN_URL = "https://origin.example.com"
[[routes]]
pattern = "cdn.elew.is/*"
zone_name = "elew.is"If you use environment-specific values, store them in Wrangler secrets or per-environment config instead of hardcoding them.
under scripts/r2_image_resizer.js
https://cdn.elew.is/thumb/photos/Film/000022590008.jpghttps://cdn.elew.is/full/photos/Film/000022590008.jpghttps://cdn.elew.is/w800/photos/Film/000022590008.jpghttps://cdn.elew.is/w1200/photos/Film/000022590008.jpg
Widths must be one of the following: 300, 400, 600, 800, 1000, 1200, 1600, 2000, 2400
Store these values in GitHub repository secrets:
R2_ACCOUNT_IDR2_ACCESS_KEYR2_SECRET_KEYR2_BUCKETR2_PUBLIC_URLIMAGE_ORIGIN_URL
- GitHub Actions runs the build using the R2 environment variables.
- The dist folder is generated and uploaded as an artifact.
- The site is deployed to GitHub Pages.
- Ensure GitHub Pages is configured for the repo.
- Confirm
R2_PUBLIC_URLmatches the CDN hostname used for served images. - Confirm
IMAGE_ORIGIN_URLpoints to the origin hostname that the Worker can fetch from. - Cache the image CDN aggressively so resized variants are reused at the edge.
- Keep original uploads in R2 and let the Worker handle all derivative image sizes.