瀏覽代碼

convert the docs/topics/** from RST to Markdown

Relates to #8383

* Convert the image tag topic to Markdown
* Convert the permissions usage to Markdown
* Convert the topic of snippets to Markdown
* Convert the streamfield topic to markdown
* Convert the writing templates doc to Markdown
Sævar Öfjörð Magnússon 2 年之前
父節點
當前提交
a316408928

+ 1 - 1
CHANGELOG.txt

@@ -5,7 +5,7 @@ Changelog
 ~~~~~~~~~~~~~~~~
 
  * Add clarity to confirmation when being asked to convert an external link to an internal one (Thijs Kramer)
- * Convert various pages in the documentation to Markdown (Khanh Hoang, Vu Pham, Daniel Kirkham, LB (Ben) Johnston, Thiago Costa de Souza, Benedict Faw, Noble Mittal)
+ * Convert various pages in the documentation to Markdown (Khanh Hoang, Vu Pham, Daniel Kirkham, LB (Ben) Johnston, Thiago Costa de Souza, Benedict Faw, Noble Mittal, Sævar Öfjörð Magnússon)
  * Add `base_url_path` to `ModelAdmin` so that the default URL structure of app_label/model_name can be overridden (Vu Pham, Khanh Hoang)
  * Add `full_url` to the API output of `ImageRenditionField` (Paarth Agarwal)
  * Fix issue where `ModelAdmin` index listings with export list enabled would show buttons with an incorrect layout (Josh Woodcock)

+ 1 - 1
docs/releases/4.0.md

@@ -16,7 +16,7 @@ When using a queryset to render a list of images, you can now use the ``prefetch
 ### Other features
 
  * Add clarity to confirmation when being asked to convert an external link to an internal one (Thijs Kramer)
- * Convert various pages in the documentation to Markdown (Khanh Hoang, Vu Pham, Daniel Kirkham, LB (Ben) Johnston, Thiago Costa de Souza, Benedict Faw, Noble Mittal)
+ * Convert various pages in the documentation to Markdown (Khanh Hoang, Vu Pham, Daniel Kirkham, LB (Ben) Johnston, Thiago Costa de Souza, Benedict Faw, Noble Mittal, Sævar Öfjörð Magnússon)
  * Add `base_url_path` to `ModelAdmin` so that the default URL structure of app_label/model_name can be overridden (Vu Pham, Khanh Hoang)
  * Add `full_url` to the API output of `ImageRenditionField` (Paarth Agarwal)
  * Use `InlinePanel`'s label when available for field comparison label (Sandil Ranasinghe)

+ 389 - 0
docs/topics/images.md

@@ -0,0 +1,389 @@
+(image_tag)=
+
+# How to use images in templates
+
+The `image` tag inserts an XHTML-compatible `img` element into the page, setting its `src`, `width`, `height` and `alt`. See also [](image_tag_alt).
+
+The syntax for the tag is thus:
+
+```html+django
+{% image [image] [resize-rule] %}
+```
+
+**Both the image and resize rule must be passed to the template tag.**
+
+For example:
+
+```html+django
+{% load wagtailimages_tags %}
+...
+
+<!-- Display the image scaled to a width of 400 pixels: -->
+{% image page.photo width-400 %}
+
+<!-- Display it again, but this time as a square thumbnail: -->
+{% image page.photo fill-80x80 %}
+```
+
+In the above syntax example `[image]` is the Django object referring to the image. If your page model defined a field called "photo" then `[image]` would probably be `page.photo`. The `[resize-rule]` defines how the image is to be resized when inserted into the page. Various resizing methods are supported, to cater to different use cases (e.g. lead images that span the whole width of the page, or thumbnails to be cropped to a fixed size).
+
+Note that a space separates `[image]` and `[resize-rule]`, but the resize rule must not contain spaces. The width is always specified before the height. Resized images will maintain their original aspect ratio unless the `fill` rule is used, which may result in some pixels being cropped.
+
+## Available resizing methods
+
+The available resizing methods are as follows:
+
+### `max`
+
+(takes two dimensions)
+
+```html+django
+{% image page.photo max-1000x500 %}
+```
+
+Fit **within** the given dimensions.
+
+The longest edge will be reduced to the matching dimension specified. For example, a portrait image of width 1000 and height 2000, treated with the `max-1000x500` rule (a landscape layout) would result in the image being shrunk so the _height_ was 500 pixels and the width was 250.
+
+![Example of max filter on an image](../_static/images/image_filter_max.png)
+
+Example: The image will keep its proportions but fit within the max (green line) dimensions provided.
+
+### `min`
+
+(takes two dimensions)
+
+```html+django
+{% image page.photo min-500x200 %}
+```
+
+**Cover** the given dimensions.
+
+This may result in an image slightly **larger** than the dimensions you specify. A square image of width 2000 and height 2000, treated with the `min-500x200` rule would have its height and width changed to 500, i.e matching the _width_ of the resize-rule, but greater than the height.
+
+![Example of min filter on an image](../_static/images/image_filter_min.png)
+
+Example: The image will keep its proportions while filling at least the min (green line) dimensions provided.
+
+### `width`
+
+(takes one dimension)
+
+```html+django
+{% image page.photo width-640 %}
+```
+
+Reduces the width of the image to the dimension specified.
+
+### `height`
+
+(takes one dimension)
+
+```html+django
+{% image page.photo height-480 %}
+```
+
+Reduces the height of the image to the dimension specified.
+
+### `scale`
+
+(takes percentage)
+
+```html+django
+{% image page.photo scale-50 %}
+```
+
+Resize the image to the percentage specified.
+
+### `fill`
+
+(takes two dimensions and an optional `-c` parameter)
+
+```html+django
+{% image page.photo fill-200x200 %}
+```
+
+Resize and **crop** to fill the **exact** dimensions specified.
+
+This can be particularly useful for websites requiring square thumbnails of arbitrary images. For example, a landscape image of width 2000 and height 1000 treated with the `fill-200x200` rule would have its height reduced to 200, then its width (ordinarily 400) cropped to 200.
+
+This resize-rule will crop to the image's focal point if it has been set. If not, it will crop to the centre of the image.
+
+![Example of fill filter on an image](../_static/images/image_filter_fill.png)
+
+Example: The image is scaled and also cropped (red line) to fit as much of the image as possible within the provided dimensions.
+
+**On images that won't upscale**
+
+It's possible to request an image with `fill` dimensions that the image can't support without upscaling. e.g. an image of width 400 and height 200 requested with `fill-400x400`. In this situation the _ratio of the requested fill_ will be matched, but the dimension will not. So that example 400x200 image (a 2:1 ratio) could become 200x200 (a 1:1 ratio, matching the resize-rule).
+
+**Cropping closer to the focal point**
+
+By default, Wagtail will only crop enough to change the aspect ratio of the image to match the ratio in the resize-rule.
+
+In some cases (e.g. thumbnails), it may be preferable to crop closer to the focal point, so that the subject of the image is more prominent.
+
+You can do this by appending `-c<percentage>` at the end of the resize-rule. For example, if you would like the image to be cropped as closely as possible to its focal point, add `-c100`:
+
+```html+django
+{% image page.photo fill-200x200-c100 %}
+```
+
+This will crop the image as much as it can, without cropping into the focal point.
+
+If you find that `-c100` is too close, you can try `-c75` or `-c50`. Any whole number from 0 to 100 is accepted.
+
+![Example of fill filter on an image with a focal point set](../_static/images/image_filter_fill_focal.png)
+
+Example: The focal point is set off centre so the image is scaled and also cropped like fill, however the center point of the crop is positioned closer the focal point.
+
+![Example of fill and closeness filter on an image with a focal point set](../_static/images/image_filter_fill_focal_close.png)
+
+Example: With `-c75` set, the final crop will be closer to the focal point.
+
+### `original`
+
+(takes no dimensions)
+
+```html+django
+{% image page.photo original %}
+```
+
+Renders the image at its original size.
+
+```{note}
+Wagtail does not allow deforming or stretching images. Image dimension ratios will always be kept. Wagtail also *does not support upscaling*. Small images forced to appear at larger sizes will "max out" at their native dimensions.
+```
+
+(image_tag_alt)=
+
+## More control over the `img` tag
+
+Wagtail provides two shortcuts to give greater control over the `img` element:
+
+**1. Adding attributes to the {% image %} tag**
+
+Extra attributes can be specified with the syntax `attribute="value"`:
+
+```html+django
+{% image page.photo width-400 class="foo" id="bar" %}
+```
+
+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**
+
+Wagtail can assign the image data to another variable using Django's `as` syntax:
+
+```html+django
+{% image page.photo width-400 as tmp_photo %}
+
+<img src="{{ tmp_photo.url }}" width="{{ tmp_photo.width }}"
+    height="{{ tmp_photo.height }}" alt="{{ tmp_photo.alt }}" class="my-custom-class" />
+```
+
+```{note}
+The image property used for the `src` attribute is `image.url`, not `image.src`.
+```
+
+This syntax exposes the underlying image Rendition (`tmp_photo`) to the developer. A "Rendition" contains the information specific to the way you've requested to format the image using the resize-rule, i.e. dimensions and source URL. The following properties are available:
+
+### `url`
+
+URL to the resized version of the image. This may be a local URL (such as `/static/images/example.jpg`) or a full URL (such as `https://assets.example.com/images/example.jpg`), depending on how static files are configured.
+
+### `width`
+
+Image width after resizing.
+
+### `height`
+
+Image height after resizing.
+
+### `alt`
+
+Alternative text for the image, typically taken from the image title.
+
+### `attrs`
+
+A shorthand for outputting the attributes `src`, `width`, `height` and `alt` in one go:
+
+```html+django
+<img {{ tmp_photo.attrs }} class="my-custom-class" />
+```
+
+### `full_url`
+
+Same as `url`, but always returns a full absolute URL. This requires `BASE_URL` to be set in the project settings.
+
+This is useful for images that will be re-used outside of the current site, such as social share images:
+
+```html+django
+<meta name="twitter:image" content="{{ tmp_photo.full_url }}">
+```
+
+If your site defines a custom image model using `AbstractImage`, any additional fields you add to an image (e.g. a copyright holder) are **not** included in the rendition.
+
+Therefore, if you'd added the field `author` to your AbstractImage in the above example, you'd access it using `{{ page.photo.author }}` rather than `{{ tmp_photo.author }}`.
+
+(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.)
+
+## Alternative HTML tags
+
+The `as` keyword allows alternative HTML image tags (such as `<picture>` or `<amp-img>`) to be used.
+For example, to use the `<picture>` tag:
+
+```html+django
+<picture>
+    {% image page.photo width-800 as wide_photo %}
+    <source srcset="{{ wide_photo.url }}" media="(min-width: 800px)">
+    {% image page.photo width-400 %}
+</picture>
+```
+
+And to use the `<amp-img>` tag (based on the `Mountains example <https://amp.dev/documentation/components/amp-img/#example:-specifying-a-fallback-image>`\_ from the AMP docs):
+
+```html+django
+{% image image width-550 format-webp as webp_image %}
+{% image image width-550 format-jpeg as jpeg_image %}
+
+<amp-img alt="{{ image.alt }}"
+    width="{{ webp_image.width }}"
+    height="{{ webp_image.height }}"
+    src="{{ webp_image.url }}">
+    <amp-img alt="{{ image.alt }}"
+        fallback
+        width="{{ jpeg_image.width }}"
+        height="{{ jpeg_image.height }}"
+        src="{{ jpeg_image.url }}"></amp-img>
+</amp-img>
+```
+
+## Images embedded in rich text
+
+The information above relates to images defined via image-specific fields in your model. However, images can also be embedded arbitrarily in Rich Text fields by the page editor (see [](rich-text)).
+
+Images embedded in Rich Text fields can't be controlled by the template developer as easily. There are no image objects to work with, so the `{% image %}` template tag can't be used. Instead, editors can choose from one of a number of image "Formats" at the point of inserting images into their text.
+
+Wagtail comes with three pre-defined image formats, but more can be defined in Python by the developer. These formats are:
+
+### `Full width`
+
+Creates an image rendition using `width-800`, giving the <img> tag the CSS class `full-width`.
+
+### `Left-aligned`
+
+Creates an image rendition using `width-500`, giving the <img> tag the CSS class `left`.
+
+### `Right-aligned`
+
+Creates an image rendition using `width-500`, giving the <img> tag the CSS class `right`.
+
+```{note}
+The CSS classes added to images do **not** come with any accompanying stylesheets, or inline styles. e.g. the `left` class will do nothing, by default. The developer is expected to add these classes to their front end CSS files, to define exactly what they want `left`, `right` or `full-width` to mean.
+```
+
+For more information about image formats, including creating your own, see [](rich_text_image_formats).
+
+(output_image_format)=
+
+## Output image format
+
+Wagtail may automatically change the format of some images when they are resized:
+
+-   PNG and JPEG images don't change format
+-   GIF images without animation are converted to PNGs
+-   BMP images are converted to PNGs
+-   WebP images are converted to PNGs
+
+It is also possible to override the output format on a per-tag basis by using the
+`format` filter after the resize rule.
+
+For example, to make the tag always convert the image to a JPEG, use `format-jpeg`:
+
+```html+django
+{% image page.photo width-400 format-jpeg %}
+```
+
+You may also use `format-png` or `format-gif`.
+
+### Lossless WebP
+
+You can encode the image into lossless WebP format by using the `format-webp-lossless` filter:
+
+```html+django
+{% image page.photo width-400 format-webp-lossless %}
+```
+
+(image_background_color)=
+
+## Background colour
+
+The PNG and GIF image formats both support transparency, but if you want to
+convert images to JPEG format, the transparency will need to be replaced with a solid background colour.
+
+By default, Wagtail will set the background to white. But if a white background doesn't fit your design, you can specify a colour using the `bgcolor` filter.
+
+This filter takes a single argument, which is a CSS 3 or 6 digit hex code
+representing the colour you would like to use:
+
+```html+django
+{# Sets the image background to black #}
+{% image page.photo width-400 bgcolor-000 format-jpeg %}
+```
+
+(image_quality)=
+
+## Image quality
+
+Wagtail's JPEG and WebP image quality settings default to 85 (which is quite high).
+This can be changed either globally or on a per-tag basis.
+
+### Changing globally
+
+Use the `WAGTAILIMAGES_JPEG_QUALITY` and `WAGTAILIMAGES_WEBP_QUALITY` settings to change the global defaults of JPEG and WebP quality:
+
+```python
+# settings.py
+
+# Make low-quality but small images
+WAGTAILIMAGES_JPEG_QUALITY = 40
+WAGTAILIMAGES_WEBP_QUALITY = 45
+```
+
+Note that this won't affect any previously generated images so you may want to delete all renditions so they can regenerate with the new setting. This can be done from the Django shell:
+
+```python
+# Replace this with your custom rendition model if you use one
+>>> from wagtail.images.models import Rendition
+>>> Rendition.objects.all().delete()
+```
+
+You can also directly use the image management command from the console for regenerating the renditions:
+
+```console
+$ ./manage.py wagtail_update_image_renditions --purge
+```
+
+You can read more about this command from [](wagtail_update_image_renditions)
+
+### Changing per-tag
+
+It's also possible to have different JPEG and WebP qualities on individual tags by using `jpegquality` and `webpquality` filters. This will always override the default setting:
+
+```html+django
+{% image page.photo_jpeg width-400 jpegquality-40 %}
+{% image page.photo_webp width-400 webpquality-50 %}
+```
+
+Note that this will have no effect on PNG or GIF files. If you want all images to be low quality, you can use this filter with `format-jpeg` or `format-webp` (which forces all images to output in JPEG or WebP format):
+
+```html+Django
+{% image page.photo width-400 format-jpeg jpegquality-40 %}
+{% image page.photo width-400 format-webp webpquality-50 %}
+```
+
+### Generating image renditions in Python
+
+All of the image transformations mentioned above can also be used directly in Python code.
+See [](image_renditions).

+ 0 - 426
docs/topics/images.rst

@@ -1,426 +0,0 @@
-.. _image_tag:
-
-How to use images in templates
-==============================
-
-The ``image`` tag inserts an XHTML-compatible ``img`` element into the page, setting its ``src``, ``width``, ``height`` and ``alt``. See also :ref:`image_tag_alt`.
-
-The syntax for the tag is thus:
-
-.. code-block:: html+django
-
-    {% image [image] [resize-rule] %}
-
-**Both the image and resize rule must be passed to the template tag.**
-
-For example:
-
-.. code-block:: html+django
-
-    {% load wagtailimages_tags %}
-    ...
-
-    <!-- Display the image scaled to a width of 400 pixels: -->
-    {% image page.photo width-400 %}
-
-    <!-- Display it again, but this time as a square thumbnail: -->
-    {% image page.photo fill-80x80 %}
-
-In the above syntax example ``[image]`` is the Django object referring to the image. If your page model defined a field called "photo" then ``[image]`` would probably be ``page.photo``. The ``[resize-rule]`` defines how the image is to be resized when inserted into the page. Various resizing methods are supported, to cater to different use cases (e.g. lead images that span the whole width of the page, or thumbnails to be cropped to a fixed size).
-
-Note that a space separates ``[image]`` and ``[resize-rule]``, but the resize rule must not contain spaces. The width is always specified before the height. Resized images will maintain their original aspect ratio unless the ``fill`` rule is used, which may result in some pixels being cropped.
-
-
-The available resizing methods are as follows:
-
-
-.. glossary::
-
-    ``max``
-        (takes two dimensions)
-
-        .. code-block:: html+django
-
-            {% image page.photo max-1000x500 %}
-
-        Fit **within** the given dimensions.
-
-        The longest edge will be reduced to the matching dimension specified. For example, a portrait image of width 1000 and height 2000, treated with the ``max-1000x500`` rule (a landscape layout) would result in the image being shrunk so the *height* was 500 pixels and the width was 250.
-
-        .. figure:: ../_static/images/image_filter_max.png
-          :alt: Example of max filter on an image.
-
-          Example: The image will keep its proportions but fit within the max (green line) dimensions provided.
-
-
-    ``min``
-        (takes two dimensions)
-
-        .. code-block:: html+django
-
-            {% image page.photo min-500x200 %}
-
-        **Cover** the given dimensions.
-
-        This may result in an image slightly **larger** than the dimensions you specify. A square image of width 2000 and height 2000, treated with the ``min-500x200`` rule would have its height and width changed to 500, i.e matching the *width* of the resize-rule, but greater than the height.
-
-        .. figure:: ../_static/images/image_filter_min.png
-          :alt: Example of min filter on an image.
-
-          Example: The image will keep its proportions while filling at least the min (green line) dimensions provided.
-
-
-    ``width``
-        (takes one dimension)
-
-        .. code-block:: html+django
-
-            {% image page.photo width-640 %}
-
-        Reduces the width of the image to the dimension specified.
-
-    ``height``
-        (takes one dimension)
-
-        .. code-block:: html+django
-
-            {% image page.photo height-480 %}
-
-        Reduces the height of the image to the dimension specified.
-
-    ``scale``
-        (takes percentage)
-
-        .. code-block:: html+django
-
-            {% image page.photo scale-50 %}
-
-        Resize the image to the percentage specified.
-
-    ``fill``
-        (takes two dimensions and an optional ``-c`` parameter)
-
-        .. code-block:: html+django
-
-            {% image page.photo fill-200x200 %}
-
-        Resize and **crop** to fill the **exact** dimensions specified.
-
-        This can be particularly useful for websites requiring square thumbnails of arbitrary images. For example, a landscape image of width 2000 and height 1000 treated with the ``fill-200x200`` rule would have its height reduced to 200, then its width (ordinarily 400) cropped to 200.
-
-        This resize-rule will crop to the image's focal point if it has been set. If not, it will crop to the centre of the image.
-
-        .. figure:: ../_static/images/image_filter_fill.png
-          :alt: Example of fill filter on an image.
-
-          Example: The image is scaled and also cropped (red line) to fit as much of the image as possible within the provided dimensions.
-
-
-        **On images that won't upscale**
-
-        It's possible to request an image with ``fill`` dimensions that the image can't support without upscaling. e.g. an image of width 400 and height 200 requested with ``fill-400x400``. In this situation the *ratio of the requested fill* will be matched, but the dimension will not. So that example 400x200 image (a 2:1 ratio) could become 200x200 (a 1:1 ratio, matching the resize-rule).
-
-        **Cropping closer to the focal point**
-
-        By default, Wagtail will only crop enough to change the aspect ratio of the image to match the ratio in the resize-rule.
-
-        In some cases (e.g. thumbnails), it may be preferable to crop closer to the focal point, so that the subject of the image is more prominent.
-
-        You can do this by appending ``-c<percentage>`` at the end of the resize-rule. For example, if you would like the image to be cropped as closely as possible to its focal point, add ``-c100``:
-
-        .. code-block:: html+django
-
-            {% image page.photo fill-200x200-c100 %}
-
-        This will crop the image as much as it can, without cropping into the focal point.
-
-        If you find that ``-c100`` is too close, you can try ``-c75`` or ``-c50``. Any whole number from 0 to 100 is accepted.
-
-        .. figure:: ../_static/images/image_filter_fill_focal.png
-          :alt: Example of fill filter on an image with a focal point set.
-
-          Example: The focal point is set off centre so the image is scaled and also cropped like fill, however the center point of the crop is positioned closer the focal point.
-
-        .. figure:: ../_static/images/image_filter_fill_focal_close.png
-          :alt: Example of fill and closeness filter on an image with a focal point set.
-
-          Example: With ``-c75`` set, the final crop will be closer to the focal point.
-
-
-    ``original``
-        (takes no dimensions)
-
-        .. code-block:: html+django
-
-            {% image page.photo original %}
-
-        Renders the image at its original size.
-
-
-
-.. Note::
-    Wagtail does not allow deforming or stretching images. Image dimension ratios will always be kept. Wagtail also *does not support upscaling*. Small images forced to appear at larger sizes will "max out" at their native dimensions.
-
-
-.. _image_tag_alt:
-
-More control over the ``img`` tag
----------------------------------
-
-Wagtail provides two shortcuts to give greater control over the ``img`` element:
-
-**1. Adding attributes to the  {% image %} tag**
-
-Extra attributes can be specified with the syntax ``attribute="value"``:
-
-.. code-block:: html+django
-
-    {% image page.photo width-400 class="foo" id="bar" %}
-
-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**
-
-Wagtail can assign the image data to another variable using Django's ``as`` syntax:
-
-.. code-block:: html+django
-
-    {% image page.photo width-400 as tmp_photo %}
-
-    <img src="{{ tmp_photo.url }}" width="{{ tmp_photo.width }}"
-        height="{{ tmp_photo.height }}" alt="{{ tmp_photo.alt }}" class="my-custom-class" />
-
-.. Note::
-    The image property used for the ``src`` attribute is ``image.url``, not ``image.src``.
-
-This syntax exposes the underlying image Rendition (``tmp_photo``) to the developer. A "Rendition" contains the information specific to the way you've requested to format the image using the resize-rule, i.e. dimensions and source URL. The following properties are available:
-
-.. attribute:: url
-
-    URL to the resized version of the image. This may be a local URL (such as ``/static/images/example.jpg``) or a full URL (such as ``https://assets.example.com/images/example.jpg``), depending on how static files are configured.
-
-.. attribute:: width
-
-    Image width after resizing.
-
-.. attribute:: height
-
-    Image height after resizing.
-
-.. attribute:: alt
-
-    Alternative text for the image, typically taken from the image title.
-
-.. attribute:: attrs
-
-    A shorthand for outputting the attributes ``src``, ``width``, ``height`` and ``alt`` in one go:
-
-    .. code-block:: html+django
-
-        <img {{ tmp_photo.attrs }} class="my-custom-class" />
-
-.. attribute:: full_url
-
-    Same as ``url``, but always returns a full absolute URL. This requires ``BASE_URL`` to be set in the project settings.
-
-    This is useful for images that will be re-used outside of the current site, such as social share images:
-
-    .. code-block:: html+django
-
-        <meta name="twitter:image" content="{{ tmp_photo.full_url }}">
-
-
-If your site defines a custom image model using ``AbstractImage``, any additional fields you add to an image (e.g. a copyright holder) are **not** included in the rendition.
-
-Therefore, if you'd added the field ``author`` to your AbstractImage in the above example, you'd access it using ``{{ page.photo.author }}`` rather than ``{{ tmp_photo.author }}``.
-
-(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.)
-
-
-The ``attrs`` shortcut
------------------------
-
-You can also use the ``attrs`` property as a shorthand to output the attributes ``src``, ``width``, ``height`` and ``alt`` in one go:
-
-.. code-block:: html+django
-
-    <img {{ tmp_photo.attrs }} class="my-custom-class" />
-
-
-Alternative HTML tags
----------------------
-
-The ``as`` keyword allows alternative HTML image tags (such as ``<picture>`` or ``<amp-img>``) to be used.
-For example, to use the ``<picture>`` tag:
-
-.. code-block:: html+django
-
-    <picture>
-        {% image page.photo width-800 as wide_photo %}
-        <source srcset="{{ wide_photo.url }}" media="(min-width: 800px)">
-        {% image page.photo width-400 %}
-    </picture>
-
-And to use the ``<amp-img>`` tag (based on the `Mountains example <https://amp.dev/documentation/components/amp-img/#example:-specifying-a-fallback-image>`_ from the AMP docs):
-
-.. code-block:: html+django
-
-    {% image image width-550 format-webp as webp_image %}
-    {% image image width-550 format-jpeg as jpeg_image %}
-
-    <amp-img alt="{{ image.alt }}"
-        width="{{ webp_image.width }}"
-        height="{{ webp_image.height }}"
-        src="{{ webp_image.url }}">
-        <amp-img alt="{{ image.alt }}"
-            fallback
-            width="{{ jpeg_image.width }}"
-            height="{{ jpeg_image.height }}"
-            src="{{ jpeg_image.url }}"></amp-img>
-    </amp-img>
-
-
-Images embedded in rich text
-----------------------------
-
-The information above relates to images defined via image-specific fields in your model. However, images can also be embedded arbitrarily in Rich Text fields by the page editor (see :ref:`rich-text`).
-
-Images embedded in Rich Text fields can't be controlled by the template developer as easily. There are no image objects to work with, so the ``{% image %}`` template tag can't be used. Instead, editors can choose from one of a number of image "Formats" at the point of inserting images into their text.
-
-Wagtail comes with three pre-defined image formats, but more can be defined in Python by the developer. These formats are:
-
-.. glossary::
-
-    ``Full width``
-        Creates an image rendition using ``width-800``, giving the <img> tag the CSS class ``full-width``.
-
-    ``Left-aligned``
-        Creates an image rendition using ``width-500``, giving the <img> tag the CSS class ``left``.
-
-    ``Right-aligned``
-        Creates an image rendition using ``width-500``, giving the <img> tag the CSS class ``right``.
-
-.. Note::
-
-    The CSS classes added to images do **not** come with any accompanying stylesheets, or inline styles. e.g. the ``left`` class will do nothing, by default. The developer is expected to add these classes to their front end CSS files, to define exactly what they want ``left``, ``right`` or ``full-width`` to mean.
-
-For more information about image formats, including creating your own, see :ref:`rich_text_image_formats`
-
-.. _output_image_format:
-
-Output image format
--------------------
-
-Wagtail may automatically change the format of some images when they are resized:
-
- - PNG and JPEG images don't change format
- - GIF images without animation are converted to PNGs
- - BMP images are converted to PNGs
- - WebP images are converted to PNGs
-
-It is also possible to override the output format on a per-tag basis by using the
-``format`` filter after the resize rule.
-
-For example, to make the tag always convert the image to a JPEG, use ``format-jpeg``:
-
-.. code-block:: html+Django
-
-    {% image page.photo width-400 format-jpeg %}
-
-You may also use ``format-png`` or ``format-gif``.
-
-Lossless WebP
-^^^^^^^^^^^^^
-
-You can encode the image into lossless WebP format by using the ``format-webp-lossless`` filter:
-
-.. code-block:: html+Django
-
-    {% image page.photo width-400 format-webp-lossless %}
-
-.. _image_background_color:
-
-Background colour
------------------
-
-The PNG and GIF image formats both support transparency, but if you want to
-convert images to JPEG format, the transparency will need to be replaced with a
-solid background colour.
-
-By default, Wagtail will set the background to white. But if a white background
-doesn't fit your design, you can specify a colour using the ``bgcolor`` filter.
-
-This filter takes a single argument, which is a CSS 3 or 6 digit hex code
-representing the colour you would like to use:
-
-.. code-block:: html+Django
-
-    {# Sets the image background to black #}
-    {% image page.photo width-400 bgcolor-000 format-jpeg %}
-
-.. _image_quality:
-
-Image quality
--------------
-
-Wagtail's JPEG and WebP image quality settings default to 85 (which is quite high).
-This can be changed either globally or on a per-tag basis.
-
-Changing globally
-^^^^^^^^^^^^^^^^^
-
-Use the ``WAGTAILIMAGES_JPEG_QUALITY`` and ``WAGTAILIMAGES_WEBP_QUALITY`` settings
-to change the global defaults of JPEG and WebP quality:
-
-.. code-block:: python
-
-    # settings.py
-
-    # Make low-quality but small images
-    WAGTAILIMAGES_JPEG_QUALITY = 40
-    WAGTAILIMAGES_WEBP_QUALITY = 45
-
-Note that this won't affect any previously generated images so you may want to
-delete all renditions so they can regenerate with the new setting. This can be
-done from the Django shell:
-
-.. code-block:: python
-
-    # Replace this with your custom rendition model if you use one
-    >>> from wagtail.images.models import Rendition
-    >>> Rendition.objects.all().delete()
-
-You can also directly use the image management command from the console for regenerating the renditions:
-
-.. code-block:: console
-
-    $ ./manage.py wagtail_update_image_renditions --purge
-
-You can read more about this command from :ref:`wagtail_update_image_renditions`
-
-Changing per-tag
-^^^^^^^^^^^^^^^^
-
-It's also possible to have different JPEG and WebP qualities on individual tags
-by using ``jpegquality`` and ``webpquality`` filters. This will always override
-the default setting:
-
-.. code-block:: html+Django
-
-    {% image page.photo_jpeg width-400 jpegquality-40 %}
-    {% image page.photo_webp width-400 webpquality-50 %}
-
-Note that this will have no effect on PNG or GIF files. If you want all images
-to be low quality, you can use this filter with ``format-jpeg`` or ``format-webp``
-(which forces all images to output in JPEG or WebP format):
-
-.. code-block:: html+Django
-
-    {% image page.photo width-400 format-jpeg jpegquality-40 %}
-    {% image page.photo width-400 format-webp webpquality-50 %}
-
-Generating image renditions in Python
-^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
-
-All of the image transformations mentioned above can also be used directly in Python code.
-See :ref:`image_renditions`.

+ 1 - 1
docs/topics/index.md

@@ -12,4 +12,4 @@ search/index
 snippets
 streamfield
 permissions
-```
+```

+ 18 - 14
docs/topics/pages.md

@@ -129,8 +129,8 @@ Here's a summary of the `Panel` classes that Wagtail provides out of the box. Se
 
 These allow editing of model fields. The `FieldPanel` class will choose the correct widget based on the type of the field, such as a rich text editor for `RichTextField`, or an image chooser for a `ForeignKey` to an image model. `FieldPanel` also provides a page chooser interface for `ForeignKey`s to page models, but for more fine-grained control over which page types can be chosen, `PageChooserPanel` provides additional configuration options.
 
-- {class}`~wagtail.admin.panels.FieldPanel`
-- {class}`~wagtail.admin.panels.PageChooserPanel`
+-   {class}`~wagtail.admin.panels.FieldPanel`
+-   {class}`~wagtail.admin.panels.PageChooserPanel`
 
 ```{versionchanged} 3.0
 Previously, certain field types required special-purpose panels: `StreamFieldPanel`, `ImageChooserPanel`, `DocumentChooserPanel` and `SnippetChooserPanel`. These are now all handled by `FieldPanel`.
@@ -140,16 +140,16 @@ Previously, certain field types required special-purpose panels: `StreamFieldPan
 
 These are used for structuring fields in the interface.
 
-- {class}`~wagtail.admin.panels.MultiFieldPanel`
-- {class}`~wagtail.admin.panels.InlinePanel`
-- {class}`~wagtail.admin.panels.FieldRowPanel`
-
+-   {class}`~wagtail.admin.panels.MultiFieldPanel`
+-   {class}`~wagtail.admin.panels.InlinePanel`
+-   {class}`~wagtail.admin.panels.FieldRowPanel`
 
 #### Customising the page editor interface
 
 The page editor can be customised further. See [Customising the editing interface](/advanced_topics/customisation/page_editing_interface).
 
 (page_type_business_rules)=
+
 ### Parent page / subpage type rules
 
 These two attributes allow you to control where page types may be used in your site. It allows you to define rules like "blog entries may only be created under a blog index".
@@ -164,9 +164,10 @@ By default, any page type can be created under any page type and it is not neces
 Setting `parent_page_types` to an empty list is a good way of preventing a particular page type from being created in the editor interface.
 
 (page_descriptions)=
+
 ### Page descriptions
 
-With every Wagtail Page you are able to add a helpful description text, similar to a ``help_text`` model attribute. By adding ``page_description`` to your Page model you'll be adding a short description that can be seen when you create a new page, edit an existing page or when you're prompted to select a child page type.
+With every Wagtail Page you are able to add a helpful description text, similar to a `help_text` model attribute. By adding `page_description` to your Page model you'll be adding a short description that can be seen when you create a new page, edit an existing page or when you're prompted to select a child page type.
 
 ```python
 class LandingPage(Page):
@@ -175,6 +176,7 @@ class LandingPage(Page):
 ```
 
 (page_urls)=
+
 ### Page URLs
 
 The most common method of retrieving page URLs is by using the `{% pageurl %}` template tag. Since it's called from a template, `pageurl` automatically includes the optimizations mentioned below. For more information, see [pageurl](pageurl_tag).
@@ -335,18 +337,19 @@ Wagtail can nest the content of other models within the page. This is useful for
 
 Each inline model requires the following:
 
-- It must inherit from {class}`wagtail.models.Orderable`
-- It must have a `ParentalKey` to the parent model
+-   It must inherit from {class}`wagtail.models.Orderable`
+-   It must have a `ParentalKey` to the parent model
 
-```{note} django-modelcluster and ParentalKey
+````{note} django-modelcluster and ParentalKey
 The model inlining feature is provided by [django-modelcluster](https://github.com/torchbox/django-modelcluster) and the `ParentalKey` field type must be imported from there:
 
 ```python
     from modelcluster.fields import ParentalKey
-```
+````
 
 `ParentalKey` is a subclass of Django's `ForeignKey`, and takes the same arguments.
-```
+
+````
 
 For example, the following inline model can be used to add related links (a list of name, url pairs) to the `BlogPage` model:
 
@@ -365,7 +368,7 @@ class BlogPageRelatedLink(Orderable):
         FieldPanel('name'),
         FieldPanel('url'),
     ]
-```
+````
 
 To add this to the admin interface, use the {class}`~wagtail.admin.panels.InlinePanel` edit panel class:
 
@@ -435,7 +438,7 @@ When users are given a choice of pages to create, the list of page types is gene
 
 ### Page QuerySet ordering
 
-`Page`-derived models *cannot* be given a default ordering by using the standard Django approach of adding an `ordering` attribute to the internal `Meta` class.
+`Page`-derived models _cannot_ be given a default ordering by using the standard Django approach of adding an `ordering` attribute to the internal `Meta` class.
 
 ```python
 class NewsItemPage(Page):
@@ -453,6 +456,7 @@ news_items = NewsItemPage.objects.live().order_by('-publication_date')
 ```
 
 (custom_page_managers)=
+
 ### Custom Page managers
 
 You can add a custom `Manager` to your `Page` class. Any custom Managers should inherit from `wagtail.models.PageManager`:

+ 60 - 0
docs/topics/permissions.md

@@ -0,0 +1,60 @@
+(permissions)=
+
+# Permissions
+
+Wagtail adapts and extends [the Django permission system](https://docs.djangoproject.com/en/stable/topics/auth/default/#topic-authorization) to cater for the needs of website content creation, such as moderation workflows, and multiple teams working on different areas of a site (or multiple sites within the same Wagtail installation). Permissions can be configured through the 'Groups' area of the Wagtail admin interface, under 'Settings'.
+
+## Page permissions
+
+Permissions can be attached at any point in the page tree, and propagate down the tree. For example, if a site had the page tree::
+
+```
+MegaCorp/
+    About us
+    Offices/
+        UK
+        France
+        Germany
+```
+
+then a group with 'edit' permissions on the 'Offices' page would automatically receive the ability to edit the 'UK', 'France' and 'Germany' pages. Permissions can be set globally for the entire tree by assigning them on the 'root' page - since all pages must exist underneath the root node, and the root cannot be deleted, this permission will cover all pages that exist now and in future.
+
+Whenever a user creates a page through the Wagtail admin, that user is designated as the owner of that page. Any user with 'add' permission has the ability to edit pages they own, as well as adding new ones. This is in recognition of the fact that creating pages is typically an iterative process involving creating a number of draft versions - giving a user the ability to create a draft but not letting them subsequently edit it would not be very useful. Ability to edit a page also implies the ability to delete it; unlike Django's standard permission model, there is no distinct 'delete' permission.
+
+The full set of available permission types is as follows:
+
+-   **Add** - grants the ability to create new subpages underneath this page (provided the page model permits this - see [](page_type_business_rules)), and to edit and delete pages owned by the current user. Published pages cannot be deleted unless the user also has 'publish' permission.
+-   **Edit** - grants the ability to edit and delete this page, and any pages underneath it, regardless of ownership. A user with only 'edit' permission may not create new pages, only edit existing ones. Published pages cannot be deleted unless the user also has 'publish' permission.
+-   **Publish** - grants the ability to publish and unpublish this page and/or its children. A user without publish permission cannot directly make changes that are visible to visitors of the website; instead, they must submit their changes for moderation. Publish permission is independent of edit permission; a user with only publish permission will not be able to make any edits of their own.
+-   **Bulk delete** - allows a user to delete pages that have descendants, in a single operation. Without this permission, a user has to delete the descendant pages individually before deleting the parent. This is a safeguard against accidental deletion. This permission must be used in conjunction with 'add' / 'edit' permission, as it does not provide any deletion rights of its own; it only provides a 'shortcut' for the permissions the user has already. For example, a user with just 'add' and 'bulk delete' permissions will only be able to bulk-delete if all the affected pages are owned by that user, and are unpublished.
+-   **Lock** - grants the ability to lock or unlock this page (and any pages underneath it) for editing, preventing users from making any further edits to it.
+
+Drafts can be viewed only if the user has either Edit or Publish permission.
+
+(image_document_permissions)=
+
+## Image / document permissions
+
+The permission rules for images and documents work on a similar basis to pages. Images and documents are considered to be 'owned' by the user who uploaded them; a user with 'add' permission also has the ability to edit items they own; and deletion is considered equivalent to editing rather than having a specific permission type.
+
+Access to specific sets of images and documents can be controlled by setting up _collections_. By default all images and documents belong to the 'root' collection, but users with appropriate permissions can create new collections the Settings -> Collections area of the admin interface. Permissions set on 'root' apply to all collections, so a user with 'edit' permission for images in the root collection can edit all images; permissions set on other collections only apply to that collection and any of its sub-collections.
+
+The 'choose' permission for images and documents determines which collections are visible within the chooser interface used to select images and document links for insertion into pages (and other models, such as snippets). Typically, all users are granted choose permission for all collections, allowing them to use any uploaded image or document on pages they create, but this permission can be limited to allow creating collections that are only visible to specific groups.
+
+(collection_management_permissions)=
+
+## Collection management permissions
+
+Permission for managing collections themselves can be attached at any point in the collection tree. The available collection management permissions are as follows:
+
+-   **Add** - grants the ability to create new collections underneath this collection.
+-   **Edit** - grants the ability to edit the name of the collection, change its location in the collection tree, and to change the privacy settings for documents within this collection.
+-   **Delete** - grants the ability to delete collections that were added below this collection. _Note:_ a collection must have no subcollections under it and the collection itself must be empty before it can be deleted.
+
+```{note}
+Users are not allowed to move or delete the collection that is used to assign them permission to manage collections.
+```
+
+## Displaying custom permissions in the admin
+
+Most permissions will automatically show up in the wagtail admin Group edit form, however, you can also add them using the `register_permissions` hook (see [](register_permissions)).

+ 0 - 65
docs/topics/permissions.rst

@@ -1,65 +0,0 @@
-.. _permissions:
-
-===========
-Permissions
-===========
-
-Wagtail adapts and extends :ref:`the Django permission system <django:topic-authorization>` to cater for the needs of website content creation, such as moderation workflows, and multiple teams working on different areas of a site (or multiple sites within the same Wagtail installation). Permissions can be configured through the 'Groups' area of the Wagtail admin interface, under 'Settings'.
-
-
-Page permissions
-----------------
-
-Permissions can be attached at any point in the page tree, and propagate down the tree. For example, if a site had the page tree::
-
-    MegaCorp/
-        About us
-        Offices/
-            UK
-            France
-            Germany
-
-then a group with 'edit' permissions on the 'Offices' page would automatically receive the ability to edit the 'UK', 'France' and 'Germany' pages. Permissions can be set globally for the entire tree by assigning them on the 'root' page - since all pages must exist underneath the root node, and the root cannot be deleted, this permission will cover all pages that exist now and in future.
-
-Whenever a user creates a page through the Wagtail admin, that user is designated as the owner of that page. Any user with 'add' permission has the ability to edit pages they own, as well as adding new ones. This is in recognition of the fact that creating pages is typically an iterative process involving creating a number of draft versions - giving a user the ability to create a draft but not letting them subsequently edit it would not be very useful. Ability to edit a page also implies the ability to delete it; unlike Django's standard permission model, there is no distinct 'delete' permission.
-
-The full set of available permission types is as follows:
-
-* **Add** - grants the ability to create new subpages underneath this page (provided the page model permits this - see :ref:`Parent page / subpage type rules<page_type_business_rules>`), and to edit and delete pages owned by the current user. Published pages cannot be deleted unless the user also has 'publish' permission.
-* **Edit** - grants the ability to edit and delete this page, and any pages underneath it, regardless of ownership. A user with only 'edit' permission may not create new pages, only edit existing ones. Published pages cannot be deleted unless the user also has 'publish' permission.
-* **Publish** - grants the ability to publish and unpublish this page and/or its children. A user without publish permission cannot directly make changes that are visible to visitors of the website; instead, they must submit their changes for moderation. Publish permission is independent of edit permission; a user with only publish permission will not be able to make any edits of their own.
-* **Bulk delete** - allows a user to delete pages that have descendants, in a single operation. Without this permission, a user has to delete the descendant pages individually before deleting the parent. This is a safeguard against accidental deletion. This permission must be used in conjunction with 'add' / 'edit' permission, as it does not provide any deletion rights of its own; it only provides a 'shortcut' for the permissions the user has already. For example, a user with just 'add' and 'bulk delete' permissions will only be able to bulk-delete if all the affected pages are owned by that user, and are unpublished.
-* **Lock** - grants the ability to lock or unlock this page (and any pages underneath it) for editing, preventing users from making any further edits to it.
-
-Drafts can be viewed only if the user has either Edit or Publish permission.
-
-
-.. _image_document_permissions:
-
-Image / document permissions
-----------------------------
-
-The permission rules for images and documents work on a similar basis to pages. Images and documents are considered to be 'owned' by the user who uploaded them; a user with 'add' permission also has the ability to edit items they own; and deletion is considered equivalent to editing rather than having a specific permission type.
-
-Access to specific sets of images and documents can be controlled by setting up *collections*. By default all images and documents belong to the 'root' collection, but users with appropriate permissions can create new collections the Settings -> Collections area of the admin interface. Permissions set on 'root' apply to all collections, so a user with 'edit' permission for images in the root collection can edit all images; permissions set on other collections only apply to that collection and any of its sub-collections.
-
-The 'choose' permission for images and documents determines which collections are visible within the chooser interface used to select images and document links for insertion into pages (and other models, such as snippets). Typically, all users are granted choose permission for all collections, allowing them to use any uploaded image or document on pages they create, but this permission can be limited to allow creating collections that are only visible to specific groups.
-
-.. _collection_management_permissions:
-
-Collection management permissions
----------------------------------
-
-Permission for managing collections themselves can be attached at any point in the collection tree. The available collection management permissions are as follows:
-
-* **Add** - grants the ability to create new collections underneath this collection.
-* **Edit** - grants the ability to edit the name of the collection, change its location in the collection tree, and to change the privacy settings for documents within this collection.
-* **Delete** - grants the ability to delete collections that were added below this collection. *Note:* a collection must have no subcollections under it and the collection itself must be empty before it can be deleted.
-
-.. Note::
-    Users are not allowed to move or delete the collection that is used to assign them permission to manage collections.
-
-Displaying custom permissions in the admin
-------------------------------------------
-
-Most permissions will automatically show up in the wagtail admin Group edit form, however, you can also add them using the ``register_permissions`` hook (see :ref:`register_permissions`).

+ 215 - 0
docs/topics/snippets.md

@@ -0,0 +1,215 @@
+(snippets)=
+
+# Snippets
+
+Snippets are pieces of content which do not necessitate a full webpage to render. They could be used for making secondary content, such as headers, footers, and sidebars, editable in the Wagtail admin. Snippets are Django models which do not inherit the `Page` class and are thus not organised into the Wagtail tree. However, they can still be made editable by assigning panels and identifying the model as a snippet with the `register_snippet` class decorator.
+
+Snippets lack many of the features of pages, such as being orderable in the Wagtail admin or having a defined URL. Decide carefully if the content type you would want to build into a snippet might be more suited to a page.
+
+## Snippet Models
+
+Here's an example snippet model:
+
+```python
+from django.db import models
+
+from wagtail.admin.panels import FieldPanel
+from wagtail.snippets.models import register_snippet
+
+# ...
+
+@register_snippet
+class Advert(models.Model):
+    url = models.URLField(null=True, blank=True)
+    text = models.CharField(max_length=255)
+
+    panels = [
+        FieldPanel('url'),
+        FieldPanel('text'),
+    ]
+
+    def __str__(self):
+        return self.text
+```
+
+The `Advert` model uses the basic Django model class and defines two properties: text and URL. The editing interface is very close to that provided for `Page`-derived models, with fields assigned in the `panels` property. Snippets do not use multiple tabs of fields, nor do they provide the "save as draft" or "submit for moderation" features.
+
+`@register_snippet` tells Wagtail to treat the model as a snippet. The `panels` list defines the fields to show on the snippet editing page. It's also important to provide a string representation of the class through `def __str__(self):` so that the snippet objects make sense when listed in the Wagtail admin.
+
+## Including Snippets in Template Tags
+
+The simplest way to make your snippets available to templates is with a template tag. This is mostly done with vanilla Django, so perhaps reviewing Django's documentation for :doc:`django custom template tags <howto/custom-template-tags>` will be more helpful. We'll go over the basics, though, and point out any considerations to make for Wagtail.
+
+First, add a new python file to a `templatetags` folder within your app - for example, `myproject/demo/templatetags/demo_tags.py`. We'll need to load some Django modules and our app's models, and ready the `register` decorator:
+
+```python
+from django import template
+from demo.models import Advert
+
+register = template.Library()
+
+# ...
+
+# Advert snippets
+@register.inclusion_tag('demo/tags/adverts.html', takes_context=True)
+def adverts(context):
+    return {
+        'adverts': Advert.objects.all(),
+        'request': context['request'],
+    }
+```
+
+`@register.inclusion_tag()` takes two variables: a template and a boolean on whether that template should be passed a request context. It's a good idea to include request contexts in your custom template tags, since some Wagtail-specific template tags like `pageurl` need the context to work properly. The template tag function could take arguments and filter the adverts to return a specific instance of the model, but for brevity we'll just use `Advert.objects.all()`.
+
+Here's what's in the template used by this template tag:
+
+```html+django
+{% for advert in adverts %}
+    <p>
+        <a href="{{ advert.url }}">
+            {{ advert.text }}
+        </a>
+    </p>
+{% endfor %}
+```
+
+Then, in your own page templates, you can include your snippet template tag with:
+
+```html+django
+{% load wagtailcore_tags demo_tags %}
+
+...
+
+{% block content %}
+
+    ...
+
+    {% adverts %}
+
+{% endblock %}
+```
+
+## Binding Pages to Snippets
+
+In the above example, the list of adverts is a fixed list that is displayed via the custom template tag independent of any other content on the page. This might be what you want for a common panel in a sidebar, but, in another scenario, you might wish to display just one specific instance of a snippet on a particular page. This can be accomplished by defining a foreign key to the snippet model within your page model and adding a `FieldPanel` to the page's `content_panels` list. For example, if you wanted to display a specific advert on a `BookPage` instance:
+
+```python
+  # ...
+  class BookPage(Page):
+      advert = models.ForeignKey(
+          'demo.Advert',
+          null=True,
+          blank=True,
+          on_delete=models.SET_NULL,
+          related_name='+'
+      )
+
+      content_panels = Page.content_panels + [
+          FieldPanel('advert'),
+          # ...
+      ]
+```
+
+The snippet could then be accessed within your template as `page.advert`.
+
+To attach multiple adverts to a page, the `FieldPanel` can be placed on an inline child object of `BookPage` rather than on `BookPage` itself. Here, this child model is named `BookPageAdvertPlacement` (so called because there is one such object for each time that an advert is placed on a BookPage):
+
+```python
+from django.db import models
+
+from wagtail.models import Page, Orderable
+
+from modelcluster.fields import ParentalKey
+
+# ...
+
+class BookPageAdvertPlacement(Orderable, models.Model):
+    page = ParentalKey('demo.BookPage', on_delete=models.CASCADE, related_name='advert_placements')
+    advert = models.ForeignKey('demo.Advert', on_delete=models.CASCADE, related_name='+')
+
+    class Meta(Orderable.Meta):
+        verbose_name = "advert placement"
+        verbose_name_plural = "advert placements"
+
+    panels = [
+        FieldPanel('advert'),
+    ]
+
+    def __str__(self):
+        return self.page.title + " -> " + self.advert.text
+
+
+class BookPage(Page):
+    # ...
+
+    content_panels = Page.content_panels + [
+        InlinePanel('advert_placements', label="Adverts"),
+        # ...
+    ]
+```
+
+These child objects are now accessible through the page's `advert_placements` property, and from there we can access the linked Advert snippet as `advert`. In the template for `BookPage`, we could include the following:
+
+```html+django
+{% for advert_placement in page.advert_placements.all %}
+    <p>
+        <a href="{{ advert_placement.advert.url }}">
+            {{ advert_placement.advert.text }}
+        </a>
+    </p>
+{% endfor %}
+```
+
+(wagtailsnippets_making_snippets_searchable)=
+
+## Making Snippets Searchable
+
+If a snippet model inherits from `wagtail.search.index.Indexed`, as described in [](wagtailsearch_indexing_models), Wagtail will automatically add a search box to the chooser interface for that snippet type. For example, the `Advert` snippet could be made searchable as follows:
+
+```python
+# ...
+
+from wagtail.search import index
+
+# ...
+
+@register_snippet
+class Advert(index.Indexed, models.Model):
+    url = models.URLField(null=True, blank=True)
+    text = models.CharField(max_length=255)
+
+    panels = [
+        FieldPanel('url'),
+        FieldPanel('text'),
+    ]
+
+    search_fields = [
+        index.SearchField('text', partial_match=True),
+    ]
+```
+
+## Tagging snippets
+
+Adding tags to snippets is very similar to adding tags to pages. The only difference is that {class}`taggit.manager.TaggableManager` should be used in the place of {class}`~modelcluster.contrib.taggit.ClusterTaggableManager`.
+
+```python
+from modelcluster.fields import ParentalKey
+from modelcluster.models import ClusterableModel
+from taggit.models import TaggedItemBase
+from taggit.managers import TaggableManager
+
+class AdvertTag(TaggedItemBase):
+    content_object = ParentalKey('demo.Advert', on_delete=models.CASCADE, related_name='tagged_items')
+
+@register_snippet
+class Advert(ClusterableModel):
+    # ...
+    tags = TaggableManager(through=AdvertTag, blank=True)
+
+    panels = [
+        # ...
+        FieldPanel('tags'),
+    ]
+```
+
+The [documentation on tagging pages](tagging) has more information on how to use tags in views.

+ 0 - 229
docs/topics/snippets.rst

@@ -1,229 +0,0 @@
-
-.. _snippets:
-
-Snippets
-========
-
-Snippets are pieces of content which do not necessitate a full webpage to render. They could be used for making secondary content, such as headers, footers, and sidebars, editable in the Wagtail admin. Snippets are Django models which do not inherit the ``Page`` class and are thus not organised into the Wagtail tree. However, they can still be made editable by assigning panels and identifying the model as a snippet with the ``register_snippet`` class decorator.
-
-Snippets lack many of the features of pages, such as being orderable in the Wagtail admin or having a defined URL. Decide carefully if the content type you would want to build into a snippet might be more suited to a page.
-
-Snippet Models
---------------
-
-Here's an example snippet model:
-
-.. code-block:: python
-
-  from django.db import models
-
-  from wagtail.admin.panels import FieldPanel
-  from wagtail.snippets.models import register_snippet
-
-  ...
-
-  @register_snippet
-  class Advert(models.Model):
-      url = models.URLField(null=True, blank=True)
-      text = models.CharField(max_length=255)
-
-      panels = [
-          FieldPanel('url'),
-          FieldPanel('text'),
-      ]
-
-      def __str__(self):
-          return self.text
-
-The ``Advert`` model uses the basic Django model class and defines two properties: text and URL. The editing interface is very close to that provided for ``Page``-derived models, with fields assigned in the ``panels`` property. Snippets do not use multiple tabs of fields, nor do they provide the "save as draft" or "submit for moderation" features.
-
-``@register_snippet`` tells Wagtail to treat the model as a snippet. The ``panels`` list defines the fields to show on the snippet editing page. It's also important to provide a string representation of the class through ``def __str__(self):`` so that the snippet objects make sense when listed in the Wagtail admin.
-
-Including Snippets in Template Tags
------------------------------------
-
-The simplest way to make your snippets available to templates is with a template tag. This is mostly done with vanilla Django, so perhaps reviewing Django's documentation for :doc:`django custom template tags <howto/custom-template-tags>` will be more helpful. We'll go over the basics, though, and point out any considerations to make for Wagtail.
-
-First, add a new python file to a ``templatetags`` folder within your app - for example, ``myproject/demo/templatetags/demo_tags.py``. We'll need to load some Django modules and our app's models, and ready the ``register`` decorator:
-
-.. code-block:: python
-
-  from django import template
-  from demo.models import Advert
-
-  register = template.Library()
-
-  ...
-
-  # Advert snippets
-  @register.inclusion_tag('demo/tags/adverts.html', takes_context=True)
-  def adverts(context):
-      return {
-          'adverts': Advert.objects.all(),
-          'request': context['request'],
-      }
-
-``@register.inclusion_tag()`` takes two variables: a template and a boolean on whether that template should be passed a request context. It's a good idea to include request contexts in your custom template tags, since some Wagtail-specific template tags like ``pageurl`` need the context to work properly. The template tag function could take arguments and filter the adverts to return a specific instance of the model, but for brevity we'll just use ``Advert.objects.all()``.
-
-Here's what's in the template used by this template tag:
-
-.. code-block:: html+django
-
-  {% for advert in adverts %}
-      <p>
-          <a href="{{ advert.url }}">
-              {{ advert.text }}
-          </a>
-      </p>
-  {% endfor %}
-
-Then, in your own page templates, you can include your snippet template tag with:
-
-.. code-block:: html+django
-
-  {% load wagtailcore_tags demo_tags %}
-
-  ...
-
-  {% block content %}
-
-      ...
-
-      {% adverts %}
-
-  {% endblock %}
-
-
-Binding Pages to Snippets
--------------------------
-
-In the above example, the list of adverts is a fixed list that is displayed via the custom template tag independent of any other content on the page. This might be what you want for a common panel in a sidebar, but, in another scenario, you might wish to display just one specific instance of a snippet on a particular page. This can be accomplished by defining a foreign key to the snippet model within your page model and adding a ``FieldPanel`` to the page's ``content_panels`` list. For example, if you wanted to display a specific advert on a  ``BookPage`` instance:
-
-.. code-block:: python
-
-  # ...
-  class BookPage(Page):
-      advert = models.ForeignKey(
-          'demo.Advert',
-          null=True,
-          blank=True,
-          on_delete=models.SET_NULL,
-          related_name='+'
-      )
-
-      content_panels = Page.content_panels + [
-          FieldPanel('advert'),
-          # ...
-      ]
-
-
-The snippet could then be accessed within your template as ``page.advert``.
-
-To attach multiple adverts to a page, the ``FieldPanel`` can be placed on an inline child object of ``BookPage`` rather than on ``BookPage`` itself. Here, this child model is named ``BookPageAdvertPlacement`` (so called because there is one such object for each time that an advert is placed on a BookPage):
-
-
-.. code-block:: python
-
-  from django.db import models
-
-  from wagtail.models import Page, Orderable
-
-  from modelcluster.fields import ParentalKey
-
-  ...
-
-  class BookPageAdvertPlacement(Orderable, models.Model):
-      page = ParentalKey('demo.BookPage', on_delete=models.CASCADE, related_name='advert_placements')
-      advert = models.ForeignKey('demo.Advert', on_delete=models.CASCADE, related_name='+')
-
-      class Meta(Orderable.Meta):
-          verbose_name = "advert placement"
-          verbose_name_plural = "advert placements"
-
-      panels = [
-          FieldPanel('advert'),
-      ]
-
-      def __str__(self):
-          return self.page.title + " -> " + self.advert.text
-
-
-  class BookPage(Page):
-      ...
-
-      content_panels = Page.content_panels + [
-          InlinePanel('advert_placements', label="Adverts"),
-          # ...
-      ]
-
-
-
-These child objects are now accessible through the page's ``advert_placements`` property, and from there we can access the linked Advert snippet as ``advert``. In the template for ``BookPage``, we could include the following:
-
-.. code-block:: html+django
-
-  {% for advert_placement in page.advert_placements.all %}
-      <p>
-          <a href="{{ advert_placement.advert.url }}">
-              {{ advert_placement.advert.text }}
-          </a>
-      </p>
-  {% endfor %}
-
-
-.. _wagtailsnippets_making_snippets_searchable:
-
-Making Snippets Searchable
---------------------------
-
-If a snippet model inherits from ``wagtail.search.index.Indexed``, as described in :ref:`wagtailsearch_indexing_models`, Wagtail will automatically add a search box to the chooser interface for that snippet type. For example, the ``Advert`` snippet could be made searchable as follows:
-
-.. code-block:: python
-
-  ...
-
-  from wagtail.search import index
-
-  ...
-
-  @register_snippet
-  class Advert(index.Indexed, models.Model):
-      url = models.URLField(null=True, blank=True)
-      text = models.CharField(max_length=255)
-
-      panels = [
-          FieldPanel('url'),
-          FieldPanel('text'),
-      ]
-
-      search_fields = [
-          index.SearchField('text', partial_match=True),
-      ]
-
-
-Tagging snippets
-----------------
-
-Adding tags to snippets is very similar to adding tags to pages. The only difference is that :class:`taggit.manager.TaggableManager` should be used in the place of :class:`~modelcluster.contrib.taggit.ClusterTaggableManager`.
-
-.. code-block:: python
-
-    from modelcluster.fields import ParentalKey
-    from modelcluster.models import ClusterableModel
-    from taggit.models import TaggedItemBase
-    from taggit.managers import TaggableManager
-
-    class AdvertTag(TaggedItemBase):
-        content_object = ParentalKey('demo.Advert', on_delete=models.CASCADE, related_name='tagged_items')
-
-    @register_snippet
-    class Advert(ClusterableModel):
-        ...
-        tags = TaggableManager(through=AdvertTag, blank=True)
-
-        panels = [
-            ...
-            FieldPanel('tags'),
-        ]
-
-The :ref:`documentation on tagging pages <tagging>` has more information on how to use tags in views.

+ 695 - 0
docs/topics/streamfield.md

@@ -0,0 +1,695 @@
+(streamfield)=
+
+# How to use StreamField for mixed content
+
+StreamField provides a content editing model suitable for pages that do not follow a fixed structure -- such as blog posts or news stories -- where the text may be interspersed with subheadings, images, pull quotes and video. It's also suitable for more specialised content types, such as maps and charts (or, for a programming blog, code snippets). In this model, these different content types are represented as a sequence of 'blocks', which can be repeated and arranged in any order.
+
+For further background on StreamField, and why you would use it instead of a rich text field for the article body, see the blog post [Rich text fields and faster horses](https://torchbox.com/blog/rich-text-fields-and-faster-horses/).
+
+StreamField also offers a rich API to define your own block types, ranging from simple collections of sub-blocks (such as a 'person' block consisting of first name, surname and photograph) to completely custom components with their own editing interface. Within the database, the StreamField content is stored as JSON, ensuring that the full informational content of the field is preserved, rather than just an HTML representation of it.
+
+## Using StreamField
+
+`StreamField` is a model field that can be defined within your page model like any other field:
+
+```python
+from django.db import models
+
+from wagtail.models import Page
+from wagtail.fields import StreamField
+from wagtail import blocks
+from wagtail.admin.panels import FieldPanel
+from wagtail.images.blocks import ImageChooserBlock
+
+class BlogPage(Page):
+    author = models.CharField(max_length=255)
+    date = models.DateField("Post date")
+    body = StreamField([
+        ('heading', blocks.CharBlock(form_classname="full title")),
+        ('paragraph', blocks.RichTextBlock()),
+        ('image', ImageChooserBlock()),
+    ], use_json_field=True)
+
+    content_panels = Page.content_panels + [
+        FieldPanel('author'),
+        FieldPanel('date'),
+        FieldPanel('body'),
+    ]
+```
+
+In this example, the body field of `BlogPage` is defined as a `StreamField` where authors can compose content from three different block types: headings, paragraphs, and images, which can be used and repeated in any order. The block types available to authors are defined as a list of `(name, block_type)` tuples: 'name' is used to identify the block type within templates, and should follow the standard Python conventions for variable names: lower-case and underscores, no spaces.
+
+You can find the complete list of available block types in the [](streamfield_block_reference).
+
+```{note}
+   StreamField is not a direct replacement for other field types such as RichTextField. If you need to migrate an existing field to StreamField, refer to [](streamfield_migrating_richtext).
+```
+
+```{versionchanged} 3.0
+The `use_json_field=True` argument was added. This indicates that the database's native JSONField support should be used for this field, and is a temporary measure to assist in migrating StreamFields created on earlier Wagtail versions; it will become the default in a future release.
+```
+
+(streamfield_template_rendering)=
+
+## Template rendering
+
+StreamField provides an HTML representation for the stream content as a whole, as well as for each individual block. To include this HTML into your page, use the `{% include_block %}` tag:
+
+```html+django
+{% load wagtailcore_tags %}
+
+    ...
+
+{% include_block page.body %}
+```
+
+In the default rendering, each block of the stream is wrapped in a `<div class="block-my_block_name">` element (where `my_block_name` is the block name given in the StreamField definition). If you wish to provide your own HTML markup, you can instead iterate over the field's value, and invoke `{% include_block %}` on each block in turn:
+
+```html+django
+{% load wagtailcore_tags %}
+
+    ...
+
+<article>
+    {% for block in page.body %}
+        <section>{% include_block block %}</section>
+    {% endfor %}
+</article>
+```
+
+For more control over the rendering of specific block types, each block object provides `block_type` and `value` properties:
+
+```html+django
+{% load wagtailcore_tags %}
+
+    ...
+
+<article>
+    {% for block in page.body %}
+        {% if block.block_type == 'heading' %}
+            <h1>{{ block.value }}</h1>
+        {% else %}
+            <section class="block-{{ block.block_type }}">
+                {% include_block block %}
+            </section>
+        {% endif %}
+    {% endfor %}
+</article>
+```
+
+## Combining blocks
+
+In addition to using the built-in block types directly within StreamField, it's possible to construct new block types by combining sub-blocks in various ways. Examples of this could include:
+
+-   An "image with caption" block consisting of an image chooser and a text field
+-   A "related links" section, where an author can provide any number of links to other pages
+-   A slideshow block, where each slide may be an image, text or video, arranged in any order
+
+Once a new block type has been built up in this way, you can use it anywhere where a built-in block type would be used - including using it as a component for yet another block type. For example, you could define an image gallery block where each item is an "image with caption" block.
+
+### StructBlock
+
+`StructBlock` allows you to group several 'child' blocks together to be presented as a single block. The child blocks are passed to `StructBlock` as a list of `(name, block_type)` tuples:
+
+```{code-block} python
+:emphasize-lines: 2-7
+
+body = StreamField([
+    ('person', blocks.StructBlock([
+        ('first_name', blocks.CharBlock()),
+        ('surname', blocks.CharBlock()),
+        ('photo', ImageChooserBlock(required=False)),
+        ('biography', blocks.RichTextBlock()),
+    ])),
+    ('heading', blocks.CharBlock(form_classname="full title")),
+    ('paragraph', blocks.RichTextBlock()),
+    ('image', ImageChooserBlock()),
+], use_json_field=True)
+```
+
+When reading back the content of a StreamField (such as when rendering a template), the value of a StructBlock is a dict-like object with keys corresponding to the block names given in the definition:
+
+```html+django
+<article>
+    {% for block in page.body %}
+        {% if block.block_type == 'person' %}
+            <div class="person">
+                {% image block.value.photo width-400 %}
+                <h2>{{ block.value.first_name }} {{ block.value.surname }}</h2>
+                {{ block.value.biography }}
+            </div>
+        {% else %}
+            (rendering for other block types)
+        {% endif %}
+    {% endfor %}
+</article>
+```
+
+### Subclassing `StructBlock`
+
+Placing a StructBlock's list of child blocks inside a `StreamField` definition can often be hard to read, and makes it difficult for the same block to be reused in multiple places. As an alternative, `StructBlock` can be subclassed, with the child blocks defined as attributes on the subclass. The 'person' block in the above example could be rewritten as:
+
+```python
+class PersonBlock(blocks.StructBlock):
+    first_name = blocks.CharBlock()
+    surname = blocks.CharBlock()
+    photo = ImageChooserBlock(required=False)
+    biography = blocks.RichTextBlock()
+```
+
+`PersonBlock` can then be used in a `StreamField` definition in the same way as the built-in block types:
+
+```python
+body = StreamField([
+    ('person', PersonBlock()),
+    ('heading', blocks.CharBlock(form_classname="full title")),
+    ('paragraph', blocks.RichTextBlock()),
+    ('image', ImageChooserBlock()),
+], use_json_field=True)
+```
+
+### Block icons
+
+In the menu that content authors use to add new blocks to a StreamField, each block type has an associated icon. For StructBlock and other structural block types, a placeholder icon is used, since the purpose of these blocks is specific to your project. To set a custom icon, pass the option `icon` as either a keyword argument to `StructBlock`, or an attribute on a `Meta` class:
+
+```{code-block} python
+:emphasize-lines: 7
+
+body = StreamField([
+    ('person', blocks.StructBlock([
+        ('first_name', blocks.CharBlock()),
+        ('surname', blocks.CharBlock()),
+        ('photo', ImageChooserBlock(required=False)),
+        ('biography', blocks.RichTextBlock()),
+    ], icon='user')),
+    ('heading', blocks.CharBlock(form_classname="full title")),
+    ('paragraph', blocks.RichTextBlock()),
+    ('image', ImageChooserBlock()),
+], use_json_field=True)
+```
+
+```{code-block} python
+:emphasize-lines: 7-8
+
+class PersonBlock(blocks.StructBlock):
+    first_name = blocks.CharBlock()
+    surname = blocks.CharBlock()
+    photo = ImageChooserBlock(required=False)
+    biography = blocks.RichTextBlock()
+
+    class Meta:
+        icon = 'user'
+```
+
+For a list of the recognised icon identifiers, see the [](styleguide).
+
+### ListBlock
+
+`ListBlock` defines a repeating block, allowing content authors to insert as many instances of a particular block type as they like. For example, a 'gallery' block consisting of multiple images can be defined as follows:
+
+```{code-block} python
+:emphasize-lines: 2
+
+body = StreamField([
+    ('gallery', blocks.ListBlock(ImageChooserBlock())),
+    ('heading', blocks.CharBlock(form_classname="full title")),
+    ('paragraph', blocks.RichTextBlock()),
+    ('image', ImageChooserBlock()),
+], use_json_field=True)
+```
+
+When reading back the content of a StreamField (such as when rendering a template), the value of a ListBlock is a list of child values:
+
+```html+django
+<article>
+    {% for block in page.body %}
+        {% if block.block_type == 'gallery' %}
+            <ul class="gallery">
+                {% for img in block.value %}
+                    <li>{% image img width-400 %}</li>
+                {% endfor %}
+            </ul>
+        {% else %}
+            (rendering for other block types)
+        {% endif %}
+    {% endfor %}
+</article>
+```
+
+### StreamBlock
+
+`StreamBlock` defines a set of child block types that can be mixed and repeated in any sequence, via the same mechanism as StreamField itself. For example, a carousel that supports both image and video slides could be defined as follows:
+
+```{code-block} python
+:emphasize-lines: 2-5
+
+body = StreamField([
+    ('carousel', blocks.StreamBlock([
+        ('image', ImageChooserBlock()),
+        ('video', EmbedBlock()),
+    ])),
+    ('heading', blocks.CharBlock(form_classname="full title")),
+    ('paragraph', blocks.RichTextBlock()),
+    ('image', ImageChooserBlock()),
+], use_json_field=True)
+```
+
+`StreamBlock` can also be subclassed in the same way as `StructBlock`, with the child blocks being specified as attributes on the class:
+
+```python
+class CarouselBlock(blocks.StreamBlock):
+    image = ImageChooserBlock()
+    video = EmbedBlock()
+
+    class Meta:
+        icon = 'image'
+```
+
+A StreamBlock subclass defined in this way can also be passed to a `StreamField` definition, instead of passing a list of block types. This allows setting up a common set of block types to be used on multiple page types:
+
+```python
+class CommonContentBlock(blocks.StreamBlock):
+    heading = blocks.CharBlock(form_classname="full title")
+    paragraph = blocks.RichTextBlock()
+    image = ImageChooserBlock()
+
+
+class BlogPage(Page):
+    body = StreamField(CommonContentBlock(), use_json_field=True)
+```
+
+When reading back the content of a StreamField, the value of a StreamBlock is a sequence of block objects with `block_type` and `value` properties, just like the top-level value of the StreamField itself.
+
+```html+django
+<article>
+    {% for block in page.body %}
+        {% if block.block_type == 'carousel' %}
+            <ul class="carousel">
+                {% for slide in block.value %}
+                    {% if slide.block_type == 'image' %}
+                        <li class="image">{% image slide.value width-200 %}</li>
+                    {% else %}
+                        <li class="video">{% include_block slide %}</li>
+                    {% endif %}
+                {% endfor %}
+            </ul>
+        {% else %}
+            (rendering for other block types)
+        {% endif %}
+    {% endfor %}
+</article>
+```
+
+### Limiting block counts
+
+By default, a StreamField can contain an unlimited number of blocks. The `min_num` and `max_num` options on `StreamField` or `StreamBlock` allow you to set a minimum or maximum number of blocks:
+
+```python
+body = StreamField([
+    ('heading', blocks.CharBlock(form_classname="full title")),
+    ('paragraph', blocks.RichTextBlock()),
+    ('image', ImageChooserBlock()),
+], min_num=2, max_num=5, use_json_field=True)
+```
+
+Or equivalently:
+
+```python
+class CommonContentBlock(blocks.StreamBlock):
+    heading = blocks.CharBlock(form_classname="full title")
+    paragraph = blocks.RichTextBlock()
+    image = ImageChooserBlock()
+
+    class Meta:
+        min_num = 2
+        max_num = 5
+```
+
+The `block_counts` option can be used to set a minimum or maximum count for specific block types. This accepts a dict, mapping block names to a dict containing either or both of `min_num` and `max_num`. For example, to permit between 1 and 3 'heading' blocks:
+
+```python
+body = StreamField([
+    ('heading', blocks.CharBlock(form_classname="full title")),
+    ('paragraph', blocks.RichTextBlock()),
+    ('image', ImageChooserBlock()),
+], block_counts={
+    'heading': {'min_num': 1, 'max_num': 3},
+}, use_json_field=True)
+```
+
+Or equivalently:
+
+```python
+class CommonContentBlock(blocks.StreamBlock):
+    heading = blocks.CharBlock(form_classname="full title")
+    paragraph = blocks.RichTextBlock()
+    image = ImageChooserBlock()
+
+    class Meta:
+        block_counts = {
+            'heading': {'min_num': 1, 'max_num': 3},
+        }
+```
+
+(streamfield_per_block_templates)=
+
+## Per-block templates
+
+By default, each block is rendered using simple, minimal HTML markup, or no markup at all. For example, a CharBlock value is rendered as plain text, while a ListBlock outputs its child blocks in a `<ul>` wrapper. To override this with your own custom HTML rendering, you can pass a `template` argument to the block, giving the filename of a template file to be rendered. This is particularly useful for custom block types derived from StructBlock:
+
+```python
+('person', blocks.StructBlock(
+    [
+        ('first_name', blocks.CharBlock()),
+        ('surname', blocks.CharBlock()),
+        ('photo', ImageChooserBlock(required=False)),
+        ('biography', blocks.RichTextBlock()),
+    ],
+    template='myapp/blocks/person.html',
+    icon='user'
+))
+```
+
+Or, when defined as a subclass of StructBlock:
+
+```python
+class PersonBlock(blocks.StructBlock):
+    first_name = blocks.CharBlock()
+    surname = blocks.CharBlock()
+    photo = ImageChooserBlock(required=False)
+    biography = blocks.RichTextBlock()
+
+    class Meta:
+        template = 'myapp/blocks/person.html'
+        icon = 'user'
+```
+
+Within the template, the block value is accessible as the variable `value`:
+
+```html+django
+{% load wagtailimages_tags %}
+
+<div class="person">
+    {% image value.photo width-400 %}
+    <h2>{{ value.first_name }} {{ value.surname }}</h2>
+    {{ value.biography }}
+</div>
+```
+
+Since `first_name`, `surname`, `photo` and `biography` are defined as blocks in their own right, this could also be written as:
+
+```html+django
+{% load wagtailcore_tags wagtailimages_tags %}
+
+<div class="person">
+    {% image value.photo width-400 %}
+    <h2>{% include_block value.first_name %} {% include_block value.surname %}</h2>
+    {% include_block value.biography %}
+</div>
+```
+
+Writing `{{ my_block }}` is roughly equivalent to `{% include_block my_block %}`, but the short form is more restrictive, as it does not pass variables from the calling template such as `request` or `page`; for this reason, it is recommended that you only use it for simple values that do not render HTML of their own. For example, if our PersonBlock used the template:
+
+```html+django
+{% load wagtailimages_tags %}
+
+<div class="person">
+    {% image value.photo width-400 %}
+    <h2>{{ value.first_name }} {{ value.surname }}</h2>
+
+    {% if request.user.is_authenticated %}
+        <a href="#">Contact this person</a>
+    {% endif %}
+
+    {{ value.biography }}
+</div>
+```
+
+then the `request.user.is_authenticated` test would not work correctly when rendering the block through a `{{ ... }}` tag:
+
+```html+django
+{# Incorrect: #}
+
+{% for block in page.body %}
+    {% if block.block_type == 'person' %}
+        <div>
+            {{ block }}
+        </div>
+    {% endif %}
+{% endfor %}
+
+{# Correct: #}
+
+{% for block in page.body %}
+    {% if block.block_type == 'person' %}
+        <div>
+            {% include_block block %}
+        </div>
+    {% endif %}
+{% endfor %}
+```
+
+Like Django's `{% include %}` tag, `{% include_block %}` also allows passing additional variables to the included template, through the syntax `{% include_block my_block with foo="bar" %}`:
+
+```html+django
+
+{# In page template: #}
+
+{% for block in page.body %}
+    {% if block.block_type == 'person' %}
+        {% include_block block with classname="important" %}
+    {% endif %}
+{% endfor %}
+
+{# In PersonBlock template: #}
+
+<div class="{{ classname }}">
+    ...
+</div>
+```
+
+The syntax `{% include_block my_block with foo="bar" only %}` is also supported, to specify that no variables from the parent template other than `foo` will be passed to the child template.
+
+(streamfield_get_context)=
+
+As well as passing variables from the parent template, block subclasses can pass additional template variables of their own by overriding the `get_context` method:
+
+```python
+import datetime
+
+class EventBlock(blocks.StructBlock):
+    title = blocks.CharBlock()
+    date = blocks.DateBlock()
+
+    def get_context(self, value, parent_context=None):
+        context = super().get_context(value, parent_context=parent_context)
+        context['is_happening_today'] = (value['date'] == datetime.date.today())
+        return context
+
+    class Meta:
+        template = 'myapp/blocks/event.html'
+```
+
+In this example, the variable `is_happening_today` will be made available within the block template. The `parent_context` keyword argument is available when the block is rendered through an `{% include_block %}` tag, and is a dict of variables passed from the calling template.
+
+All block types, not just `StructBlock`, support the `template` property. However, for blocks that handle basic Python data types, such as `CharBlock` and `IntegerBlock`, there are some limitations on where the template will take effect. For further details, see [](boundblocks_and_values).
+
+## Customisations
+
+All block types implement a common API for rendering their front-end and form representations, and storing and retrieving values to and from the database. By subclassing the various block classes and overriding these methods, all kinds of customisations are possible, from modifying the layout of StructBlock form fields to implementing completely new ways of combining blocks. For further details, see [](custom_streamfield_blocks).
+
+(modifying_streamfield_data)=
+
+## Modifying StreamField data
+
+A StreamField's value behaves as a list, and blocks can be inserted, overwritten and deleted before saving the instance back to the database. A new item can be written to the list as a tuple of _(block_type, value)_ - when read back, it will be returned as a `BoundBlock` object.
+
+```python
+# Replace the first block with a new block of type 'heading'
+my_page.body[0] = ('heading', "My story")
+
+# Delete the last block
+del my_page.body[-1]
+
+# Append a rich text block to the stream
+from wagtail.rich_text import RichText
+my_page.body.append(('paragraph', RichText("<p>And they all lived happily ever after.</p>")))
+
+# Save the updated data back to the database
+my_page.save()
+```
+
+(streamfield_migrating_richtext)=
+
+## Migrating RichTextFields to StreamField
+
+If you change an existing RichTextField to a StreamField, the database migration will complete with no errors, since both fields use a text column within the database. However, StreamField uses a JSON representation for its data, so the existing text requires an extra conversion step in order to become accessible again. For this to work, the StreamField needs to include a RichTextBlock as one of the available block types. Create the migration as normal using `./manage.py makemigrations`, then edit it as follows (in this example, the 'body' field of the `demo.BlogPage` model is being converted to a StreamField with a RichTextBlock named `rich_text`):
+
+```{note}
+This migration cannot be used if the StreamField has the `use_json_field` argument set to `True`. To migrate, set the `use_json_field` argument to `False` first, migrate the data, then set it back to `True`.
+```
+
+```python
+# -*- coding: utf-8 -*-
+from django.db import models, migrations
+from wagtail.rich_text import RichText
+
+
+def convert_to_streamfield(apps, schema_editor):
+    BlogPage = apps.get_model("demo", "BlogPage")
+    for page in BlogPage.objects.all():
+        if page.body.raw_text and not page.body:
+            page.body = [('rich_text', RichText(page.body.raw_text))]
+            page.save()
+
+
+def convert_to_richtext(apps, schema_editor):
+    BlogPage = apps.get_model("demo", "BlogPage")
+    for page in BlogPage.objects.all():
+        if page.body.raw_text is None:
+            raw_text = ''.join([
+                child.value.source for child in page.body
+                if child.block_type == 'rich_text'
+            ])
+            page.body = raw_text
+            page.save()
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        # leave the dependency line from the generated migration intact!
+        ('demo', '0001_initial'),
+    ]
+
+    operations = [
+        # leave the generated AlterField intact!
+        migrations.AlterField(
+            model_name='BlogPage',
+            name='body',
+            field=wagtail.fields.StreamField([('rich_text', wagtail.blocks.RichTextBlock())]),
+        ),
+
+        migrations.RunPython(
+            convert_to_streamfield,
+            convert_to_richtext,
+        ),
+    ]
+```
+
+Note that the above migration will work on published Page objects only. If you also need to migrate draft pages and page revisions, then edit the migration as in the following example instead:
+
+```python
+# -*- coding: utf-8 -*-
+import json
+
+from django.core.serializers.json import DjangoJSONEncoder
+from django.db import migrations, models
+
+from wagtail.rich_text import RichText
+
+
+def page_to_streamfield(page):
+    changed = False
+    if page.body.raw_text and not page.body:
+        page.body = [('rich_text', {'rich_text': RichText(page.body.raw_text)})]
+        changed = True
+    return page, changed
+
+
+def pagerevision_to_streamfield(revision_data):
+    changed = False
+    body = revision_data.get('body')
+    if body:
+        try:
+            json.loads(body)
+        except ValueError:
+            revision_data['body'] = json.dumps(
+                [{
+                    "value": {"rich_text": body},
+                    "type": "rich_text"
+                }],
+                cls=DjangoJSONEncoder)
+            changed = True
+        else:
+            # It's already valid JSON. Leave it.
+            pass
+    return revision_data, changed
+
+
+def page_to_richtext(page):
+    changed = False
+    if page.body.raw_text is None:
+        raw_text = ''.join([
+            child.value['rich_text'].source for child in page.body
+            if child.block_type == 'rich_text'
+        ])
+        page.body = raw_text
+        changed = True
+    return page, changed
+
+
+def pagerevision_to_richtext(revision_data):
+    changed = False
+    body = revision_data.get('body', 'definitely non-JSON string')
+    if body:
+        try:
+            body_data = json.loads(body)
+        except ValueError:
+            # It's not apparently a StreamField. Leave it.
+            pass
+        else:
+            raw_text = ''.join([
+                child['value']['rich_text'] for child in body_data
+                if child['type'] == 'rich_text'
+            ])
+            revision_data['body'] = raw_text
+            changed = True
+    return revision_data, changed
+
+
+def convert(apps, schema_editor, page_converter, pagerevision_converter):
+    BlogPage = apps.get_model("demo", "BlogPage")
+    for page in BlogPage.objects.all():
+
+        page, changed = page_converter(page)
+        if changed:
+            page.save()
+
+        for revision in page.revisions.all():
+            revision_data = revision.content
+            revision_data, changed = pagerevision_converter(revision_data)
+            if changed:
+                revision.content = revision_data
+                revision.save()
+
+
+def convert_to_streamfield(apps, schema_editor):
+    return convert(apps, schema_editor, page_to_streamfield, pagerevision_to_streamfield)
+
+
+def convert_to_richtext(apps, schema_editor):
+    return convert(apps, schema_editor, page_to_richtext, pagerevision_to_richtext)
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        # leave the dependency line from the generated migration intact!
+        ('demo', '0001_initial'),
+    ]
+
+    operations = [
+        # leave the generated AlterField intact!
+        migrations.AlterField(
+            model_name='BlogPage',
+            name='body',
+            field=wagtail.fields.StreamField([('rich_text', wagtail.blocks.RichTextBlock())]),
+        ),
+
+        migrations.RunPython(
+            convert_to_streamfield,
+            convert_to_richtext,
+        ),
+    ]
+```

+ 0 - 721
docs/topics/streamfield.rst

@@ -1,721 +0,0 @@
-.. _streamfield:
-
-How to use StreamField for mixed content
-========================================
-
-StreamField provides a content editing model suitable for pages that do not follow a fixed structure -- such as blog posts or news stories -- where the text may be interspersed with subheadings, images, pull quotes and video. It's also suitable for more specialised content types, such as maps and charts (or, for a programming blog, code snippets). In this model, these different content types are represented as a sequence of 'blocks', which can be repeated and arranged in any order.
-
-For further background on StreamField, and why you would use it instead of a rich text field for the article body, see the blog post `Rich text fields and faster horses <https://torchbox.com/blog/rich-text-fields-and-faster-horses/>`__.
-
-StreamField also offers a rich API to define your own block types, ranging from simple collections of sub-blocks (such as a 'person' block consisting of first name, surname and photograph) to completely custom components with their own editing interface. Within the database, the StreamField content is stored as JSON, ensuring that the full informational content of the field is preserved, rather than just an HTML representation of it.
-
-
-Using StreamField
------------------
-
-``StreamField`` is a model field that can be defined within your page model like any other field:
-
-.. code-block:: python
-
-    from django.db import models
-
-    from wagtail.models import Page
-    from wagtail.fields import StreamField
-    from wagtail import blocks
-    from wagtail.admin.panels import FieldPanel
-    from wagtail.images.blocks import ImageChooserBlock
-
-    class BlogPage(Page):
-        author = models.CharField(max_length=255)
-        date = models.DateField("Post date")
-        body = StreamField([
-            ('heading', blocks.CharBlock(form_classname="full title")),
-            ('paragraph', blocks.RichTextBlock()),
-            ('image', ImageChooserBlock()),
-        ], use_json_field=True)
-
-        content_panels = Page.content_panels + [
-            FieldPanel('author'),
-            FieldPanel('date'),
-            FieldPanel('body'),
-        ]
-
-In this example, the body field of ``BlogPage`` is defined as a ``StreamField`` where authors can compose content from three different block types: headings, paragraphs, and images, which can be used and repeated in any order. The block types available to authors are defined as a list of ``(name, block_type)`` tuples: 'name' is used to identify the block type within templates, and should follow the standard Python conventions for variable names: lower-case and underscores, no spaces.
-
-You can find the complete list of available block types in the :ref:`StreamField block reference <streamfield_block_reference>`.
-
-.. note::
-   StreamField is not a direct replacement for other field types such as RichTextField. If you need to migrate an existing field to StreamField, refer to :ref:`streamfield_migrating_richtext`.
-
-.. versionchanged:: 3.0
-
-  The ``use_json_field=True`` argument was added. This indicates that the database's native JSONField support should be used for this field, and is a temporary measure to assist in migrating StreamFields created on earlier Wagtail versions; it will become the default in a future release.
-
-
-.. _streamfield_template_rendering:
-
-Template rendering
-------------------
-
-StreamField provides an HTML representation for the stream content as a whole, as well as for each individual block. To include this HTML into your page, use the ``{% include_block %}`` tag:
-
-.. code-block:: html+django
-
-    {% load wagtailcore_tags %}
-
-     ...
-
-    {% include_block page.body %}
-
-
-In the default rendering, each block of the stream is wrapped in a ``<div class="block-my_block_name">`` element (where ``my_block_name`` is the block name given in the StreamField definition). If you wish to provide your own HTML markup, you can instead iterate over the field's value, and invoke ``{% include_block %}`` on each block in turn:
-
-.. code-block:: html+django
-
-    {% load wagtailcore_tags %}
-
-     ...
-
-    <article>
-        {% for block in page.body %}
-            <section>{% include_block block %}</section>
-        {% endfor %}
-    </article>
-
-
-For more control over the rendering of specific block types, each block object provides ``block_type`` and ``value`` properties:
-
-.. code-block:: html+django
-
-    {% load wagtailcore_tags %}
-
-     ...
-
-    <article>
-        {% for block in page.body %}
-            {% if block.block_type == 'heading' %}
-                <h1>{{ block.value }}</h1>
-            {% else %}
-                <section class="block-{{ block.block_type }}">
-                    {% include_block block %}
-                </section>
-            {% endif %}
-        {% endfor %}
-    </article>
-
-
-Combining blocks
-----------------
-
-In addition to using the built-in block types directly within StreamField, it's possible to construct new block types by combining sub-blocks in various ways. Examples of this could include:
-
-* An "image with caption" block consisting of an image chooser and a text field
-* A "related links" section, where an author can provide any number of links to other pages
-* A slideshow block, where each slide may be an image, text or video, arranged in any order
-
-Once a new block type has been built up in this way, you can use it anywhere where a built-in block type would be used - including using it as a component for yet another block type. For example, you could define an image gallery block where each item is an "image with caption" block.
-
-StructBlock
-~~~~~~~~~~~
-
-``StructBlock`` allows you to group several 'child' blocks together to be presented as a single block. The child blocks are passed to ``StructBlock`` as a list of ``(name, block_type)`` tuples:
-
-.. code-block:: python
-   :emphasize-lines: 2-7
-
-    body = StreamField([
-        ('person', blocks.StructBlock([
-            ('first_name', blocks.CharBlock()),
-            ('surname', blocks.CharBlock()),
-            ('photo', ImageChooserBlock(required=False)),
-            ('biography', blocks.RichTextBlock()),
-        ])),
-        ('heading', blocks.CharBlock(form_classname="full title")),
-        ('paragraph', blocks.RichTextBlock()),
-        ('image', ImageChooserBlock()),
-    ], use_json_field=True)
-
-When reading back the content of a StreamField (such as when rendering a template), the value of a StructBlock is a dict-like object with keys corresponding to the block names given in the definition:
-
-.. code-block:: html+django
-
-    <article>
-        {% for block in page.body %}
-            {% if block.block_type == 'person' %}
-                <div class="person">
-                    {% image block.value.photo width-400 %}
-                    <h2>{{ block.value.first_name }} {{ block.value.surname }}</h2>
-                    {{ block.value.biography }}
-                </div>
-            {% else %}
-                (rendering for other block types)
-            {% endif %}
-        {% endfor %}
-    </article>
-
-
-Subclassing ``StructBlock``
-~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-Placing a StructBlock's list of child blocks inside a ``StreamField`` definition can often be hard to read, and makes it difficult for the same block to be reused in multiple places. As an alternative, ``StructBlock`` can be subclassed, with the child blocks defined as attributes on the subclass. The 'person' block in the above example could be rewritten as:
-
-.. code-block:: python
-
-    class PersonBlock(blocks.StructBlock):
-        first_name = blocks.CharBlock()
-        surname = blocks.CharBlock()
-        photo = ImageChooserBlock(required=False)
-        biography = blocks.RichTextBlock()
-
-``PersonBlock`` can then be used in a ``StreamField`` definition in the same way as the built-in block types:
-
-.. code-block:: python
-
-    body = StreamField([
-        ('person', PersonBlock()),
-        ('heading', blocks.CharBlock(form_classname="full title")),
-        ('paragraph', blocks.RichTextBlock()),
-        ('image', ImageChooserBlock()),
-    ], use_json_field=True)
-
-
-Block icons
-~~~~~~~~~~~
-
-In the menu that content authors use to add new blocks to a StreamField, each block type has an associated icon. For StructBlock and other structural block types, a placeholder icon is used, since the purpose of these blocks is specific to your project. To set a custom icon, pass the option ``icon`` as either a keyword argument to ``StructBlock``, or an attribute on a ``Meta`` class:
-
-.. code-block:: python
-   :emphasize-lines: 7
-
-    body = StreamField([
-        ('person', blocks.StructBlock([
-            ('first_name', blocks.CharBlock()),
-            ('surname', blocks.CharBlock()),
-            ('photo', ImageChooserBlock(required=False)),
-            ('biography', blocks.RichTextBlock()),
-        ], icon='user')),
-        ('heading', blocks.CharBlock(form_classname="full title")),
-        ('paragraph', blocks.RichTextBlock()),
-        ('image', ImageChooserBlock()),
-    ], use_json_field=True)
-
-.. code-block:: python
-   :emphasize-lines: 7-8
-
-    class PersonBlock(blocks.StructBlock):
-        first_name = blocks.CharBlock()
-        surname = blocks.CharBlock()
-        photo = ImageChooserBlock(required=False)
-        biography = blocks.RichTextBlock()
-
-        class Meta:
-            icon = 'user'
-
-For a list of the recognised icon identifiers, see the :ref:`styleguide`.
-
-
-ListBlock
-~~~~~~~~~
-
-``ListBlock`` defines a repeating block, allowing content authors to insert as many instances of a particular block type as they like. For example, a 'gallery' block consisting of multiple images can be defined as follows:
-
-.. code-block:: python
-   :emphasize-lines: 2
-
-    body = StreamField([
-        ('gallery', blocks.ListBlock(ImageChooserBlock())),
-        ('heading', blocks.CharBlock(form_classname="full title")),
-        ('paragraph', blocks.RichTextBlock()),
-        ('image', ImageChooserBlock()),
-    ], use_json_field=True)
-
-When reading back the content of a StreamField (such as when rendering a template), the value of a ListBlock is a list of child values:
-
-.. code-block:: html+django
-
-    <article>
-        {% for block in page.body %}
-            {% if block.block_type == 'gallery' %}
-                <ul class="gallery">
-                    {% for img in block.value %}
-                        <li>{% image img width-400 %}</li>
-                    {% endfor %}
-                </ul>
-            {% else %}
-                (rendering for other block types)
-            {% endif %}
-        {% endfor %}
-    </article>
-
-
-StreamBlock
-~~~~~~~~~~~
-
-``StreamBlock`` defines a set of child block types that can be mixed and repeated in any sequence, via the same mechanism as StreamField itself. For example, a carousel that supports both image and video slides could be defined as follows:
-
-.. code-block:: python
-   :emphasize-lines: 2-5
-
-    body = StreamField([
-        ('carousel', blocks.StreamBlock([
-            ('image', ImageChooserBlock()),
-            ('video', EmbedBlock()),
-        ])),
-        ('heading', blocks.CharBlock(form_classname="full title")),
-        ('paragraph', blocks.RichTextBlock()),
-        ('image', ImageChooserBlock()),
-    ], use_json_field=True)
-
-``StreamBlock`` can also be subclassed in the same way as ``StructBlock``, with the child blocks being specified as attributes on the class:
-
-.. code-block:: python
-
-    class CarouselBlock(blocks.StreamBlock):
-        image = ImageChooserBlock()
-        video = EmbedBlock()
-
-        class Meta:
-            icon = 'image'
-
-A StreamBlock subclass defined in this way can also be passed to a ``StreamField`` definition, instead of passing a list of block types. This allows setting up a common set of block types to be used on multiple page types:
-
-.. code-block:: python
-
-    class CommonContentBlock(blocks.StreamBlock):
-        heading = blocks.CharBlock(form_classname="full title")
-        paragraph = blocks.RichTextBlock()
-        image = ImageChooserBlock()
-
-
-    class BlogPage(Page):
-        body = StreamField(CommonContentBlock(), use_json_field=True)
-
-
-When reading back the content of a StreamField, the value of a StreamBlock is a sequence of block objects with ``block_type`` and ``value`` properties, just like the top-level value of the StreamField itself.
-
-.. code-block:: html+django
-
-    <article>
-        {% for block in page.body %}
-            {% if block.block_type == 'carousel' %}
-                <ul class="carousel">
-                    {% for slide in block.value %}
-                        {% if slide.block_type == 'image' %}
-                            <li class="image">{% image slide.value width-200 %}</li>
-                        {% else %}
-                            <li class="video">{% include_block slide %}</li>
-                        {% endif %}
-                    {% endfor %}
-                </ul>
-            {% else %}
-                (rendering for other block types)
-            {% endif %}
-        {% endfor %}
-    </article>
-
-
-Limiting block counts
-~~~~~~~~~~~~~~~~~~~~~
-
-By default, a StreamField can contain an unlimited number of blocks. The ``min_num`` and ``max_num`` options on ``StreamField`` or ``StreamBlock`` allow you to set a minimum or maximum number of blocks:
-
-.. code-block:: python
-
-    body = StreamField([
-        ('heading', blocks.CharBlock(form_classname="full title")),
-        ('paragraph', blocks.RichTextBlock()),
-        ('image', ImageChooserBlock()),
-    ], min_num=2, max_num=5, use_json_field=True)
-
-Or equivalently:
-
-.. code-block:: python
-
-    class CommonContentBlock(blocks.StreamBlock):
-        heading = blocks.CharBlock(form_classname="full title")
-        paragraph = blocks.RichTextBlock()
-        image = ImageChooserBlock()
-
-        class Meta:
-            min_num = 2
-            max_num = 5
-
-
-The ``block_counts`` option can be used to set a minimum or maximum count for specific block types. This accepts a dict, mapping block names to a dict containing either or both of ``min_num`` and ``max_num``. For example, to permit between 1 and 3 'heading' blocks:
-
-.. code-block:: python
-
-    body = StreamField([
-        ('heading', blocks.CharBlock(form_classname="full title")),
-        ('paragraph', blocks.RichTextBlock()),
-        ('image', ImageChooserBlock()),
-    ], block_counts={
-        'heading': {'min_num': 1, 'max_num': 3},
-    }, use_json_field=True)
-
-Or equivalently:
-
-.. code-block:: python
-
-    class CommonContentBlock(blocks.StreamBlock):
-        heading = blocks.CharBlock(form_classname="full title")
-        paragraph = blocks.RichTextBlock()
-        image = ImageChooserBlock()
-
-        class Meta:
-            block_counts = {
-                'heading': {'min_num': 1, 'max_num': 3},
-            }
-
-
-.. _streamfield_per_block_templates:
-
-Per-block templates
--------------------
-
-By default, each block is rendered using simple, minimal HTML markup, or no markup at all. For example, a CharBlock value is rendered as plain text, while a ListBlock outputs its child blocks in a ``<ul>`` wrapper. To override this with your own custom HTML rendering, you can pass a ``template`` argument to the block, giving the filename of a template file to be rendered. This is particularly useful for custom block types derived from StructBlock:
-
-.. code-block:: python
-
-    ('person', blocks.StructBlock(
-        [
-            ('first_name', blocks.CharBlock()),
-            ('surname', blocks.CharBlock()),
-            ('photo', ImageChooserBlock(required=False)),
-            ('biography', blocks.RichTextBlock()),
-        ],
-        template='myapp/blocks/person.html',
-        icon='user'
-    ))
-
-
-Or, when defined as a subclass of StructBlock:
-
-.. code-block:: python
-
-    class PersonBlock(blocks.StructBlock):
-        first_name = blocks.CharBlock()
-        surname = blocks.CharBlock()
-        photo = ImageChooserBlock(required=False)
-        biography = blocks.RichTextBlock()
-
-        class Meta:
-            template = 'myapp/blocks/person.html'
-            icon = 'user'
-
-
-Within the template, the block value is accessible as the variable ``value``:
-
-.. code-block:: html+django
-
-    {% load wagtailimages_tags %}
-
-    <div class="person">
-        {% image value.photo width-400 %}
-        <h2>{{ value.first_name }} {{ value.surname }}</h2>
-        {{ value.biography }}
-    </div>
-
-Since ``first_name``, ``surname``, ``photo`` and ``biography`` are defined as blocks in their own right, this could also be written as:
-
-.. code-block:: html+django
-
-    {% load wagtailcore_tags wagtailimages_tags %}
-
-    <div class="person">
-        {% image value.photo width-400 %}
-        <h2>{% include_block value.first_name %} {% include_block value.surname %}</h2>
-        {% include_block value.biography %}
-    </div>
-
-Writing ``{{ my_block }}`` is roughly equivalent to ``{% include_block my_block %}``, but the short form is more restrictive, as it does not pass variables from the calling template such as ``request`` or ``page``; for this reason, it is recommended that you only use it for simple values that do not render HTML of their own. For example, if our PersonBlock used the template:
-
-.. code-block:: html+django
-
-    {% load wagtailimages_tags %}
-
-    <div class="person">
-        {% image value.photo width-400 %}
-        <h2>{{ value.first_name }} {{ value.surname }}</h2>
-
-        {% if request.user.is_authenticated %}
-            <a href="#">Contact this person</a>
-        {% endif %}
-
-        {{ value.biography }}
-    </div>
-
-then the ``request.user.is_authenticated`` test would not work correctly when rendering the block through a ``{{ ... }}`` tag:
-
-.. code-block:: html+django
-
-    {# Incorrect: #}
-
-    {% for block in page.body %}
-        {% if block.block_type == 'person' %}
-            <div>
-                {{ block }}
-            </div>
-        {% endif %}
-    {% endfor %}
-
-    {# Correct: #}
-
-    {% for block in page.body %}
-        {% if block.block_type == 'person' %}
-            <div>
-                {% include_block block %}
-            </div>
-        {% endif %}
-    {% endfor %}
-
-Like Django's ``{% include %}`` tag, ``{% include_block %}`` also allows passing additional variables to the included template, through the syntax ``{% include_block my_block with foo="bar" %}``:
-
-.. code-block:: html+django
-
-    {# In page template: #}
-
-    {% for block in page.body %}
-        {% if block.block_type == 'person' %}
-            {% include_block block with classname="important" %}
-        {% endif %}
-    {% endfor %}
-
-    {# In PersonBlock template: #}
-
-    <div class="{{ classname }}">
-        ...
-    </div>
-
-The syntax ``{% include_block my_block with foo="bar" only %}`` is also supported, to specify that no variables from the parent template other than ``foo`` will be passed to the child template.
-
-.. _streamfield_get_context:
-
-As well as passing variables from the parent template, block subclasses can pass additional template variables of their own by overriding the ``get_context`` method:
-
-.. code-block:: python
-
-    import datetime
-
-    class EventBlock(blocks.StructBlock):
-        title = blocks.CharBlock()
-        date = blocks.DateBlock()
-
-        def get_context(self, value, parent_context=None):
-            context = super().get_context(value, parent_context=parent_context)
-            context['is_happening_today'] = (value['date'] == datetime.date.today())
-            return context
-
-        class Meta:
-            template = 'myapp/blocks/event.html'
-
-
-In this example, the variable ``is_happening_today`` will be made available within the block template. The ``parent_context`` keyword argument is available when the block is rendered through an ``{% include_block %}`` tag, and is a dict of variables passed from the calling template.
-
-All block types, not just ``StructBlock``, support the ``template`` property. However, for blocks that handle basic Python data types, such as ``CharBlock`` and ``IntegerBlock``, there are some limitations on where the template will take effect. For further details, see :ref:`boundblocks_and_values`.
-
-
-Customisations
---------------
-
-All block types implement a common API for rendering their front-end and form representations, and storing and retrieving values to and from the database. By subclassing the various block classes and overriding these methods, all kinds of customisations are possible, from modifying the layout of StructBlock form fields to implementing completely new ways of combining blocks. For further details, see :ref:`custom_streamfield_blocks`.
-
-
-.. _modifying_streamfield_data:
-
-Modifying StreamField data
---------------------------
-
-A StreamField's value behaves as a list, and blocks can be inserted, overwritten and deleted before saving the instance back to the database. A new item can be written to the list as a tuple of *(block_type, value)* - when read back, it will be returned as a ``BoundBlock`` object.
-
-.. code-block:: python
-
-    # Replace the first block with a new block of type 'heading'
-    my_page.body[0] = ('heading', "My story")
-
-    # Delete the last block
-    del my_page.body[-1]
-
-    # Append a rich text block to the stream
-    from wagtail.rich_text import RichText
-    my_page.body.append(('paragraph', RichText("<p>And they all lived happily ever after.</p>")))
-
-    # Save the updated data back to the database
-    my_page.save()
-
-
-.. _streamfield_migrating_richtext:
-
-Migrating RichTextFields to StreamField
----------------------------------------
-
-If you change an existing RichTextField to a StreamField, the database migration will complete with no errors, since both fields use a text column within the database. However, StreamField uses a JSON representation for its data, so the existing text requires an extra conversion step in order to become accessible again. For this to work, the StreamField needs to include a RichTextBlock as one of the available block types. Create the migration as normal using ``./manage.py makemigrations``, then edit it as follows (in this example, the 'body' field of the ``demo.BlogPage`` model is being converted to a StreamField with a RichTextBlock named ``rich_text``):
-
-.. note::
-    This migration cannot be used if the StreamField has the ``use_json_field`` argument set to ``True``. To migrate, set the ``use_json_field`` argument to ``False`` first, migrate the data, then set it back to ``True``.
-
-.. code-block:: python
-
-    # -*- coding: utf-8 -*-
-    from django.db import models, migrations
-    from wagtail.rich_text import RichText
-
-
-    def convert_to_streamfield(apps, schema_editor):
-        BlogPage = apps.get_model("demo", "BlogPage")
-        for page in BlogPage.objects.all():
-            if page.body.raw_text and not page.body:
-                page.body = [('rich_text', RichText(page.body.raw_text))]
-                page.save()
-
-
-    def convert_to_richtext(apps, schema_editor):
-        BlogPage = apps.get_model("demo", "BlogPage")
-        for page in BlogPage.objects.all():
-            if page.body.raw_text is None:
-                raw_text = ''.join([
-                    child.value.source for child in page.body
-                    if child.block_type == 'rich_text'
-                ])
-                page.body = raw_text
-                page.save()
-
-
-    class Migration(migrations.Migration):
-
-        dependencies = [
-            # leave the dependency line from the generated migration intact!
-            ('demo', '0001_initial'),
-        ]
-
-        operations = [
-            # leave the generated AlterField intact!
-            migrations.AlterField(
-                model_name='BlogPage',
-                name='body',
-                field=wagtail.fields.StreamField([('rich_text', wagtail.blocks.RichTextBlock())]),
-            ),
-
-            migrations.RunPython(
-                convert_to_streamfield,
-                convert_to_richtext,
-            ),
-        ]
-
-
-Note that the above migration will work on published Page objects only. If you also need to migrate draft pages and page revisions, then edit the migration as in the following example instead:
-
-.. code-block:: python
-
-    # -*- coding: utf-8 -*-
-    import json
-
-    from django.core.serializers.json import DjangoJSONEncoder
-    from django.db import migrations, models
-
-    from wagtail.rich_text import RichText
-
-
-    def page_to_streamfield(page):
-        changed = False
-        if page.body.raw_text and not page.body:
-            page.body = [('rich_text', {'rich_text': RichText(page.body.raw_text)})]
-            changed = True
-        return page, changed
-
-
-    def pagerevision_to_streamfield(revision_data):
-        changed = False
-        body = revision_data.get('body')
-        if body:
-            try:
-                json.loads(body)
-            except ValueError:
-                revision_data['body'] = json.dumps(
-                    [{
-                        "value": {"rich_text": body},
-                        "type": "rich_text"
-                    }],
-                    cls=DjangoJSONEncoder)
-                changed = True
-            else:
-                # It's already valid JSON. Leave it.
-                pass
-        return revision_data, changed
-
-
-    def page_to_richtext(page):
-        changed = False
-        if page.body.raw_text is None:
-            raw_text = ''.join([
-                child.value['rich_text'].source for child in page.body
-                if child.block_type == 'rich_text'
-            ])
-            page.body = raw_text
-            changed = True
-        return page, changed
-
-
-    def pagerevision_to_richtext(revision_data):
-        changed = False
-        body = revision_data.get('body', 'definitely non-JSON string')
-        if body:
-            try:
-                body_data = json.loads(body)
-            except ValueError:
-                # It's not apparently a StreamField. Leave it.
-                pass
-            else:
-                raw_text = ''.join([
-                    child['value']['rich_text'] for child in body_data
-                    if child['type'] == 'rich_text'
-                ])
-                revision_data['body'] = raw_text
-                changed = True
-        return revision_data, changed
-
-
-    def convert(apps, schema_editor, page_converter, pagerevision_converter):
-        BlogPage = apps.get_model("demo", "BlogPage")
-        for page in BlogPage.objects.all():
-
-            page, changed = page_converter(page)
-            if changed:
-                page.save()
-
-            for revision in page.revisions.all():
-                revision_data = revision.content
-                revision_data, changed = pagerevision_converter(revision_data)
-                if changed:
-                    revision.content = revision_data
-                    revision.save()
-
-
-    def convert_to_streamfield(apps, schema_editor):
-        return convert(apps, schema_editor, page_to_streamfield, pagerevision_to_streamfield)
-
-
-    def convert_to_richtext(apps, schema_editor):
-        return convert(apps, schema_editor, page_to_richtext, pagerevision_to_richtext)
-
-
-    class Migration(migrations.Migration):
-
-        dependencies = [
-            # leave the dependency line from the generated migration intact!
-            ('demo', '0001_initial'),
-        ]
-
-        operations = [
-            # leave the generated AlterField intact!
-            migrations.AlterField(
-                model_name='BlogPage',
-                name='body',
-                field=wagtail.fields.StreamField([('rich_text', wagtail.blocks.RichTextBlock())]),
-            ),
-
-            migrations.RunPython(
-                convert_to_streamfield,
-                convert_to_richtext,
-            ),
-        ]

+ 277 - 0
docs/topics/writing_templates.md

@@ -0,0 +1,277 @@
+(writing_templates)=
+
+# Writing templates
+
+Wagtail uses Django's templating language. For developers new to Django, start with Django's own template documentation:
+[](django:topics/templates)
+
+Python programmers new to Django/Wagtail may prefer more technical documentation:
+[](django:ref/templates/api)
+
+You should be familiar with Django templating basics before continuing with this documentation.
+
+## Templates
+
+Every type of page or "content type" in Wagtail is defined as a "model" in a file called `models.py`. If your site has a blog, you might have a `BlogPage` model and another called `BlogPageListing`. The names of the models are up to the Django developer.
+
+For each page model in `models.py`, Wagtail assumes an HTML template file exists of (almost) the same name. The Front End developer may need to create these templates themselves by referring to `models.py` to infer template names from the models defined therein.
+
+To find a suitable template, Wagtail converts CamelCase names to snake_case. So for a `BlogPage`, a template `blog_page.html` will be expected. The name of the template file can be overridden per model if necessary.
+
+Template files are assumed to exist here::
+
+```
+name_of_project/
+    name_of_app/
+        templates/
+            name_of_app/
+                blog_page.html
+        models.py
+```
+
+For more information, see the Django documentation for the [application directories template loader](django:ref/templates/api).
+
+### Page content
+
+The data/content entered into each page is accessed/output through Django's `{{ double-brace }}` notation. Each field from the model must be accessed by prefixing `page.`. e.g the page title `{{ page.title }}` or another field `{{ page.author }}`.
+
+A custom variable name can be :attr:`configured on the page model <wagtail.models.Page.context_object_name>`. If a custom name is defined, `page` is still available for use in shared templates.
+
+Additionally `request.` is available and contains Django's request object.
+
+## Static assets
+
+Static files e.g CSS, JS and images are typically stored here::
+
+```
+name_of_project/
+    name_of_app/
+        static/
+            name_of_app/
+                css/
+                js/
+                images/
+        models.py
+```
+
+(The names "css", "js" etc aren't important, only their position within the tree.)
+
+Any file within the static folder should be inserted into your HTML using the `{% static %}` tag. More about it: [](static_tag).
+
+### User images
+
+Images uploaded to a Wagtail site by its users (as opposed to a developer's static files, mentioned above) go into the image library and from there are added to pages via the [page editor interface](inserting_images).
+
+Unlike other CMSs, adding images to a page does not involve choosing a "version" of the image to use. Wagtail has no predefined image "formats" or "sizes". Instead the template developer defines image manipulation to occur _on the fly_ when the image is requested, via a special syntax within the template.
+
+Images from the library must be requested using this syntax, but a developer's static images can be added via conventional means e.g `img` tags. Only images from the library can be manipulated on the fly.
+
+Read more about the image manipulation syntax here: [](image_tag).
+
+(template-tags-and-filters)=
+
+## Template tags & filters
+
+In addition to Django's standard tags and filters, Wagtail provides some of its own, which can be `load`-ed [just like any other](django:howto/custom-template-tags).
+
+## Images (tag)
+
+The `image` tag inserts an XHTML-compatible `img` element into the page, setting its `src`, `width`, `height` and `alt`. See also [](image_tag_alt).
+
+The syntax for the `image` tag is thus:
+
+```html+django
+{% image [image] [resize-rule] %}
+```
+
+For example:
+
+```html+django
+{% load wagtailimages_tags %}
+...
+
+{% image page.photo width-400 %}
+
+<!-- or a square thumbnail: -->
+{% image page.photo fill-80x80 %}
+```
+
+See [](image_tag) for full documentation.
+
+(rich-text-filter)=
+
+## Rich text (filter)
+
+This filter takes a chunk of HTML content and renders it as safe HTML in the page. Importantly, it also expands internal shorthand references to embedded images, and links made in the Wagtail editor, into fully-baked HTML ready for display.
+
+Only fields using `RichTextField` need this applied in the template.
+
+```html+django
+{% load wagtailcore_tags %}
+...
+{{ page.body|richtext }}
+```
+
+(responsive-embeds)=
+
+### Responsive Embeds
+
+As Wagtail does not impose any styling of its own on templates, images and embedded media will be displayed at a fixed width as determined by the HTML. Images can be made to resize to fit their container using a CSS rule such as the following:
+
+```css
+.body img {
+    max-width: 100%;
+    height: auto;
+}
+```
+
+where `body` is a container element in your template surrounding the images.
+
+Making embedded media resizable is also possible, but typically requires custom style rules matching the media's aspect ratio. To assist in this, Wagtail provides built-in support for responsive embeds, which can be enabled by setting `WAGTAILEMBEDS_RESPONSIVE_HTML = True` in your project settings. This adds a CSS class of `responsive-object` and an inline `padding-bottom` style to the embed, to be used in conjunction with the following CSS:
+
+```css
+.responsive-object {
+    position: relative;
+}
+
+.responsive-object iframe,
+.responsive-object object,
+.responsive-object embed {
+    position: absolute;
+    top: 0;
+    left: 0;
+    width: 100%;
+    height: 100%;
+}
+```
+
+## Internal links (tag)
+
+(pageurl_tag)=
+
+### `pageurl`
+
+Takes a Page object and returns a relative URL (`/foo/bar/`) if within the same Site as the current page, or absolute (`http://example.com/foo/bar/`) if not.
+
+```html+django
+{% load wagtailcore_tags %}
+...
+<a href="{% pageurl page.get_parent %}">Back to index</a>
+```
+
+A `fallback` keyword argument can be provided - this can be a URL string, a named URL route that can be resolved with no parameters, or an object with a `get_absolute_url` method, and will be used as a substitute URL when the passed page is `None`.
+
+```html+django
+{% load wagtailcore_tags %}
+
+{% for publication in page.related_publications.all %}
+    <li>
+        <a href="{% pageurl publication.detail_page fallback='coming_soon' %}">
+            {{ publication.title }}
+        </a>
+    </li>
+{% endfor %}
+```
+
+(slugurl_tag)=
+
+### `slugurl`
+
+Takes any `slug` as defined in a page's "Promote" tab and returns the URL for the matching Page. If multiple pages exist with the same slug, the page chosen is undetermined.
+
+Like `pageurl`, this will try to provide a relative link if possible, but will default to an absolute link if the Page is on a different Site. This is most useful when creating shared page furniture, e.g. top level navigation or site-wide links.
+
+```html+django
+{% load wagtailcore_tags %}
+...
+<a href="{% slugurl 'news' %}">News index</a>
+```
+
+(static_tag)=
+
+## Static files (tag)
+
+Used to load anything from your static files directory. Use of this tag avoids rewriting all static paths if hosting arrangements change, as they might between development and live environments.
+
+```html+django
+{% load static %}
+...
+<img src="{% static "name_of_app/myimage.jpg" %}" alt="My image"/>
+```
+
+Notice that the full path is not required - the path given here is relative to the app's `static` directory. To avoid clashes with static files from other apps (including Wagtail itself), it's recommended to place static files in a subdirectory of `static` with the same name as the app.
+
+## Multi-site support
+
+(wagtail_site_tag)=
+
+### `wagtail_site`
+
+Returns the Site object corresponding to the current request.
+
+```html+django
+{% load wagtailcore_tags %}
+
+{% wagtail_site as current_site %}
+```
+
+(wagtailuserbar_tag)=
+
+## Wagtail User Bar
+
+This tag provides a contextual flyout menu for logged-in users. The menu gives editors the ability to edit the current page or add a child page, besides the options to show the page in the Wagtail page explorer or jump to the Wagtail admin dashboard. Moderators are also given the ability to accept or reject a page being previewed as part of content moderation.
+
+This tag may be used on standard Django views, without page object. The user bar will contain one item pointing to the admin.
+
+We recommend putting the tag near the top of the `<body>` element so keyboard users can reach it. You should consider putting the tag after any `[skip links](https://webaim.org/techniques/skipnav/) but before the navigation and main content of your page.
+
+```html+django
+{% load wagtailuserbar %}
+...
+<body>
+    <a id="#content">Skip to content</a>
+    {% wagtailuserbar %} {# This is a good place for the userbar #}
+    <nav>
+    ...
+    </nav>
+    <main id="content">
+    ...
+    </main>
+</body>
+```
+
+By default the User Bar appears in the bottom right of the browser window, inset from the edge. If this conflicts with your design it can be moved by passing a parameter to the template tag. These examples show you how to position the userbar in each corner of the screen:
+
+```html+django
+...
+{% wagtailuserbar 'top-left' %}
+{% wagtailuserbar 'top-right' %}
+{% wagtailuserbar 'bottom-left' %}
+{% wagtailuserbar 'bottom-right' %}
+...
+```
+
+The userbar can be positioned where it works best with your design. Alternatively, you can position it with a CSS rule in your own CSS files, for example:
+
+```css
+.wagtail-userbar {
+    top: 200px !important;
+    left: 10px !important;
+}
+```
+
+## Varying output between preview and live
+
+Sometimes you may wish to vary the template output depending on whether the page is being previewed or viewed live. For example, if you have visitor tracking code such as Google Analytics in place on your site, it's a good idea to leave this out when previewing, so that editor activity doesn't appear in your analytics reports. Wagtail provides a `request.is_preview` variable to distinguish between preview and live:
+
+```html+django
+{% if not request.is_preview %}
+    <script>
+        (function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
+        ...
+    </script>
+{% endif %}
+```
+
+If the page is being previewed, `request.preview_mode` can be used to determine the specific preview mode being used,
+if the page supports [multiple preview modes](wagtail.models.Page.preview_modes).

+ 0 - 302
docs/topics/writing_templates.rst

@@ -1,302 +0,0 @@
-.. _writing_templates:
-
-=================
-Writing templates
-=================
-
-Wagtail uses Django's templating language. For developers new to Django, start with Django's own template documentation:
-:doc:`django:topics/templates`
-
-Python programmers new to Django/Wagtail may prefer more technical documentation:
-:doc:`ref/templates/api`
-
-You should be familiar with Django templating basics before continuing with this documentation.
-
-Templates
-=========
-
-Every type of page or "content type" in Wagtail is defined as a "model" in a file called ``models.py``. If your site has a blog, you might have a ``BlogPage``  model and another called ``BlogPageListing``. The names of the models are up to the Django developer.
-
-For each page model in ``models.py``, Wagtail assumes an HTML template file exists of (almost) the same name. The Front End developer may need to create these templates themselves by referring to ``models.py`` to infer template names from the models defined therein.
-
-To find a suitable template, Wagtail converts CamelCase names to snake_case. So for a ``BlogPage``, a template ``blog_page.html`` will be expected. The name of the template file can be overridden per model if necessary.
-
-Template files are assumed to exist here::
-
-    name_of_project/
-        name_of_app/
-            templates/
-                name_of_app/
-                    blog_page.html
-            models.py
-
-
-For more information, see the Django documentation for the :doc:`application directories template loader <ref/templates/api>`.
-
-Page content
-~~~~~~~~~~~~
-
-The data/content entered into each page is accessed/output through Django's ``{{ double-brace }}`` notation. Each field from the model must be accessed by prefixing ``page.``. e.g the page title ``{{ page.title }}`` or another field ``{{ page.author }}``.
-
-A custom variable name can be :attr:`configured on the page model <wagtail.models.Page.context_object_name>`. If a custom name is defined, ``page`` is still available for use in shared templates.
-
-Additionally ``request.`` is available and contains Django's request object.
-
-Static assets
-=============
-
-Static files e.g CSS, JS and images are typically stored here::
-
-    name_of_project/
-        name_of_app/
-            static/
-                name_of_app/
-                    css/
-                    js/
-                    images/
-            models.py
-
-(The names "css", "js" etc aren't important, only their position within the tree.)
-
-Any file within the static folder should be inserted into your HTML using the ``{% static %}`` tag. More about it: :ref:`static_tag`.
-
-User images
-~~~~~~~~~~~
-
-Images uploaded to a Wagtail site by its users (as opposed to a developer's static files, mentioned above) go into the image library and from there are added to pages via the :doc:`page editor interface </editor_manual/new_pages/inserting_images>`.
-
-Unlike other CMSs, adding images to a page does not involve choosing a "version" of the image to use. Wagtail has no predefined image "formats" or "sizes". Instead the template developer defines image manipulation to occur *on the fly* when the image is requested, via a special syntax within the template.
-
-Images from the library must be requested using this syntax, but a developer's static images can be added via conventional means e.g ``img`` tags. Only images from the library can be manipulated on the fly.
-
-Read more about the image manipulation syntax here :ref:`image_tag`.
-
-.. _template-tags-and-filters:
-
-Template tags & filters
-=======================
-
-In addition to Django's standard tags and filters, Wagtail provides some of its own, which can be ``load``-ed :doc:`just like any other <howto/custom-template-tags>`.
-
-
-Images (tag)
-~~~~~~~~~~~~
-
-The ``image`` tag inserts an XHTML-compatible ``img`` element into the page, setting its ``src``, ``width``, ``height`` and ``alt``. See also :ref:`image_tag_alt`.
-
-The syntax for the ``image`` tag is thus:
-
-.. code-block:: html+django
-
-    {% image [image] [resize-rule] %}
-
-For example:
-
-.. code-block:: html+django
-
-    {% load wagtailimages_tags %}
-    ...
-
-    {% image page.photo width-400 %}
-
-    <!-- or a square thumbnail: -->
-    {% image page.photo fill-80x80 %}
-
-
-See :ref:`image_tag` for full documentation.
-
-
-.. _rich-text-filter:
-
-Rich text (filter)
-~~~~~~~~~~~~~~~~~~
-
-This filter takes a chunk of HTML content and renders it as safe HTML in the page. Importantly, it also expands internal shorthand references to embedded images, and links made in the Wagtail editor, into fully-baked HTML ready for display.
-
-Only fields using ``RichTextField`` need this applied in the template.
-
-.. code-block:: html+django
-
-    {% load wagtailcore_tags %}
-    ...
-    {{ page.body|richtext }}
-
-
-.. _responsive-embeds:
-
-Responsive Embeds
------------------
-
-As Wagtail does not impose any styling of its own on templates, images and embedded media will be displayed at a fixed width as determined by the HTML. Images can be made to resize to fit their container using a CSS rule such as the following:
-
-.. code-block:: css
-
-    .body img {
-        max-width: 100%;
-        height: auto;
-    }
-
-where ``body`` is a container element in your template surrounding the images.
-
-Making embedded media resizable is also possible, but typically requires custom style rules matching the media's aspect ratio. To assist in this, Wagtail provides built-in support for responsive embeds, which can be enabled by setting ``WAGTAILEMBEDS_RESPONSIVE_HTML = True`` in your project settings. This adds a CSS class of ``responsive-object`` and an inline ``padding-bottom`` style to the embed, to be used in conjunction with the following CSS:
-
-.. code-block:: css
-
-    .responsive-object {
-        position: relative;
-    }
-
-    .responsive-object iframe,
-    .responsive-object object,
-    .responsive-object embed {
-        position: absolute;
-        top: 0;
-        left: 0;
-        width: 100%;
-        height: 100%;
-    }
-
-
-Internal links (tag)
-~~~~~~~~~~~~~~~~~~~~
-
-.. _pageurl_tag:
-
-``pageurl``
------------
-
-Takes a Page object and returns a relative URL (``/foo/bar/``) if within the same Site as the current page, or absolute (``http://example.com/foo/bar/``) if not.
-
-.. code-block:: html+django
-
-    {% load wagtailcore_tags %}
-    ...
-    <a href="{% pageurl page.get_parent %}">Back to index</a>
-
-
-A ``fallback`` keyword argument can be provided - this can be a URL string, a named URL route that can be resolved with no parameters, or an object with a ``get_absolute_url`` method, and will be used as a substitute URL when the passed page is ``None``.
-
-.. code-block:: html+django
-
-    {% load wagtailcore_tags %}
-
-    {% for publication in page.related_publications.all %}
-        <li>
-            <a href="{% pageurl publication.detail_page fallback='coming_soon' %}">
-                {{ publication.title }}
-            </a>
-        </li>
-    {% endfor %}
-
-
-.. _slugurl_tag:
-
-``slugurl``
-------------
-
-Takes any ``slug`` as defined in a page's "Promote" tab and returns the URL for the matching Page. If multiple pages exist with the same slug, the page chosen is undetermined.
-
-Like ``pageurl``, this will try to provide a relative link if possible, but will default to an absolute link if the Page is on a different Site. This is most useful when creating shared page furniture, e.g. top level navigation or site-wide links.
-
-.. code-block:: html+django
-
-    {% load wagtailcore_tags %}
-    ...
-    <a href="{% slugurl 'news' %}">News index</a>
-
-
-.. _static_tag:
-
-Static files (tag)
-~~~~~~~~~~~~~~~~~~
-
-Used to load anything from your static files directory. Use of this tag avoids rewriting all static paths if hosting arrangements change, as they might between development and live environments.
-
-.. code-block:: html+django
-
-    {% load static %}
-    ...
-    <img src="{% static "name_of_app/myimage.jpg" %}" alt="My image"/>
-
-Notice that the full path is not required - the path given here is relative to the app's ``static`` directory. To avoid clashes with static files from other apps (including Wagtail itself), it's recommended to place static files in a subdirectory of ``static`` with the same name as the app.
-
-
-Multi-site support
-~~~~~~~~~~~~~~~~~~
-
-.. _wagtail_site_tag:
-
-``wagtail_site``
-----------------
-
-Returns the Site object corresponding to the current request.
-
-.. code-block:: html+django
-
-    {% load wagtailcore_tags %}
-
-    {% wagtail_site as current_site %}
-
-.. _wagtailuserbar_tag:
-
-Wagtail User Bar
-================
-
-This tag provides a contextual flyout menu for logged-in users. The menu gives editors the ability to edit the current page or add a child page, besides the options to show the page in the Wagtail page explorer or jump to the Wagtail admin dashboard. Moderators are also given the ability to accept or reject a page being previewed as part of content moderation.
-
-This tag may be used on standard Django views, without page object. The user bar will contain one item pointing to the admin.
-
-We recommend putting the tag near the top of the ``<body>`` element so keyboard users can reach it. You should consider putting the tag after any `skip links <https://webaim.org/techniques/skipnav/>`_ but before the navigation and main content of your page.
-
-.. code-block:: html+django
-
-    {% load wagtailuserbar %}
-    ...
-    <body>
-      <a id="#content">Skip to content</a>
-      {% wagtailuserbar %} {# This is a good place for the userbar #}
-      <nav>
-      ...
-      </nav>
-      <main id="content">
-      ...
-      </main>
-    </body>
-
-By default the User Bar appears in the bottom right of the browser window, inset from the edge. If this conflicts with your design it can be moved by passing a parameter to the template tag. These examples show you how to position the userbar in each corner of the screen:
-
-.. code-block:: html+django
-
-    ...
-    {% wagtailuserbar 'top-left' %}
-    {% wagtailuserbar 'top-right' %}
-    {% wagtailuserbar 'bottom-left' %}
-    {% wagtailuserbar 'bottom-right' %}
-    ...
-
-The userbar can be positioned where it works best with your design. Alternatively, you can position it with a CSS rule in your own CSS files, for example:
-
-.. code-block:: css
-
-    .wagtail-userbar {
-         top: 200px !important;
-         left: 10px !important;
-    }
-
-
-Varying output between preview and live
-=======================================
-
-Sometimes you may wish to vary the template output depending on whether the page is being previewed or viewed live. For example, if you have visitor tracking code such as Google Analytics in place on your site, it's a good idea to leave this out when previewing, so that editor activity doesn't appear in your analytics reports. Wagtail provides a ``request.is_preview`` variable to distinguish between preview and live:
-
-.. code-block:: html+django
-
-    {% if not request.is_preview %}
-        <script>
-          (function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
-          ...
-        </script>
-    {% endif %}
-
-If the page is being previewed, ``request.preview_mode`` can be used to determine the specific preview mode being used,
-if the page supports :attr:`multiple preview modes <wagtail.models.Page.preview_modes>`.