WagtailでHTMLごとRichTextの画像をカスタマイズする

RichTextの画像でLazy loadingやレスポンシブの対応ができるようにします。

WagtailでHTMLごとRichTextの画像をカスタマイズする

Wagtailでは、RichTextの中でも画像を貼り付けることができます。デフォルトの状態ではFull width、Left-aligned、Right-alignedという三つの画像フォーマットが実装されており、HTML上ではimgタグにそれぞれのclass名が付与されるため、CSSなどで表示をコントロールすることができます。

デフォルトの実装では、それらの画像フォーマットでは画像の大きさとクロップ方法しか指定されていないため、RichText内での画像の表示について修正を加えたい場合には追加で実装が必要です。

画像のファイル形式や大きさ、クロップの方法などの画像自体のカスタマイズをするためには、新しい画像フォーマットを追加するだけで対応できます。具体的な方法は『WagtailのRichTextに画像フォーマットを追加する』の記事を参照してください。

この記事では、画像自体のカスタマイズに加えて、imgタグ以外のタグも含めてHTMLごとカスタマイズする方法を紹介します。

この方法を使うと、画像の周りにキャプションを追加したり、loading="lazy"などの属性を加えたり、pictureタグで画面幅に合わせて画像のサイズやフォーマットを調整したりすることができるようになります。

Formatクラスをオーバーライドする

Wagtailでは、Formatクラスとregister_image_formatを利用して新しい画像フォーマットを追加することができます。この方法では、画像のサイズやPNGなどのファイル形式を指定することはできるものの、テンプレート上での表示の方法はその画像に付与したclassnameに従ってCSS側で実装する必要があります。

※『WagtailのRichTextに画像フォーマットを追加する』を参照

多くの場合では上記の内容で事足りますが、画像の周りにキャプションをつけたり、loadingdecodingなどの属性を付け加えたい場合にはHTMLごとカスタマイズしたいことがあります。

その場合は、Formatクラスをオーバーライドして、挿入する際のHTMLを書き換えることで実現できます。

基本的な使い方は以下のとおりです。

# image_formats.py

from django.utils.html import format_html
from wagtail.images.formats import Format, register_image_format


# Formatを継承する
class CaptionedImageFormat(Format):

    # 画像とHTMLを紐づけるメソッドをオーバーライドする
    def image_to_html(self, image, alt_text, extra_attributes=None):

        # デフォルトのHTMLを取得する
        default_html = super().image_to_html(image, alt_text, extra_attributes)

        # デフォルトのHTMLにfigcaptionタグを付け加える
        custom_html = format_html("<figure>{}<figcaption>{}</figcaption></figure>", default_html, alt_text)
        return custom_html


# 新しい画像フォーマットとして登録する
register_image_format(
    CaptionedImageFormat('captioned_fullwidth', # 画像フォーマットを管理するユニークな名前
                         'Full width captioned', # 管理画面で表示する名前
                         'richtext-image captioned-fullwidth', # 挿入されるimgタグのclass属性
                         'width-750') # Wagtailの画像フィルタの指定
)

上のサンプルでは、CaptionedImageFormatという新しい画像フォーマットを追加しています。Formatクラスにはimage_to_htmlというメソッドが定義されており、その中でRichTextで作成された画像をHTMLに変換しています。このメソッドの中でどのようにHTMLに変換するかを自由に設定することができるので、好きな形にカスタマイズしましょう。

今回のサンプルだと、通常のimgタグに加えて、figcaptionタグを返すようにしているため、テンプレートに表示されるHTMLは以下のようになります。

<figure>
    <img alt="キャプション" class="richtext-image captioned-fullwidth" height="400" src="http://example.com/media/images/sample.png" width="750">
    <figcaption>キャプション</figcaption>
</figure>

imgタグに属性を付与する

imgタグにloadingやdecodingといった属性を付与することもできます。WagtailのソースコードではFormatクラスのimage_to_htmlメソッドの中で画像に対して属性を付与しています。同じように属性を付け加えることで、RichText内の画像でも非同期のデコードやLazy loadingを実現することができます。

# image_formats.py

from django.utils.html import escape
from wagtail.images.formats import Format, register_image_format
from wagtail.images.shortcuts import get_rendition_or_not_found


# loading="lazy"とdecoding="async"を付与した画像フォーマット
class LazyImageFormat(Format):

    def image_to_html(self, image, alt_text, extra_attributes=None):

        # デフォルトの実装をそのまま引き継いだもの
        # get_rendition_or_not_foundで、画像フィルタに合わせた画像を取得する
        # この例だと'width-800|format-webp'の画像を取得する
        if extra_attributes is None:
            extra_attributes = {}
        rendition = get_rendition_or_not_found(image, self.filter_spec)

        # imgタグに付与したい属性を指定
        extra_attributes['loading'] = "lazy"
        extra_attributes['decoding'] = "async"
        extra_attributes['alt'] = escape(alt_text)
        if self.classnames:
            extra_attributes['class'] = "%s" % escape(self.classnames)

        return rendition.img_tag(extra_attributes)


register_image_format(LazyImageFormat('lazy_image', 'Lazy Image', 'richtext-image lazy', 'width-800|format-webp'))

このサンプルでは、LazyImageFormatという新しい画像フォーマットを追加しています。横幅800pxのWebP形式の画像を、Lazy Loadingと非同期のデコーディングに対応した状態でHTML化します。

テンプレート上では以下のようなHTMLがレンダリングされることになります。

<img alt="alt text" class="richtext-image lazy" height="400" loading="lazy" decoding="async" src="http://example.com/media/images/sample.webp" width="800">

レスポンシブに対応する

Wagtailではオリジナル画像の解像度やクロップ方法、ファイル形式などを変更してテンプレート上にレンダリングできるようにした画像のことを、Renditionと呼びます。このRenditionを利用して複数の形式の画像を作成することで、RichText内の画像をレスポンシブ対応にすることができます。

以下の例は、pictureタグを利用して条件によって画像を出し分けする実装です。

# image_formats.py

from django.utils.html import format_html, escape
from wagtail.images.formats import Format, register_image_format
from wagtail.images.shortcuts import get_rendition_or_not_found


class LazyResponsiveImageFormat(Format):

    def image_to_html(self, image, alt_text, extra_attributes=None):
        # それぞれのフィルタで画像(Rendition)を作成
        rendition_webp_1500 = get_rendition_or_not_found(image, 'width-1500|format-webp')
        rendition_webp_750  = get_rendition_or_not_found(image, 'width-750|format-webp')
        rendition_jpeg_1500 = get_rendition_or_not_found(image, 'width-1500|format-jpeg')
        rendition_jpeg_750  = get_rendition_or_not_found(image, 'width-750|format-jpeg')

        # テンプレートにレンダリングするHTMLの枠組み
        html_string = '''
        <picture>
            <source type="image/webp"
                    srcset="{} 1500w,{} 750w"
                    sizes="(max-width: 1500px) 100vw, 1500px">
            <img
                 srcset="{} 1500w,{} 750w"
                 alt="{}"
                 class="{}"
                 width="1500"
                 height="{}"
                 decoding="async"
                 loading="lazy">
        </picture>
        '''

        # 引数の順番に注意してHTML化する
        return format_html(html_string,
                           rendition_webp_1500.url,
                           rendition_webp_750.url,
                           rendition_jpeg_1500.url,
                           rendition_jpeg_750.url,
                           escape(alt_text),
                           escape(self.classnames),
                           rendition_webp_1500.height)


register_image_format(LazyResponsiveImageFormat('lazy-responsive', 'Lazy responsive', 'richtext-image lazy-responsive', 'original'))

このサンプルでは、WebP形式の画像が使える環境ではWebPを使い、それ以外はJPEGを利用します。また、画面幅によって最適な解像度の画像を選択してレンダリングしてくれます。Lazy loadingにも対応しているため、ブラウザに映っていない画像のデータ取得を遅らせることができます。

この記事で紹介したように、RichText内での画像はHTMLごと振る舞いを実装できるため、仕様に応じて柔軟な対応が可能です。特にRichText内で画像を多く利用するようなメディアの実装では必須となる知識なので、ぜひ試してみてください。