Using images & creating a gallery

Images in Poops are two cooperating parts:

  1. Processing โ€” the images key runs poops-images to resize, convert (WebP/AVIF), crop and read EXIF. It writes variants and a .poops-images-cache.json.
  2. Markup โ€” the {% image %} tag and the exif/images filters read that cache and emit correct HTML.

Warning

poops-images (and its sharp dependency) is not bundled with Poops. Install it only if you use the images key: npm i poops-images. If the key is present but the package isn't installed, Poops logs a warning and skips image processing โ€” the rest of the build still runs.

Processing images

{
  "images": {
    "in": "src/images",
    "out": "dist/images",
    "sizes": [{ "width": 640 }, { "width": 1280 }],
    "format": "smart"
  }
}
  • in / out โ€” keep out distinct from in and outside your watched sources, so generated variants don't retrigger the build.
  • sizes โ€” responsive widths to generate.
  • format โ€” e.g. ["webp"], or "smart" to keep whichever of JPEG/WebP is smaller.

Images are processed before markup, so the {% image %} tag and images filter always read a fresh cache.

Emitting a responsive image

{% image 'images/hero.jpg', alt='Sunrise', sizes='(max-width: 640px) 100vw, 50vw' %}

With the poops-images cache present you also get exact width/height attributes (no layout shift), correct src when the source format was converted, and EXIF via the exif filter.

Custom sizes (named crops)

A plain size ({ "width": 640 }) is one rung of the responsive ladder. A named size is a fixed crop of its own โ€” WordPress-style add_image_size โ€” for a thumbnail, a hero banner, a social card: each has its own dimensions and crop anchor, independent of the responsive widths.

{
  "images": {
    "in": "src/images",
    "out": "dist/images",
    "sizes": [
      { "width": 640 },
      { "width": 1280 },
      { "name": "thumb", "width": 200, "height": 200, "crop": true },
      { "name": "hero", "width": 1600, "height": 500, "crop": ["center", "top"] }
    ],
    "format": ["webp"]
  }
}

For photo.jpg that writes photo-640w.webp and photo-1280w.webp (the responsive ladder) plus photo-thumb-200w.webp and photo-hero-1600w.webp (the named crops).

The plain-width variants feed the responsive srcset automatically; named crops (and preprocessed variants like photo-blurred-640w.webp) are deliberately kept out of it โ€” they have their own aspect ratios. To emit one, pass the size name as the size kwarg โ€” Poops pulls that whole crop group from the compile cache and builds its own srcset, so soft crops (no fixed height) resolve too (needs poops-images โ‰ฅ 1.2.1):

{# responsive: uses the -640w / -1280w ladder #}
{% image 'images/photo.jpg', alt='Hero', sizes='100vw' %}

{# just the 200ร—200 thumb crop #}
{% image 'images/photo.jpg', size='thumb', alt='', sizes='200px' %}

{# just the hero banner crop #}
{% image 'images/photo.jpg', size='hero', alt='' %}

Each named reference resolves to that crop group, e.g. <img src="images/photo-thumb-200w.webp" โ€ฆ>.

The images filter lists every image under a directory from the cache. Combine it with groupby, engine-native sorting and the image tag, and a gallery is pure templating โ€” no manual list to maintain.

{% for group in 'images' | images | sort(reverse=true, attribute='date') | groupby("date", "year") %}
  <h2>{{ group.key }}</h2>
  <div class="grid">
    {% for img in group.items %}
      <figure>
        {% image img.path, alt='', sizes='(max-width: 640px) 50vw, 25vw' %}
        {% if img.exif and img.exif.gps %}
          <figcaption>
            <a href="{{ img.exif.gps.googleMapsUrl }}">๐Ÿ“</a> {{ img.date | date("MMM D, YYYY") }}
          </figcaption>
        {% endif %}
      </figure>
    {% endfor %}
  </div>
{% endfor %}

Each img exposes path (feeds straight into the image tag), width, height, date (EXIF date if present, else file mtime), exif, and outputs (every generated file).

EXIF captions

The exif filter returns camera, exposure, timestamp and GPS metadata:

{% set meta = 'images/photo.jpeg' | exif %}
<figure>
  {% image 'images/photo.jpeg', alt='At dusk' %}
  {% if meta %}
    <figcaption>
      {{ meta.dateTime | date("MMMM D, YYYY") }}
      {% if meta.gps %} โ€” <a href="{{ meta.gps.googleMapsUrl }}">{{ meta.gps.latitude.formatted }}, {{ meta.gps.longitude.formatted }}</a>{% endif %}
      {% if meta.model %} ยท {{ meta.model }}{% endif %}
    </figcaption>
  {% endif %}
</figure>

Tip

In watch mode, adding a source image processes it and rebuilds the galleries that reference it; deleting one removes its variants and updates the galleries. You never hand-edit an image list.

Next: A documentation site.