Intermediate

Image Gallery

A responsive image grid sourced from the Pexels API, processed at build time with @11ty/eleventy-img.

2 min read

Live Implementation

Six real photos fetched live from Pexels, each resized to two widths and encoded as WebP and JPEG, laid out in a plain CSS grid:

How It Works

The Responsive Images and Multi-format Images demos both process one fixed local source image. A gallery needs real, varied photos instead, so this demo fetches six curated photos from the Pexels API at build time and runs each one straight through @11ty/eleventy-img:

const photos = await fetchPexelsPhotos("curated", 6);

for (const photo of photos) {
    const metadata = await Image(photo.url, {
        widths: [400, 800],
        formats: ["webp", "jpeg"],
    });
    // ...generate HTML for each
}

@11ty/eleventy-img accepts a remote URL exactly like a local file path — it downloads the source once, caches it, and generates the same resized/re-encoded output either way. Each photo ends up at two widths and two formats, computed once at build time and cached so a second build doesn’t re-download or reprocess anything already fetched today. The grid itself is plain CSS Grid with auto-fill, no JavaScript, no lightbox library.

Folder Structure

src/_11ty/pexels.js                       ← shared Pexels fetch helper, used by this and Masonry Gallery
src/_data/imageGalleryDemo.js             ← fetches photos, processes each with eleventy-img
src/_includes/demo-live/image-gallery.njk ← the grid markup
src/assets/scss/components/_gallery.scss  ← grid + masonry styles

Important Files

src/_data/imageGalleryDemo.js

import Image from "@11ty/eleventy-img";
import fastGlob from "fast-glob";
import { fetchPexelsPhotos } from "../_11ty/pexels.js";

async function fallbackPhotos() {
    const files = await fastGlob("src/assets/images/demos/gallery/*.jpg");
    files.sort();
    return files.map((file) => ({ url: file, alt: "" }));
}

export default async function () {
    let photos;
    try {
        photos = await fetchPexelsPhotos("curated", 6);
    } catch {
        photos = await fallbackPhotos();
    }

    const items = [];
    for (const photo of photos) {
        const metadata = await Image(photo.url, {
            widths: [400, 800],
            formats: ["webp", "jpeg"],
            outputDir: "public/assets/images/optimized/",
            urlPath: "/assets/images/optimized/",
        });

        items.push(
            Image.generateHTML(metadata, {
                alt: photo.alt,
                sizes: "(min-width: 40rem) 30vw, 45vw",
                loading: "lazy",
                decoding: "async",
            })
        );
    }

    return items;
}

Notes

  • This needs a PEXELS_API_KEY environment variable (a free key from pexels.com/api) — set it in .env locally and as a Netlify build environment variable for the deployed site. See .env.example.
  • Without a key, this quietly falls back to a handful of generated placeholder images instead of failing the build — a missing API key shouldn’t be able to break every page that happens to show a gallery.
  • loading="lazy" is on every image here since the grid always renders below the fold on this page. On a real gallery page, the first row or two visible on load should usually drop that attribute.