diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 4b97ebab2c..fede2d1b58 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -7,6 +7,7 @@ Changelog * Add basic keyboard control and screen reader support for page listing re-ordering (Paarth Agarwal, Thomas van der Hoeven) * Add `PageQuerySet.private` method as an alias of `not_public` (Mehrdad Moradizadeh) + * Allow setting default attributes on image tags (Jake Howard) * Fix: Prevent `PageQuerySet.not_public` from returning all pages when no page restrictions exist (Mehrdad Moradizadeh) diff --git a/docs/advanced_topics/performance.md b/docs/advanced_topics/performance.md index 3189101909..15d0b4f31f 100644 --- a/docs/advanced_topics/performance.md +++ b/docs/advanced_topics/performance.md @@ -83,3 +83,9 @@ TEMPLATES = [{ To support high volumes of traffic with excellent response times, we recommend a caching proxy. Both [Varnish](https://varnish-cache.org/) and [Squid](http://www.squid-cache.org/) have been tested in production. Hosted proxies like [Cloudflare](https://www.cloudflare.com/) should also work well. Wagtail supports automatic cache invalidation for Varnish/Squid. See [](frontend_cache_purging) for more information. + +### Image attributes + +For some images, it may be beneficial to lazy load images, so the rest of the page can continue to load. It can be configured site-wide [](adding_default_attributes_to_images) or per-image [](image_tag_alt). For more details you can read about the [`loading='lazy'` attribute](https://developer.mozilla.org/en-US/docs/Web/Performance/Lazy_loading#images_and_iframes) and the [`'decoding='async'` attribute](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/img#attr-decoding) or this [web.dev article on lazy loading images](https://web.dev/lazy-loading-images/). + +This optimisation is already handled for you for images in the admin site. diff --git a/docs/releases/4.1.md b/docs/releases/4.1.md index 4a7d4969b2..1aa75706fd 100644 --- a/docs/releases/4.1.md +++ b/docs/releases/4.1.md @@ -16,10 +16,10 @@ Wagtail 4.1 is designated a Long Term Support (LTS) release. Long Term Support r * Add basic keyboard control and screen reader support for page listing re-ordering (Paarth Agarwal, Thomas van der Hoeven) * Add `PageQuerySet.private` method as an alias of `not_public` (Mehrdad Moradizadeh) + * Allow setting default attributes on image tags [](adding_default_attributes_to_images) (Jake Howard) ### Bug fixes * Prevent `PageQuerySet.not_public` from returning all pages when no page restrictions exist (Mehrdad Moradizadeh) - ## Upgrade considerations diff --git a/docs/topics/images.md b/docs/topics/images.md index c6559b4bf0..f56d196654 100644 --- a/docs/topics/images.md +++ b/docs/topics/images.md @@ -161,7 +161,7 @@ Wagtail does not allow deforming or stretching images. Image dimension ratios wi Wagtail provides two shortcuts to give greater control over the `img` element: -**1. Adding attributes to the {% image %} tag** +### 1. Adding attributes to the {% image %} tag Extra attributes can be specified with the syntax `attribute="value"`: @@ -171,7 +171,9 @@ Extra attributes can be specified with the syntax `attribute="value"`: You can set a more relevant `alt` attribute this way, overriding the one automatically generated from the title of the image. The `src`, `width`, and `height` attributes can also be overridden, if necessary. -**2. Generating the image "as foo" to access individual properties** +You can also add default attributes to all images (a default class or data attribute for example) - see [](adding_default_attributes_to_images). + +### 2. Generating the image "as foo" to access individual properties Wagtail can assign the image data to another variable using Django's `as` syntax: @@ -228,6 +230,35 @@ Therefore, if you'd added the field `author` to your AbstractImage in the above (Due to the links in the database between renditions and their parent image, you _could_ access it as `{{ tmp_photo.image.author }}`, but that has reduced readability.) +(adding_default_attributes_to_images)= + +## Adding default attributes to all images + +We can configure the `wagtail.images` application to specify additional attributes to add to images. This is done by setting up a custom `AppConfig` class within your project folder (i.e. the package containing the top-level settings and urls modules). + +To do this, create or update your existing `apps.py` file with the following: + +```python +from wagtail.images.apps import WagtailImagesAppConfig + + +class CustomImagesAppConfig(WagtailImagesAppConfig): + default_attrs = {"decoding": "async", "loading": "lazy"} +``` + +Then, replace `wagtail.images` in `settings.INSTALLED_APPS` with the path to `CustomUsersAppConfig`: + +```python +INSTALLED_APPS = [ + ..., + "myapplication.apps.CustomImagesAppConfig", + # "wagtail.images", + ..., +] +``` + +Now, images created with `{% image %}` will additionally have `decoding="async" loading="lazy"` attributes. This also goes for images added to Rich Text and `ImageBlock` blocks. + ## Alternative HTML tags The `as` keyword allows alternative HTML image tags (such as `` or ``) to be used. diff --git a/wagtail/images/apps.py b/wagtail/images/apps.py index 223eaa45a6..af91a516dd 100644 --- a/wagtail/images/apps.py +++ b/wagtail/images/apps.py @@ -11,6 +11,7 @@ class WagtailImagesAppConfig(AppConfig): label = "wagtailimages" verbose_name = _("Wagtail images") default_auto_field = "django.db.models.AutoField" + default_attrs = {} def ready(self): register_signal_handlers() diff --git a/wagtail/images/models.py b/wagtail/images/models.py index df28058c0a..a335328a27 100644 --- a/wagtail/images/models.py +++ b/wagtail/images/models.py @@ -7,6 +7,7 @@ from contextlib import contextmanager from io import BytesIO from typing import Union +from django.apps import apps from django.conf import settings from django.core import checks from django.core.cache import InvalidCacheBackendError, caches @@ -826,7 +827,11 @@ class AbstractRendition(ImageFileMixin, models.Model): def img_tag(self, extra_attributes={}): attrs = self.attrs_dict.copy() + + attrs.update(apps.get_app_config("wagtailimages").default_attrs) + attrs.update(extra_attributes) + return mark_safe("".format(flatatt(attrs))) def __html__(self): diff --git a/wagtail/images/tests/test_blocks.py b/wagtail/images/tests/test_blocks.py index bf8d537786..5982f914f5 100644 --- a/wagtail/images/tests/test_blocks.py +++ b/wagtail/images/tests/test_blocks.py @@ -1,6 +1,8 @@ # -*- coding: utf-8 -* import os +import unittest.mock +from django.apps import apps from django.conf import settings from django.core import serializers from django.test import TestCase @@ -55,6 +57,19 @@ class TestImageChooserBlock(TestCase): self.assertHTMLEqual(html, expected_html) + def test_render_with_custom_default_attrs(self): + block = ImageChooserBlock() + with unittest.mock.patch.object( + apps.get_app_config("wagtailimages"), + "default_attrs", + new={"decoding": "async", "loading": "lazy"}, + ): + html = block.render(self.bad_image) + self.assertHTMLEqual( + html, + 'missing image', + ) + def test_render_missing(self): block = ImageChooserBlock() html = block.render(self.bad_image) diff --git a/wagtail/images/tests/test_jinja2.py b/wagtail/images/tests/test_jinja2.py index 28b6c9264a..351a542c9f 100644 --- a/wagtail/images/tests/test_jinja2.py +++ b/wagtail/images/tests/test_jinja2.py @@ -1,6 +1,8 @@ import os +import unittest.mock from django import template +from django.apps import apps from django.conf import settings from django.core import serializers from django.template import engines @@ -99,6 +101,19 @@ class TestImagesJinja(TestCase): with self.assertRaises(template.TemplateSyntaxError): self.render('{{ image(myimage, "fill-200×200") }}', {"myimage": self.image}) + def test_custom_default_attrs(self): + with unittest.mock.patch.object( + apps.get_app_config("wagtailimages"), + "default_attrs", + new={"decoding": "async", "loading": "lazy"}, + ): + self.assertHTMLEqual( + self.render( + '{{ image(myimage, "width-200") }}', {"myimage": self.bad_image} + ), + 'missing image', + ) + def test_chaining_filterspecs(self): self.assertHTMLEqual( self.render(