A lightweight Wagtail StreamField block + DRF serializer that exposes responsive WebP thumbnails with dimensions and focal points.
Project description
wagtail-thumbnails
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-inImageBlockthat adds a pluggable validator pipeline and emits aThumbnailSerializerpayload - A
ThumbnailSerializerthat emits:- Source URL
- Resolved alt text (block override → image
description→null; 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 withurl,width,height,format
- An
ImageResolutionValidatoryou can configure globally (WAGTAIL_THUMBNAILSsettings) 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:
- A configurable validator pipeline (see Validation).
- 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_pointisnullwhen no focal point is set. Itswidth/heightarenullwhen only a centre point was picked (no focal area).alt_textisnullwhen 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
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
365a2884b06b89b015a8984d653666024a972b8820ebb5fcf5c9ae6844aa830c
|
|
| MD5 |
0a4d0d4180452306e0838fa4de02aff1
|
|
| BLAKE2b-256 |
043eedc3b3d2aad5cc8843831922afe2b3f22a55a66b94314689f7b2f3c9e24a
|
Provenance
The following attestation bundles were made for wagtail_thumbnails-0.2.0.tar.gz:
Publisher:
publish.yml on profilsoftware/wagtail-thumbnails
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
wagtail_thumbnails-0.2.0.tar.gz -
Subject digest:
365a2884b06b89b015a8984d653666024a972b8820ebb5fcf5c9ae6844aa830c - Sigstore transparency entry: 1579007043
- Sigstore integration time:
-
Permalink:
profilsoftware/wagtail-thumbnails@394ce087f9e0d5673236e3d9b53cb3bd48a0de1d -
Branch / Tag:
refs/tags/v0.2.0 - Owner: https://github.com/profilsoftware
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@394ce087f9e0d5673236e3d9b53cb3bd48a0de1d -
Trigger Event:
push
-
Statement type:
File details
Details for the file wagtail_thumbnails-0.2.0-py3-none-any.whl.
File metadata
- Download URL: wagtail_thumbnails-0.2.0-py3-none-any.whl
- Upload date:
- Size: 13.4 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
2ffd886c9d506f3610d038045e147a25fbc93bc21bdd86b4cc49994f0e1f23c0
|
|
| MD5 |
97599f4eb1060eb8a74ea09e59ad0400
|
|
| BLAKE2b-256 |
6854a64fd76670b601090541079cfb023079bb358277315aadb3c8f0f3f202c5
|
Provenance
The following attestation bundles were made for wagtail_thumbnails-0.2.0-py3-none-any.whl:
Publisher:
publish.yml on profilsoftware/wagtail-thumbnails
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
wagtail_thumbnails-0.2.0-py3-none-any.whl -
Subject digest:
2ffd886c9d506f3610d038045e147a25fbc93bc21bdd86b4cc49994f0e1f23c0 - Sigstore transparency entry: 1579007241
- Sigstore integration time:
-
Permalink:
profilsoftware/wagtail-thumbnails@394ce087f9e0d5673236e3d9b53cb3bd48a0de1d -
Branch / Tag:
refs/tags/v0.2.0 - Owner: https://github.com/profilsoftware
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@394ce087f9e0d5673236e3d9b53cb3bd48a0de1d -
Trigger Event:
push
-
Statement type: