Skip to main content

A lightweight Wagtail StreamField block + DRF serializer that exposes responsive WebP thumbnails with dimensions and focal points.

Project description

wagtail-thumbnails

PyPI CI Python License: MIT

A drop-in Wagtail StreamField block + DRF serializer that turns any uploaded image into a multi-variant WebP payload with dimensions and focal points - ready for headless frontends.

What you get

  • A ThumbnailBlock - a thin extension of Wagtail's built-in ImageBlock that adds a pluggable validator pipeline and emits a ThumbnailSerializer payload
  • A ThumbnailSerializer that emits:
    • Source URL
    • Resolved alt text (block override → image descriptionnull; titles are intentionally not used)
    • Focal point (from Wagtail's built-in picker)
    • A configurable map of responsive variants (defaults: full_hd, large, medium, small) - each with url, width, height, format
  • An ImageResolutionValidator you can configure globally (WAGTAIL_THUMBNAILS settings) or per-field
  • Settings-driven variants - ship sensible defaults, override per project

You upload JPEG/PNG. Wagtail/Pillow generates and caches WebP renditions on first request. No user-side conversion required.

Install

Requires Django 4.2+, Wagtail 6.3+ (the block subclasses Wagtail's built-in ImageBlock, added in 6.3), and DRF 3.14+.

pip install wagtail-thumbnails

Add to INSTALLED_APPS:

INSTALLED_APPS = [
    # ...
    "wagtail_thumbnails",
]

Quickstart

StreamField block

from wagtail.fields import StreamField
from wagtail_thumbnails.blocks import ThumbnailBlock

class ArticlePage(Page):
    body = StreamField([
        ("thumbnail", ThumbnailBlock()),
        # ... your other blocks
    ])

ThumbnailBlock is a subclass of Wagtail's ImageBlock, so editors get the same image / alt_text / decorative UI and behaviours - including the alt-text-required check (a value must have alt text or be marked decorative). The block adds:

  1. A configurable validator pipeline (see Validation).
  2. A multi-variant API payload via ThumbnailSerializer.get_api_representation.

Nested serializer

from rest_framework import serializers
from wagtail_thumbnails.serializers import ThumbnailSerializer

class ProductSerializer(serializers.ModelSerializer):
    hero_image = ThumbnailSerializer()

    class Meta:
        model = Product
        fields = ["hero_image"]

Example output

{
  "src": "https://cdn.example.com/media/images/hero.jpg",
  "alt_text": "A sunset over the bay",
  "focal_point": { "x": 400, "y": 300, "width": 100, "height": 100 },
  "variants": {
    "full_hd": { "url": "https://.../hero.fill-1920x1280.format-webp.webp", "width": 1920, "height": 1280, "format": "webp" },
    "large":   { "url": "https://.../hero.fill-800x533.format-webp.webp",   "width": 800,  "height": 533,  "format": "webp" },
    "medium":  { "url": "https://.../hero.fill-450x300.format-webp.webp",   "width": 450,  "height": 300,  "format": "webp" },
    "small":   { "url": "https://.../hero.fill-125x83.format-webp.webp",    "width": 125,  "height": 83,   "format": "webp" }
  }
}

Notes on the shape:

  • focal_point is null when no focal point is set. Its width / height are null when only a centre point was picked (no focal area).
  • alt_text is null when nothing is available. Wagtail titles are intentionally not used as alt text - they're typically filenames, which makes for a poor screen-reader experience.
  • The block emits alt_text: "" (empty string, not null) when the editor ticks the Decorative checkbox. Mark purely visual imagery this way so assistive tech can skip it.
  • Variants never upscale: if the source is narrower than a variant's target width, the variant is generated at the source's native dimensions.

Configuration

All settings live under a single dict. User-supplied keys override defaults (variant maps fully replace, not merge).

WAGTAIL_THUMBNAILS = {
    "VARIANTS": {
        "full_hd": {"width": 1920, "format": "webp", "quality": 80},
        "large":   {"width": 800,  "format": "webp", "quality": 80},
        "medium":  {"width": 450,  "format": "webp", "quality": 80},
        "small":   {"width": 125,  "format": "webp", "quality": 40},
    },
    "MIN_IMAGE_WIDTH": 1920,   # optional
    "MIN_IMAGE_HEIGHT": 1080,  # optional
}
Key Default Description
VARIANTS see above Mapping of variant name → {width, format, quality}. Variant names become keys in the API output's variants dict.
MIN_IMAGE_WIDTH None Minimum source-image width enforced by image_resolution_validator. Omit (or set to None) to skip the width check.
MIN_IMAGE_HEIGHT None Minimum source-image height. Omit (or set to None) to skip the height check.

Supported format values: webp (default), jpeg, png. quality is honoured for webp and jpeg.

Misconfigurations (unknown keys, bad variant shapes, unsupported formats, out-of-range quality) raise ImproperlyConfigured at first access - not at request time.

Validation

ThumbnailBlock accepts a validators= keyword argument: an iterable of callables of the shape f(image) -> None that raise django.core.exceptions.ValidationError on failure. The default is the module-level image_resolution_validator, which itself silently passes any image until you configure MIN_IMAGE_WIDTH / MIN_IMAGE_HEIGHT (globally or per-instance) - so adding the block to a project without any settings is safe and side-effect-free.

from wagtail_thumbnails.blocks import ThumbnailBlock
from wagtail_thumbnails.validators import ImageResolutionValidator, image_resolution_validator

# 1. Defaults - reads MIN_IMAGE_* from WAGTAIL_THUMBNAILS, or skips if unset.
ThumbnailBlock()

# 2. Per-field threshold, ignoring whatever is in settings.
ThumbnailBlock(validators=[ImageResolutionValidator(min_width=1920, min_height=1080)])

# 3. Disable validation entirely on this field.
ThumbnailBlock(validators=[])

# 4. Add extra checks on top of the default.
def must_be_landscape(image):
    if image.width <= image.height:
        raise ValidationError("Landscape orientation required.")

ThumbnailBlock(validators=[image_resolution_validator, must_be_landscape])

# 5. Project-wide override via subclass.
class HeroImageBlock(ThumbnailBlock):
    default_validators = (ImageResolutionValidator(min_width=1920, min_height=1080),)

ImageResolutionValidator resolves each axis independently: setting only min_width (per-instance or in settings) leaves the height check disabled, and vice versa. Per-instance values take precedence over the global setting; pass None to fall back to the setting.

The validator pipeline runs from ThumbnailBlock.clean(), on top of Wagtail's built-in ImageBlock checks (a value must have alt text or be marked decorative). Validators run for non-empty values only - an empty optional block is not run through the pipeline.

Migrating from ImageChooserBlock

ThumbnailBlock subclasses Wagtail's ImageBlock, whose on-disk JSON shape inside a StreamField is {"image": <id>, "alt_text": "...", "decorative": false} - different from a bare ImageChooserBlock (whose value is just an image ID). Existing rows need a one-shot data migration:

# yourapp/migrations/00XX_migrate_image_chooser_to_thumbnail.py
from django.db import migrations

OLD_TYPE = "image_block"
NEW_TYPE = "thumbnail"


def forwards(apps, schema_editor):
    YourPage = apps.get_model("yourapp", "YourPage")
    for page in YourPage.objects.all():
        changed = False
        for block in page.body.raw_data:
            if block["type"] == OLD_TYPE:
                block["type"] = NEW_TYPE
                block["value"] = {"image": block["value"], "alt_text": "", "decorative": False}
                changed = True
        if changed:
            page.save()


class Migration(migrations.Migration):
    dependencies = [("yourapp", "00XX_previous")]
    operations = [migrations.RunPython(forwards, migrations.RunPython.noop)]

For revisions (PageRevision), apply the same transform to revision.content.

If you're already on Wagtail's own ImageBlock, no data migration is needed - the StructBlock shape matches and ThumbnailBlock is a drop-in replacement.

Development

git clone https://github.com/profilsoftware/wagtail-thumbnails
cd wagtail-thumbnails
uv sync --extra dev
uv run pytest
uv run ruff check .
uv run mypy src/

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

wagtail_thumbnails-0.2.0.tar.gz (12.5 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

wagtail_thumbnails-0.2.0-py3-none-any.whl (13.4 kB view details)

Uploaded Python 3

File details

Details for the file wagtail_thumbnails-0.2.0.tar.gz.

File metadata

  • Download URL: wagtail_thumbnails-0.2.0.tar.gz
  • Upload date:
  • Size: 12.5 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for wagtail_thumbnails-0.2.0.tar.gz
Algorithm Hash digest
SHA256 365a2884b06b89b015a8984d653666024a972b8820ebb5fcf5c9ae6844aa830c
MD5 0a4d0d4180452306e0838fa4de02aff1
BLAKE2b-256 043eedc3b3d2aad5cc8843831922afe2b3f22a55a66b94314689f7b2f3c9e24a

See more details on using hashes here.

Provenance

The following attestation bundles were made for wagtail_thumbnails-0.2.0.tar.gz:

Publisher: publish.yml on profilsoftware/wagtail-thumbnails

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file wagtail_thumbnails-0.2.0-py3-none-any.whl.

File metadata

File hashes

Hashes for wagtail_thumbnails-0.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 2ffd886c9d506f3610d038045e147a25fbc93bc21bdd86b4cc49994f0e1f23c0
MD5 97599f4eb1060eb8a74ea09e59ad0400
BLAKE2b-256 6854a64fd76670b601090541079cfb023079bb358277315aadb3c8f0f3f202c5

See more details on using hashes here.

Provenance

The following attestation bundles were made for wagtail_thumbnails-0.2.0-py3-none-any.whl:

Publisher: publish.yml on profilsoftware/wagtail-thumbnails

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page