Browse Source

convert documentation in advanced_topics from rst to md

- relates to #8383
Thiago Costa de Souza 2 years ago
parent
commit
f42ec9ed6b

+ 152 - 0
docs/advanced_topics/accessibility_considerations.md

@@ -0,0 +1,152 @@
+# Accessibility considerations
+
+Accessibility for CMS-driven websites is a matter of [modeling content appropriately](content_modeling), [creating accessible templates](accessibility_in_templates), and [authoring accessible content](authoring_accessible_content) with readability and accessibility guidelines in mind.
+
+Wagtail generally puts developers in control of content modeling and front-end markup, but there are a few areas to be aware of nonetheless, and ways to help authors be aware of readability best practices.
+Note there is much more to building accessible websites than we cover here – see our list of [accessibility resources](accessibility_resources) for more information.
+
+```{contents}
+---
+local:
+depth: 1
+---
+```
+
+(content_modeling)=
+
+## Content modeling
+
+As part of defining your site’s models, here are areas to pay special attention to:
+
+### Alt text for images
+
+The default behaviour for Wagtail images is to use the `title` field as the alt text ([#4945](https://github.com/wagtail/wagtail/issues/4945)).
+This is inappropriate, as it’s not communicated in the CMS interface, and the image upload form uses the image’s filename as the title by default.
+
+Ideally, always add an optional “alt text” field wherever an image is used, alongside the image field:
+
+-   For normal fields, add an alt text field to your image’s panel.
+-   For StreamField, add an extra field to your image block.
+-   For rich text – Wagtail already makes it possible to customise alt text for rich text images.
+
+When defining the alt text fields, make sure they are optional so editors can choose to not write any alt text for decorative images. Take the time to provide `help_text` with appropriate guidance.
+For example, linking to [established resources on alt text](https://axesslab.com/alt-texts/).
+
+```{note}
+Should I add an alt text field on the Image model for my site?
+
+It’s better than nothing to have a dedicated alt field on the Image model ([#5789](https://github.com/wagtail/wagtail/pull/5789)), and may be appropriate for some websites, but we recommend to have it inline with the content because ideally alt text should be written for the context the image is used in:
+
+- If the alt text’s content is already part of the rest of the page, ideally the image should not repeat the same content.
+- Ideally, the alt text should be written based on the context the image is displayed in.
+- An image might be decorative in some cases but not in others. For example, thumbnails in page listings can often be considered decorative.
+```
+
+See [RFC 51: Contextual alt text](https://github.com/wagtail/rfcs/pull/51) for a long-term solution to this problem.
+
+### Embeds title
+
+Missing embed titles are common failures in accessibility audits of Wagtail websites. In some cases, Wagtail embeds’ iframe doesn’t have a `title` attribute set. This is generally a problem with OEmbed providers like YouTube ([#5982](https://github.com/wagtail/wagtail/issues/5982)).
+This is very problematic for screen reader users, who rely on the title to understand what the embed is, and whether to interact with it or not.
+
+If your website relies on embeds that have are missing titles, make sure to either:
+
+-   Add the OEmbed _title_ field as a `title` on the `iframe`.
+-   Add a custom mandatory Title field to your embeds, and add it as the `iframe`’s `title`.
+
+### Available heading levels
+
+Wagtail makes it very easy for developers to control which heading levels should be available for any given content, via [rich text features](rich_text_features) or custom StreamField blocks.
+In both cases, take the time to restrict what heading levels are available so the pages’ document outline is more likely to be logical and sequential. Consider using the following restrictions:
+
+-   Disallow `h1` in rich text. There should only be one `h1` tag per page, which generally maps to the page’s `title`.
+-   Limit heading levels to `h2` for the main content of a page. Add `h3` only if deemed necessary. Avoid other levels as a general rule.
+-   For content that is displayed in a specific section of the page, limit heading levels to those directly below the section’s main heading.
+
+If managing headings via StreamField, make sure to apply the same restrictions there.
+
+### Bold and italic formatting in rich text
+
+By default, Wagtail stores its bold formatting as a `b` tag, and italic as `i` ([#4665](https://github.com/wagtail/wagtail/issues/4665)). While those tags don’t necessarily always have correct semantics (`strong` and `em` are more ubiquitous), there isn’t much consequence for screen reader users, as by default screen readers do not announce content differently based on emphasis.
+
+If this is a concern to you, you can change which tags are used when saving content with [rich text format converters](rich_text_format_converters). In the future, [rich text rewrite handlers](rich_text_rewrite_handlers) should also support this being done without altering the storage format ([#4223](https://github.com/wagtail/wagtail/issues/4223)).
+
+### TableBlock
+
+The [TableBlock](../reference/contrib/table_block) default implementation makes it too easy for end-users to miss they need either row or column headers ([#5989](https://github.com/wagtail/wagtail/issues/5989>)). Make sure to always have either row headers or column headers set.
+Always add a Caption, so screen reader users navigating the site’s tables know where they are.
+
+(accessibility_in_templates)=
+
+## Accessibility in templates
+
+Here are common gotchas to be aware of to make the site’s templates as accessible as possible,
+
+### Alt text in templates
+
+See the [content modelling](content_modeling) section above. Additionally, make sure to [customise images’ alt text](image_tag_alt), either setting it to the relevant field, or to an empty string for decorative images, or images where the alt text would be a repeat of other content.
+Even when your images have alt text coming directly from the image model, you still need to decide whether there should be alt text for the particular context the image is used in. For example, avoid alt text in listings where the alt text just repeats the listing items’ title.
+
+### Empty heading tags
+
+In both rich text and custom StreamField blocks, it’s sometimes easy for editors to create a heading block but not add any content to it. If this is a problem for your site,
+
+-   Add validation rules to those fields, making sure the page can’t be saved with the empty headings, for example by using the [StreamField](../topics/streamfield) `CharBlock` which is required by default.
+-   Consider adding similar validation rules for rich text fields ([#6526](https://github.com/wagtail/wagtail/issues/6526)).
+
+Additionally, you can hide empty heading blocks with CSS:
+
+```css
+h1:empty,
+h2:empty,
+h3:empty,
+h4:empty,
+h5:empty,
+h6:empty {
+    display: none;
+}
+```
+
+### Forms
+
+The [Form builder](form_builder) uses Django’s forms API. Here are considerations specific to forms in templates:
+
+-   Avoid rendering helpers such as `as_table`, `as_ul`, `as_p`, which can make forms harder to navigate for screen reader users or cause HTML validation issues (see Django ticket [#32339](https://code.djangoproject.com/ticket/32339)).
+-   Make sure to visually distinguish required and optional fields.
+-   Take the time to group related fields together in `fieldset`, with an appropriate `legend`, in particular for radios and checkboxes (see Django ticket [#32338](https://code.djangoproject.com/ticket/32338)).
+-   If relevant, use the appropriate `autocomplete` and `autocapitalize` attributes.
+-   For Date and Datetime fields, make sure to display the expected format or an example value (see Django ticket [#32340](https://code.djangoproject.com/ticket/32340)). Or use [input type="date"](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/date).
+-   For Number fields, consider whether `input type="number"` really is appropriate, or whether there may be [better alternatives such as inputmode](https://technology.blog.gov.uk/2020/02/24/why-the-gov-uk-design-system-team-changed-the-input-type-for-numbers).
+
+Make sure to test your forms’ implementation with assistive technologies, and review [official W3C guidance on accessible forms development](https://www.w3.org/WAI/tutorials/forms) for further information.
+
+(authoring_accessible_content)=
+
+## Authoring accessible content
+
+Here are things you can do to help authors create accessible content.
+
+### wagtail-accessibility
+
+[wagtail-accessibility](https://github.com/neon-jungle/wagtail-accessibility) is a third-party package which adds [tota11y](https://khan.github.io/tota11y/) to Wagtail previews.
+This makes it easy for authors to run basic accessibility checks – validating the page’s heading outline, or link text.
+
+### help_text and HelpPanel
+
+Occasional Wagtail users may not be aware of your site’s content guidelines, or best practices of writing for the web. Use fields’ `help_text` and `HelpPanel` (see [Panel types](../reference/pages/panels)).
+
+### Readability
+
+Readability is fundamental to accessibility. One of the ways to improve text content is to have a clear target for reading level / reading age, which can be assessed with [wagtail-readinglevel](https://github.com/vixdigital/wagtail-readinglevel) as a score displayed in rich text fields.
+
+(accessibility_resources)=
+
+## Accessibility resources
+
+We focus on considerations specific to Wagtail websites, but there is much more to accessibility. Here are valuable resources to learn more, for developers but also designers and authors:
+
+-   [W3C Accessibility Fundamentals](https://www.w3.org/WAI/fundamentals/)
+-   [The A11Y Project](https://www.a11yproject.com/)
+-   [US GSA – Accessibility for Teams](https://accessibility.digital.gov/)
+-   [UK GDS – Dos and don’ts on designing for accessibility](https://accessibility.blog.gov.uk/2016/09/02/dos-and-donts-on-designing-for-accessibility/)
+-   [Accessibility Developer Guide](https://www.accessibility-developer-guide.com/)

+ 0 - 168
docs/advanced_topics/accessibility_considerations.rst

@@ -1,168 +0,0 @@
-Accessibility considerations
-============================
-
-Accessibility for CMS-driven websites is a matter of :ref:`modeling content appropriately <content_modeling>`, :ref:`creating accessible templates <accessibility_in_templates>`, and :ref:`authoring content <authoring_accessible_content>` with readability and accessibility guidelines in mind.
-
-Wagtail generally puts developers in control of content modeling and front-end markup, but there are a few areas to be aware of nonetheless, and ways to help authors be aware of readability best practices.
-Note there is much more to building accessible websites than we cover here – see our list of :ref:`accessibility resources <accessibility_resources>` for more information.
-
-
-* :ref:`Content modeling <content_modeling>`
-* :ref:`Accessibility in templates <accessibility_in_templates>`
-* :ref:`Authoring accessible content <authoring_accessible_content>`
-* :ref:`Accessibility resources <accessibility_resources>`
-
-----
-
-.. _content_modeling:
-
-Content modeling
-~~~~~~~~~~~~~~~~
-
-As part of defining your site’s models, here are areas to pay special attention to:
-
-Alt text for images
--------------------
-
-The default behaviour for Wagtail images is to use the ``title`` field as the alt text (`#4945 <https://github.com/wagtail/wagtail/issues/4945>`_).
-This is inappropriate, as it’s not communicated in the CMS interface, and the image upload form uses the image’s filename as the title by default.
-
-Ideally, always add an optional “alt text” field wherever an image is used, alongside the image field:
-
-- For normal fields, add an alt text field to your image’s panel.
-- For StreamField, add an extra field to your image block.
-- For rich text – Wagtail already makes it possible to customise alt text for rich text images.
-
-When defining the alt text fields, make sure they are optional so editors can choose to not write any alt text for decorative images. Take the time to provide ``help_text`` with appropriate guidance.
-For example, linking to `established resources on alt text <https://axesslab.com/alt-texts/>`_.
-
-.. note:: Should I add an alt text field on the Image model for my site?
-
-    It’s better than nothing to have a dedicated alt field on the Image model (`#5789 <https://github.com/wagtail/wagtail/pull/5789>`_), and may be appropriate for some websites, but we recommend to have it inline with the content because ideally alt text should be written for the context the image is used in:
-
-    - If the alt text’s content is already part of the rest of the page, ideally the image should not repeat the same content.
-    - Ideally, the alt text should be written based on the context the image is displayed in.
-    - An image might be decorative in some cases but not in others. For example, thumbnails in page listings can often be considered decorative.
-
-See `RFC 51: Contextual alt text <https://github.com/wagtail/rfcs/pull/51>`_ for a long-term solution to this problem.
-
-Embeds title
-------------
-
-Missing embed titles are common failures in accessibility audits of Wagtail websites. In some cases, Wagtail embeds’ iframe doesn’t have a ``title`` attribute set. This is generally a problem with OEmbed providers like YouTube (`#5982 <https://github.com/wagtail/wagtail/issues/5982>`_).
-This is very problematic for screen reader users, who rely on the title to understand what the embed is, and whether to interact with it or not.
-
-If your website relies on embeds that have are missing titles, make sure to either:
-
-- Add the OEmbed `title` field as a ``title`` on the ``iframe``.
-- Add a custom mandatory Title field to your embeds, and add it as the ``iframe``’s ``title``.
-
-Available heading levels
-------------------------
-
-Wagtail makes it very easy for developers to control which heading levels should be available for any given content, via :ref:`rich text features <rich_text_features>` or custom StreamField blocks.
-In both cases, take the time to restrict what heading levels are available so the pages’ document outline is more likely to be logical and sequential. Consider using the following restrictions:
-
-- Disallow ``h1`` in rich text. There should only be one ``h1`` tag per page, which generally maps to the page’s ``title``.
-- Limit heading levels to ``h2`` for the main content of a page. Add ``h3`` only if deemed necessary. Avoid other levels as a general rule.
-- For content that is displayed in a specific section of the page, limit heading levels to those directly below the section’s main heading.
-
-If managing headings via StreamField, make sure to apply the same restrictions there.
-
-Bold and italic formatting in rich text
----------------------------------------
-
-By default, Wagtail stores its bold formatting as a ``b`` tag, and italic as ``i`` (`#4665 <https://github.com/wagtail/wagtail/issues/4665>`_). While those tags don’t necessarily always have correct semantics (``strong`` and ``em`` are more ubiquitous), there isn’t much consequence for screen reader users, as by default screen readers do not announce content differently based on emphasis.
-
-If this is a concern to you, you can change which tags are used when saving content with :ref:`rich text format converters <rich_text_format_converters>`. In the future, :ref:`rich text rewrite handlers <rich_text_rewrite_handlers>` should also support this being done without altering the storage format (`#4223 <https://github.com/wagtail/wagtail/issues/4223>`_).
-
-TableBlock
-----------
-
-The :doc:`/reference/contrib/table_block` default implementation makes it too easy for end-users to miss they need either row or column headers (`#5989 <https://github.com/wagtail/wagtail/issues/5989>`_). Make sure to always have either row headers or column headers set.
-Always add a Caption, so screen reader users navigating the site’s tables know where they are.
-
-----
-
-.. _accessibility_in_templates:
-
-Accessibility in templates
-~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-Here are common gotchas to be aware of to make the site’s templates as accessible as possible,
-
-Alt text in templates
----------------------
-
-See the :ref:`content modelling <content_modeling>` section above. Additionally, make sure to :ref:`customise images’ alt text <image_tag_alt>`, either setting it to the relevant field, or to an empty string for decorative images, or images where the alt text would be a repeat of other content.
-Even when your images have alt text coming directly from the image model, you still need to decide whether there should be alt text for the particular context the image is used in. For example, avoid alt text in listings where the alt text just repeats the listing items’ title.
-
-Empty heading tags
-------------------
-
-In both rich text and custom StreamField blocks, it’s sometimes easy for editors to create a heading block but not add any content to it. If this is a problem for your site,
-
-- Add validation rules to those fields, making sure the page can’t be saved with the empty headings, for example by using the :doc:`StreamField </topics/streamfield>` ``CharBlock`` which is required by default.
-- Consider adding similar validation rules for rich text fields (`#6526 <https://github.com/wagtail/wagtail/issues/6526>`_).
-
-Additionally, you can hide empty heading blocks with CSS:
-
-.. code-block:: css
-
-    h1:empty, h2:empty, h3:empty, h4:empty, h5:empty, h6:empty {
-        display: none;
-    }
-
-Forms
------
-
-The :ref:`Form builder <form_builder>` uses Django’s forms API. Here are considerations specific to forms in templates:
-
-- Avoid rendering helpers such as ``as_table``, ``as_ul``, ``as_p``, which can make forms harder to navigate for screen reader users or cause HTML validation issues (see Django ticket `#32339 <https://code.djangoproject.com/ticket/32339>`_).
-- Make sure to visually distinguish required and optional fields.
-- Take the time to group related fields together in ``fieldset``, with an appropriate ``legend``, in particular for radios and checkboxes (see Django ticket `#32338 <https://code.djangoproject.com/ticket/32338>`_).
-- If relevant, use the appropriate ``autocomplete`` and ``autocapitalize`` attributes.
-- For Date and Datetime fields, make sure to display the expected format or an example value (see Django ticket `#32340 <https://code.djangoproject.com/ticket/32340>`_). Or use `input type="date" <https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/date>`_.
-- For Number fields, consider whether ``input type="number"`` really is appropriate, or whether there may be `better alternatives such as inputmode <https://technology.blog.gov.uk/2020/02/24/why-the-gov-uk-design-system-team-changed-the-input-type-for-numbers/>`_.
-
-Make sure to test your forms’ implementation with assistive technologies, and review `official W3C guidance on accessible forms development <https://www.w3.org/WAI/tutorials/forms/>`_ for further information.
-
-----
-
-.. _authoring_accessible_content:
-
-Authoring accessible content
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-Here are things you can do to help authors create accessible content.
-
-wagtail-accessibility
----------------------
-
-`wagtail-accessibility <https://github.com/neon-jungle/wagtail-accessibility>`_ is a third-party package which adds `tota11y <https://khan.github.io/tota11y/>`_ to Wagtail previews.
-This makes it easy for authors to run basic accessibility checks – validating the page’s heading outline, or link text.
-
-help_text and HelpPanel
------------------------
-
-Occasional Wagtail users may not be aware of your site’s content guidelines, or best practices of writing for the web. Use fields’ ``help_text`` and ``HelpPanel`` (see :doc:`/reference/pages/panels`).
-
-Readability
------------
-
-Readability is fundamental to accessibility. One of the ways to improve text content is to have a clear target for reading level / reading age, which can be assessed with `wagtail-readinglevel <https://github.com/vixdigital/wagtail-readinglevel>`_ as a score displayed in rich text fields.
-
-----
-
-.. _accessibility_resources:
-
-Accessibility resources
-~~~~~~~~~~~~~~~~~~~~~~~
-
-We focus on considerations specific to Wagtail websites, but there is much more to accessibility. Here are valuable resources to learn more, for developers but also designers and authors:
-
-- `W3C Accessibility Fundamentals <https://www.w3.org/WAI/fundamentals/>`_
-- `The A11Y Project <https://www.a11yproject.com/>`_
-- `US GSA – Accessibility for Teams <https://accessibility.digital.gov/>`_
-- `UK GDS – Dos and don’ts on designing for accessibility <https://accessibility.blog.gov.uk/2016/09/02/dos-and-donts-on-designing-for-accessibility/>`_
-- `Accessibility Developer Guide <https://www.accessibility-developer-guide.com/>`_

+ 381 - 0
docs/advanced_topics/add_to_django_project.md

@@ -0,0 +1,381 @@
+# How to add Wagtail into an existing Django project
+
+To install Wagtail completely from scratch, create a new Django project and an app within that project. For instructions on these tasks, see [Writing your first Django app](django:intro/tutorial01). Your project directory will look like the following:
+
+```
+myproject/
+    myproject/
+        __init__.py
+        settings.py
+        urls.py
+        wsgi.py
+    myapp/
+        __init__.py
+        models.py
+        tests.py
+        admin.py
+        views.py
+    manage.py
+```
+
+From your app directory, you can safely remove `admin.py` and `views.py`, since Wagtail will provide this functionality for your models. Configuring Django to load Wagtail involves adding modules and variables to `settings.py` and URL configuration to `urls.py`. For a more complete view of what's defined in these files, see [Django Settings](django:topics/settings) and [Django URL Dispatcher](django:topics/http/urls).
+
+What follows is a settings reference which skips many boilerplate Django settings. If you just want to get your Wagtail install up quickly without fussing with settings at the moment, see [](complete_example_config).
+
+## Middleware (`settings.py`)
+
+```python
+MIDDLEWARE = [
+  'django.contrib.sessions.middleware.SessionMiddleware',
+  'django.middleware.common.CommonMiddleware',
+  'django.middleware.csrf.CsrfViewMiddleware',
+  'django.contrib.auth.middleware.AuthenticationMiddleware',
+  'django.contrib.messages.middleware.MessageMiddleware',
+  'django.middleware.clickjacking.XFrameOptionsMiddleware',
+  'django.middleware.security.SecurityMiddleware',
+
+  'wagtail.contrib.redirects.middleware.RedirectMiddleware',
+]
+```
+
+Wagtail depends on the default set of Django middleware modules, to cover basic security and functionality such as login sessions. One additional middleware module is provided:
+
+**`RedirectMiddleware`**  
+ Wagtail provides a simple interface for adding arbitrary redirects to your site and this module makes it happen.
+
+## Apps (`settings.py`)
+
+```python
+INSTALLED_APPS = [
+
+  'myapp',  # your own app
+
+  'wagtail.contrib.forms',
+  'wagtail.contrib.redirects',
+  'wagtail.embeds',
+  'wagtail.sites',
+  'wagtail.users',
+  'wagtail.snippets',
+  'wagtail.documents',
+  'wagtail.images',
+  'wagtail.search',
+  'wagtail.admin',
+  'wagtail',
+
+  'taggit',
+  'modelcluster',
+
+  'django.contrib.auth',
+  'django.contrib.contenttypes',
+  'django.contrib.sessions',
+  'django.contrib.messages',
+  'django.contrib.staticfiles',
+]
+```
+
+Wagtail requires several Django app modules, third-party apps, and defines several apps of its own. Wagtail was built to be modular, so many Wagtail apps can be omitted to suit your needs. Your own app (here `myapp`) is where you define your models, templates, static assets, template tags, and other custom functionality for your site.
+
+### Wagtail Apps
+
+**`wagtail`**  
+ The core functionality of Wagtail, such as the `Page` class, the Wagtail tree, and model fields.
+
+**`wagtail.admin`**  
+ The administration interface for Wagtail, including page edit handlers.
+
+**`wagtail.documents`**  
+ The Wagtail document content type.
+
+**`wagtail.snippets`**  
+ Editing interface for non-Page models and objects. See [](Snippets).
+
+**`wagtail.users`**  
+ User editing interface.
+
+**`wagtail.images`**  
+ The Wagtail image content type.
+
+**`wagtail.embeds`**  
+ Module governing oEmbed and Embedly content in Wagtail rich text fields. See [](inserting_videos).
+
+**`wagtail.search`**  
+ Search framework for Page content. See [](wagtailsearch).
+
+**`wagtail.sites`**  
+ Management UI for Wagtail sites.
+
+**`wagtail.contrib.redirects`**  
+ Admin interface for creating arbitrary redirects on your site.
+
+**`wagtail.contrib.forms`**  
+ Models for creating forms on your pages and viewing submissions. See [Form builder](form_builder).
+
+### Third-Party Apps
+
+**`taggit`**  
+ Tagging framework for Django. This is used internally within Wagtail for image and document tagging and is available for your own models as well. See [](tagging) for a Wagtail model recipe or the [Taggit Documentation](https://django-taggit.readthedocs.org/en/latest/index.html).
+
+**`modelcluster`**  
+ Extension of Django ForeignKey relation functionality, which is used in Wagtail pages for on-the-fly related object creation. For more information, see [](inline_panels) or [the django-modelcluster github project page](https://github.com/torchbox/django-modelcluster).
+
+## URL Patterns
+
+```python
+from django.contrib import admin
+
+from wagtail import urls as wagtail_urls
+from wagtail.admin import urls as wagtailadmin_urls
+from wagtail.documents import urls as wagtaildocs_urls
+
+urlpatterns = [
+    path('django-admin/', admin.site.urls),
+
+    path('admin/', include(wagtailadmin_urls)),
+    path('documents/', include(wagtaildocs_urls)),
+
+    # Optional URL for including your own vanilla Django urls/views
+    re_path(r'', include('myapp.urls')),
+
+    # For anything not caught by a more specific rule above, hand over to
+    # Wagtail's serving mechanism
+    re_path(r'', include(wagtail_urls)),
+]
+```
+
+This block of code for your project's `urls.py` does a few things:
+
+-   Load the vanilla Django admin interface to `/django-admin/`
+-   Load the Wagtail admin and its various apps
+-   Dispatch any vanilla Django apps you're using other than Wagtail which require their own URL configuration (this is optional, since Wagtail might be all you need)
+-   Lets Wagtail handle any further URL dispatching.
+
+That's not everything you might want to include in your project's URL configuration, but it's what's necessary for Wagtail to flourish.
+
+(complete_example_config)=
+
+## Ready to Use Example Configuration Files
+
+These two files should reside in your project directory (`myproject/myproject/`).
+
+### `settings.py`
+
+```python
+import os
+
+PROJECT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
+BASE_DIR = os.path.dirname(PROJECT_DIR)
+
+DEBUG = True
+
+# Application definition
+
+INSTALLED_APPS = [
+    'myapp',
+
+    'wagtail.contrib.forms',
+    'wagtail.contrib.redirects',
+    'wagtail.embeds',
+    'wagtail.sites',
+    'wagtail.users',
+    'wagtail.snippets',
+    'wagtail.documents',
+    'wagtail.images',
+    'wagtail.search',
+    'wagtail.admin',
+    'wagtail',
+
+    'taggit',
+    'modelcluster',
+
+    'django.contrib.auth',
+    'django.contrib.contenttypes',
+    'django.contrib.sessions',
+    'django.contrib.messages',
+    'django.contrib.staticfiles',
+]
+
+
+MIDDLEWARE = [
+    'django.contrib.sessions.middleware.SessionMiddleware',
+    'django.middleware.common.CommonMiddleware',
+    'django.middleware.csrf.CsrfViewMiddleware',
+    'django.contrib.auth.middleware.AuthenticationMiddleware',
+    'django.contrib.messages.middleware.MessageMiddleware',
+    'django.middleware.clickjacking.XFrameOptionsMiddleware',
+    'django.middleware.security.SecurityMiddleware',
+
+    'wagtail.contrib.redirects.middleware.RedirectMiddleware',
+]
+
+ROOT_URLCONF = 'myproject.urls'
+
+TEMPLATES = [
+    {
+        'BACKEND': 'django.template.backends.django.DjangoTemplates',
+        'DIRS': [
+            os.path.join(PROJECT_DIR, 'templates'),
+        ],
+        'APP_DIRS': True,
+        'OPTIONS': {
+            'context_processors': [
+                'django.template.context_processors.debug',
+                'django.template.context_processors.request',
+                'django.contrib.auth.context_processors.auth',
+                'django.contrib.messages.context_processors.messages',
+            ],
+        },
+    },
+]
+
+WSGI_APPLICATION = 'myproject.wsgi.application'
+
+# Database
+
+DATABASES = {
+    'default': {
+        'ENGINE': 'django.db.backends.postgresql',
+        'NAME': 'myprojectdb',
+        'USER': 'postgres',
+        'PASSWORD': '',
+        'HOST': '',  # Set to empty string for localhost.
+        'PORT': '',  # Set to empty string for default.
+        'CONN_MAX_AGE': 600,  # number of seconds database connections should persist for
+    }
+}
+
+# Internationalization
+
+LANGUAGE_CODE = 'en-us'
+TIME_ZONE = 'UTC'
+USE_I18N = True
+USE_L10N = True
+USE_TZ = True
+
+
+# Static files (CSS, JavaScript, Images)
+
+STATICFILES_FINDERS = [
+    'django.contrib.staticfiles.finders.FileSystemFinder',
+    'django.contrib.staticfiles.finders.AppDirectoriesFinder',
+]
+
+STATICFILES_DIRS = [
+    os.path.join(PROJECT_DIR, 'static'),
+]
+
+STATIC_ROOT = os.path.join(BASE_DIR, 'static')
+STATIC_URL = '/static/'
+
+MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
+MEDIA_URL = '/media/'
+
+
+ADMINS = [
+    # ('Your Name', 'your_email@example.com'),
+]
+MANAGERS = ADMINS
+
+# Default to dummy email backend. Configure dev/production/local backend
+# as per https://docs.djangoproject.com/en/stable/topics/email/#email-backends
+EMAIL_BACKEND = 'django.core.mail.backends.dummy.EmailBackend'
+
+# Hosts/domain names that are valid for this site; required if DEBUG is False
+ALLOWED_HOSTS = []
+
+# Make this unique, and don't share it with anybody.
+SECRET_KEY = 'change-me'
+
+EMAIL_SUBJECT_PREFIX = '[Wagtail] '
+
+INTERNAL_IPS = ('127.0.0.1', '10.0.2.2')
+
+# A sample logging configuration. The only tangible logging
+# performed by this configuration is to send an email to
+# the site admins on every HTTP 500 error when DEBUG=False.
+# See https://docs.djangoproject.com/en/stable/topics/logging for
+# more details on how to customise your logging configuration.
+LOGGING = {
+    'version': 1,
+    'disable_existing_loggers': False,
+    'filters': {
+        'require_debug_false': {
+            '()': 'django.utils.log.RequireDebugFalse'
+        }
+    },
+    'handlers': {
+        'mail_admins': {
+            'level': 'ERROR',
+            'filters': ['require_debug_false'],
+            'class': 'django.utils.log.AdminEmailHandler'
+        }
+    },
+    'loggers': {
+        'django.request': {
+            'handlers': ['mail_admins'],
+            'level': 'ERROR',
+            'propagate': True,
+        },
+    }
+}
+
+
+# WAGTAIL SETTINGS
+
+# This is the human-readable name of your Wagtail install
+# which welcomes users upon login to the Wagtail admin.
+WAGTAIL_SITE_NAME = 'My Project'
+
+# Replace the search backend
+#WAGTAILSEARCH_BACKENDS = {
+#  'default': {
+#    'BACKEND': 'wagtail.search.backends.elasticsearch5',
+#    'INDEX': 'myapp'
+#  }
+#}
+
+# Wagtail email notifications from address
+# WAGTAILADMIN_NOTIFICATION_FROM_EMAIL = 'wagtail@myhost.io'
+
+# Wagtail email notification format
+# WAGTAILADMIN_NOTIFICATION_USE_HTML = True
+
+# Reverse the default case-sensitive handling of tags
+TAGGIT_CASE_INSENSITIVE = True
+```
+
+### `urls.py`
+
+```python
+from django.urls import include, path, re_path
+from django.conf.urls.static import static
+from django.views.generic.base import RedirectView
+from django.contrib import admin
+from django.conf import settings
+import os.path
+
+from wagtail import urls as wagtail_urls
+from wagtail.admin import urls as wagtailadmin_urls
+from wagtail.documents import urls as wagtaildocs_urls
+
+
+urlpatterns = [
+    path('django-admin/', admin.site.urls),
+
+    path('admin/', include(wagtailadmin_urls)),
+    path('documents/', include(wagtaildocs_urls)),
+
+    # For anything not caught by a more specific rule above, hand over to
+    # Wagtail's serving mechanism
+    re_path(r'', include(wagtail_urls)),
+]
+
+
+if settings.DEBUG:
+    from django.contrib.staticfiles.urls import staticfiles_urlpatterns
+
+    urlpatterns += staticfiles_urlpatterns() # tell gunicorn where static files are in dev mode
+    urlpatterns += static(settings.MEDIA_URL + 'images/', document_root=os.path.join(settings.MEDIA_ROOT, 'images'))
+    urlpatterns += [
+        path('favicon.ico', RedirectView.as_view(url=settings.STATIC_URL + 'myapp/images/favicon.ico'))
+    ]
+```

+ 0 - 401
docs/advanced_topics/add_to_django_project.rst

@@ -1,401 +0,0 @@
-==================================================
-How to add Wagtail into an existing Django project
-==================================================
-
-To install Wagtail completely from scratch, create a new Django project and an app within that project. For instructions on these tasks, see :doc:`Writing your first Django app <django:intro/tutorial01>`. Your project directory will look like the following::
-
-  myproject/
-      myproject/
-          __init__.py
-          settings.py
-          urls.py
-          wsgi.py
-      myapp/
-          __init__.py
-          models.py
-          tests.py
-          admin.py
-          views.py
-      manage.py
-
-From your app directory, you can safely remove ``admin.py`` and ``views.py``, since Wagtail will provide this functionality for your models. Configuring Django to load Wagtail involves adding modules and variables to ``settings.py`` and URL configuration to ``urls.py``. For a more complete view of what's defined in these files, see :doc:`Django Settings <django:topics/settings>` and :doc:`Django URL Dispatcher <django:topics/http/urls>`.
-
-What follows is a settings reference which skips many boilerplate Django settings. If you just want to get your Wagtail install up quickly without fussing with settings at the moment, see :ref:`complete_example_config`.
-
-
-Middleware (``settings.py``)
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-.. code-block:: python
-
-  MIDDLEWARE = [
-    'django.contrib.sessions.middleware.SessionMiddleware',
-    'django.middleware.common.CommonMiddleware',
-    'django.middleware.csrf.CsrfViewMiddleware',
-    'django.contrib.auth.middleware.AuthenticationMiddleware',
-    'django.contrib.messages.middleware.MessageMiddleware',
-    'django.middleware.clickjacking.XFrameOptionsMiddleware',
-    'django.middleware.security.SecurityMiddleware',
-
-    'wagtail.contrib.redirects.middleware.RedirectMiddleware',
-  ]
-
-Wagtail depends on the default set of Django middleware modules, to cover basic security and functionality such as login sessions. One additional middleware module is provided:
-
-``RedirectMiddleware``
-  Wagtail provides a simple interface for adding arbitrary redirects to your site and this module makes it happen.
-
-
-Apps (``settings.py``)
-~~~~~~~~~~~~~~~~~~~~~~
-
-.. code-block:: python
-
-  INSTALLED_APPS = [
-
-    'myapp',  # your own app
-
-    'wagtail.contrib.forms',
-    'wagtail.contrib.redirects',
-    'wagtail.embeds',
-    'wagtail.sites',
-    'wagtail.users',
-    'wagtail.snippets',
-    'wagtail.documents',
-    'wagtail.images',
-    'wagtail.search',
-    'wagtail.admin',
-    'wagtail',
-
-    'taggit',
-    'modelcluster',
-
-    'django.contrib.auth',
-    'django.contrib.contenttypes',
-    'django.contrib.sessions',
-    'django.contrib.messages',
-    'django.contrib.staticfiles',
-  ]
-
-Wagtail requires several Django app modules, third-party apps, and defines several apps of its own. Wagtail was built to be modular, so many Wagtail apps can be omitted to suit your needs. Your own app (here ``myapp``) is where you define your models, templates, static assets, template tags, and other custom functionality for your site.
-
-
-Wagtail Apps
-------------
-
-``wagtail``
-  The core functionality of Wagtail, such as the ``Page`` class, the Wagtail tree, and model fields.
-
-``wagtail.admin``
-  The administration interface for Wagtail, including page edit handlers.
-
-``wagtail.documents``
-  The Wagtail document content type.
-
-``wagtail.snippets``
-  Editing interface for non-Page models and objects. See :ref:`Snippets`.
-
-``wagtail.users``
-  User editing interface.
-
-``wagtail.images``
-  The Wagtail image content type.
-
-``wagtail.embeds``
-  Module governing oEmbed and Embedly content in Wagtail rich text fields. See :ref:`inserting_videos`.
-
-``wagtail.search``
-  Search framework for Page content. See :ref:`wagtailsearch`.
-
-``wagtail.sites``
-  Management UI for Wagtail sites.
-
-``wagtail.contrib.redirects``
-  Admin interface for creating arbitrary redirects on your site.
-
-``wagtail.contrib.forms``
-  Models for creating forms on your pages and viewing submissions. See :ref:`Form builder <form_builder>`.
-
-
-Third-Party Apps
-----------------
-
-``taggit``
-  Tagging framework for Django. This is used internally within Wagtail for image and document tagging and is available for your own models as well. See :ref:`tagging` for a Wagtail model recipe or the `Taggit Documentation`_.
-
-.. _Taggit Documentation: https://django-taggit.readthedocs.org/en/latest/index.html
-
-``modelcluster``
-  Extension of Django ForeignKey relation functionality, which is used in Wagtail pages for on-the-fly related object creation. For more information, see :ref:`inline_panels` or `the django-modelcluster github project page`_.
-
-.. _the django-modelcluster github project page: https://github.com/torchbox/django-modelcluster
-
-
-URL Patterns
-~~~~~~~~~~~~
-
-.. code-block:: python
-
-  from django.contrib import admin
-
-  from wagtail import urls as wagtail_urls
-  from wagtail.admin import urls as wagtailadmin_urls
-  from wagtail.documents import urls as wagtaildocs_urls
-
-  urlpatterns = [
-      path('django-admin/', admin.site.urls),
-
-      path('admin/', include(wagtailadmin_urls)),
-      path('documents/', include(wagtaildocs_urls)),
-
-      # Optional URL for including your own vanilla Django urls/views
-      re_path(r'', include('myapp.urls')),
-
-      # For anything not caught by a more specific rule above, hand over to
-      # Wagtail's serving mechanism
-      re_path(r'', include(wagtail_urls)),
-  ]
-
-This block of code for your project's ``urls.py`` does a few things:
-
-* Load the vanilla Django admin interface to ``/django-admin/``
-* Load the Wagtail admin and its various apps
-* Dispatch any vanilla Django apps you're using other than Wagtail which require their own URL configuration (this is optional, since Wagtail might be all you need)
-* Lets Wagtail handle any further URL dispatching.
-
-That's not everything you might want to include in your project's URL configuration, but it's what's necessary for Wagtail to flourish.
-
-
-.. _complete_example_config:
-
-Ready to Use Example Configuration Files
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-These two files should reside in your project directory (``myproject/myproject/``).
-
-
-``settings.py``
----------------
-
-.. code-block:: python
-
-  import os
-
-  PROJECT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
-  BASE_DIR = os.path.dirname(PROJECT_DIR)
-
-  DEBUG = True
-
-  # Application definition
-
-  INSTALLED_APPS = [
-      'myapp',
-
-      'wagtail.contrib.forms',
-      'wagtail.contrib.redirects',
-      'wagtail.embeds',
-      'wagtail.sites',
-      'wagtail.users',
-      'wagtail.snippets',
-      'wagtail.documents',
-      'wagtail.images',
-      'wagtail.search',
-      'wagtail.admin',
-      'wagtail',
-
-      'taggit',
-      'modelcluster',
-
-      'django.contrib.auth',
-      'django.contrib.contenttypes',
-      'django.contrib.sessions',
-      'django.contrib.messages',
-      'django.contrib.staticfiles',
-  ]
-
-
-  MIDDLEWARE = [
-      'django.contrib.sessions.middleware.SessionMiddleware',
-      'django.middleware.common.CommonMiddleware',
-      'django.middleware.csrf.CsrfViewMiddleware',
-      'django.contrib.auth.middleware.AuthenticationMiddleware',
-      'django.contrib.messages.middleware.MessageMiddleware',
-      'django.middleware.clickjacking.XFrameOptionsMiddleware',
-      'django.middleware.security.SecurityMiddleware',
-
-      'wagtail.contrib.redirects.middleware.RedirectMiddleware',
-  ]
-
-  ROOT_URLCONF = 'myproject.urls'
-
-  TEMPLATES = [
-      {
-          'BACKEND': 'django.template.backends.django.DjangoTemplates',
-          'DIRS': [
-              os.path.join(PROJECT_DIR, 'templates'),
-          ],
-          'APP_DIRS': True,
-          'OPTIONS': {
-              'context_processors': [
-                  'django.template.context_processors.debug',
-                  'django.template.context_processors.request',
-                  'django.contrib.auth.context_processors.auth',
-                  'django.contrib.messages.context_processors.messages',
-              ],
-          },
-      },
-  ]
-
-  WSGI_APPLICATION = 'myproject.wsgi.application'
-
-  # Database
-
-  DATABASES = {
-      'default': {
-          'ENGINE': 'django.db.backends.postgresql',
-          'NAME': 'myprojectdb',
-          'USER': 'postgres',
-          'PASSWORD': '',
-          'HOST': '',  # Set to empty string for localhost.
-          'PORT': '',  # Set to empty string for default.
-          'CONN_MAX_AGE': 600,  # number of seconds database connections should persist for
-      }
-  }
-
-  # Internationalization
-
-  LANGUAGE_CODE = 'en-us'
-  TIME_ZONE = 'UTC'
-  USE_I18N = True
-  USE_L10N = True
-  USE_TZ = True
-
-
-  # Static files (CSS, JavaScript, Images)
-
-  STATICFILES_FINDERS = [
-      'django.contrib.staticfiles.finders.FileSystemFinder',
-      'django.contrib.staticfiles.finders.AppDirectoriesFinder',
-  ]
-
-  STATICFILES_DIRS = [
-      os.path.join(PROJECT_DIR, 'static'),
-  ]
-
-  STATIC_ROOT = os.path.join(BASE_DIR, 'static')
-  STATIC_URL = '/static/'
-
-  MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
-  MEDIA_URL = '/media/'
-
-
-  ADMINS = [
-      # ('Your Name', 'your_email@example.com'),
-  ]
-  MANAGERS = ADMINS
-
-  # Default to dummy email backend. Configure dev/production/local backend
-  # as per https://docs.djangoproject.com/en/stable/topics/email/#email-backends
-  EMAIL_BACKEND = 'django.core.mail.backends.dummy.EmailBackend'
-
-  # Hosts/domain names that are valid for this site; required if DEBUG is False
-  ALLOWED_HOSTS = []
-
-  # Make this unique, and don't share it with anybody.
-  SECRET_KEY = 'change-me'
-
-  EMAIL_SUBJECT_PREFIX = '[Wagtail] '
-
-  INTERNAL_IPS = ('127.0.0.1', '10.0.2.2')
-
-  # A sample logging configuration. The only tangible logging
-  # performed by this configuration is to send an email to
-  # the site admins on every HTTP 500 error when DEBUG=False.
-  # See https://docs.djangoproject.com/en/stable/topics/logging for
-  # more details on how to customise your logging configuration.
-  LOGGING = {
-      'version': 1,
-      'disable_existing_loggers': False,
-      'filters': {
-          'require_debug_false': {
-              '()': 'django.utils.log.RequireDebugFalse'
-          }
-      },
-      'handlers': {
-          'mail_admins': {
-              'level': 'ERROR',
-              'filters': ['require_debug_false'],
-              'class': 'django.utils.log.AdminEmailHandler'
-          }
-      },
-      'loggers': {
-          'django.request': {
-              'handlers': ['mail_admins'],
-              'level': 'ERROR',
-              'propagate': True,
-          },
-      }
-  }
-
-
-  # WAGTAIL SETTINGS
-
-  # This is the human-readable name of your Wagtail install
-  # which welcomes users upon login to the Wagtail admin.
-  WAGTAIL_SITE_NAME = 'My Project'
-
-  # Replace the search backend
-  #WAGTAILSEARCH_BACKENDS = {
-  #  'default': {
-  #    'BACKEND': 'wagtail.search.backends.elasticsearch5',
-  #    'INDEX': 'myapp'
-  #  }
-  #}
-
-  # Wagtail email notifications from address
-  # WAGTAILADMIN_NOTIFICATION_FROM_EMAIL = 'wagtail@myhost.io'
-
-  # Wagtail email notification format
-  # WAGTAILADMIN_NOTIFICATION_USE_HTML = True
-
-  # Reverse the default case-sensitive handling of tags
-  TAGGIT_CASE_INSENSITIVE = True
-
-
-``urls.py``
------------
-
-.. code-block:: python
-
-  from django.urls import include, path, re_path
-  from django.conf.urls.static import static
-  from django.views.generic.base import RedirectView
-  from django.contrib import admin
-  from django.conf import settings
-  import os.path
-
-  from wagtail import urls as wagtail_urls
-  from wagtail.admin import urls as wagtailadmin_urls
-  from wagtail.documents import urls as wagtaildocs_urls
-
-
-  urlpatterns = [
-      path('django-admin/', admin.site.urls),
-
-      path('admin/', include(wagtailadmin_urls)),
-      path('documents/', include(wagtaildocs_urls)),
-
-      # For anything not caught by a more specific rule above, hand over to
-      # Wagtail's serving mechanism
-      re_path(r'', include(wagtail_urls)),
-  ]
-
-
-  if settings.DEBUG:
-      from django.contrib.staticfiles.urls import staticfiles_urlpatterns
-
-      urlpatterns += staticfiles_urlpatterns() # tell gunicorn where static files are in dev mode
-      urlpatterns += static(settings.MEDIA_URL + 'images/', document_root=os.path.join(settings.MEDIA_ROOT, 'images'))
-      urlpatterns += [
-          path('favicon.ico', RedirectView.as_view(url=settings.STATIC_URL + 'myapp/images/favicon.ico'))
-      ]

+ 334 - 0
docs/advanced_topics/amp.md

@@ -0,0 +1,334 @@
+# How to build a site with AMP support
+
+This recipe document describes a method for creating an
+[AMP](https://amp.dev/) version of a Wagtail site and hosting it separately
+to the rest of the site on a URL prefix. It also describes how to make Wagtail
+render images with the `<amp-img>` tag when a user is visiting a page on the
+AMP version of the site.
+
+## Overview
+
+In the next section, we will add a new URL entry that points at Wagtail's
+internal `serve()` view which will have the effect of rendering the whole
+site again under the `/amp` prefix.
+
+Then, we will add some utilities that will allow us to track whether the
+current request is in the `/amp` prefixed version of the site without needing
+a request object.
+
+After that, we will add a template context processor to allow us to check from
+within templates which version of the site is being rendered.
+
+Then, finally, we will modify the behaviour of the `{% image %}` tag to make it
+render `<amp-img>` tags when rendering the AMP version of the site.
+
+## Creating the second page tree
+
+We can render the whole site at a different prefix by duplicating the Wagtail
+URL in the project `urls.py` file and giving it a prefix. This must be before
+the default URL from Wagtail, or it will try to find `/amp` as a page:
+
+```python
+# <project>/urls.py
+
+urlpatterns += [
+    # Add this line just before the default ``include(wagtail_urls)`` line
+    path('amp/', include(wagtail_urls)),
+
+    path('', include(wagtail_urls)),
+]
+```
+
+If you now open `http://localhost:8000/amp/` in your browser, you should see
+the homepage.
+
+## Making pages aware of "AMP mode"
+
+All the pages will now render under the `/amp` prefix, but right now there
+isn't any difference between the AMP version and the normal version.
+
+To make changes, we need to add a way to detect which URL was used to render
+the page. To do this, we will have to wrap Wagtail's `serve()` view and
+set a thread-local to indicate to all downstream code that AMP mode is active.
+
+```{note}
+Why a thread-local?
+
+(feel free to skip this part if you're not interested)
+
+Modifying the `request` object would be the most common way to do this.
+However, the image tag rendering is performed in a part of Wagtail that
+does not have access to the request.
+
+Thread-locals are global variables that can have a different value for each
+running thread. As each thread only handles one request at a time, we can
+use it as a way to pass around data that is specific to that request
+without having to pass the request object everywhere.
+
+Django uses thread-locals internally to track the currently active language
+for the request.
+
+Python implements thread-local data through the `threading.local` class,
+but as of Django 3.x, multiple requests can be handled in a single thread
+and so thread-locals will no longer be unique to a single request. Django
+therefore provides `asgiref.Local` as a drop-in replacement.
+```
+
+Now let's create that thread-local and some utility functions to interact with it,
+save this module as `amp_utils.py` in an app in your project:
+
+```python
+# <app>/amp_utils.py
+
+from contextlib import contextmanager
+from asgiref.local import Local
+
+_amp_mode_active = Local()
+
+@contextmanager
+def activate_amp_mode():
+    """
+    A context manager used to activate AMP mode
+    """
+    _amp_mode_active.value = True
+    try:
+        yield
+    finally:
+        del _amp_mode_active.value
+
+def amp_mode_active():
+    """
+    Returns True if AMP mode is currently active
+    """
+    return hasattr(_amp_mode_active, 'value')
+```
+
+This module defines two functions:
+
+-   `activate_amp_mode` is a context manager which can be invoked using Python's
+    `with` syntax. In the body of the `with` statement, AMP mode would be active.
+
+-   `amp_mode_active` is a function that returns `True` when AMP mode is active.
+
+Next, we need to define a view that wraps Wagtail's builtin `serve` view and
+invokes the `activate_amp_mode` context manager:
+
+```python
+# <app>/amp_views.py
+
+from django.template.response import SimpleTemplateResponse
+from wagtail.views import serve as wagtail_serve
+
+from .amp_utils import activate_amp_mode
+
+def serve(request, path):
+    with activate_amp_mode():
+        response = wagtail_serve(request, path)
+
+        # Render template responses now while AMP mode is still active
+        if isinstance(response, SimpleTemplateResponse):
+            response.render()
+
+        return response
+```
+
+Then we need to create a `amp_urls.py` file in the same app:
+
+```python
+# <app>/amp_urls.py
+
+from django.urls import re_path
+from wagtail.urls import serve_pattern
+
+from . import amp_views
+
+urlpatterns = [
+    re_path(serve_pattern, amp_views.serve, name='wagtail_amp_serve')
+]
+```
+
+Finally, we need to update the project's main `urls.py` to use this new URLs
+file for the `/amp` prefix:
+
+```python
+# <project>/urls.py
+
+from myapp import amp_urls as wagtail_amp_urls
+
+urlpatterns += [
+    # Change this line to point at your amp_urls instead of Wagtail's urls
+    path('amp/', include(wagtail_amp_urls)),
+
+    re_path(r'', include(wagtail_urls)),
+]
+```
+
+After this, there shouldn't be any noticeable difference to the AMP version of
+the site.
+
+## Write a template context processor so that AMP state can be checked in templates
+
+This is optional, but worth doing so we can confirm that everything is working
+so far.
+
+Add a `amp_context_processors.py` file into your app that contains the
+following:
+
+```python
+# <app>/amp_context_processors.py
+
+from .amp_utils import amp_mode_active
+
+def amp(request):
+    return {
+        'amp_mode_active': amp_mode_active(),
+    }
+```
+
+Now add the path to this context processor to the
+`['OPTIONS']['context_processors']` key of the `TEMPLATES` setting:
+
+```python
+# Either <project>/settings.py or <project>/settings/base.py
+
+TEMPLATES = [
+    {
+        ...
+
+        'OPTIONS': {
+            'context_processors': [
+                ...
+                # Add this after other context processors
+                'myapp.amp_context_processors.amp',
+            ],
+        },
+    },
+]
+```
+
+You should now be able to use the `amp_mode_active` variable in templates.
+For example:
+
+```html+Django
+{% if amp_mode_active %}
+    AMP MODE IS ACTIVE!
+{% endif %}
+```
+
+## Using a different page template when AMP mode is active
+
+You're probably not going to want to use the same templates on the AMP site as
+you do on the normal web site. Let's add some logic in to make Wagtail use a
+separate template whenever a page is served with AMP enabled.
+
+We can use a mixin, which allows us to re-use the logic on different page types.
+Add the following to the bottom of the amp_utils.py file that you created earlier:
+
+```python
+# <app>/amp_utils.py
+
+import os.path
+
+...
+
+class PageAMPTemplateMixin:
+
+    @property
+    def amp_template(self):
+        # Get the default template name and insert `_amp` before the extension
+        name, ext = os.path.splitext(self.template)
+        return name + '_amp' + ext
+
+    def get_template(self, request):
+        if amp_mode_active():
+            return self.amp_template
+
+        return super().get_template(request)
+```
+
+Now add this mixin to any page model, for example:
+
+```python
+# <app>/models.py
+
+from .amp_utils import PageAMPTemplateMixin
+
+class MyPageModel(PageAMPTemplateMixin, Page):
+    ...
+```
+
+When AMP mode is active, the template at `app_label/mypagemodel_amp.html`
+will be used instead of the default one.
+
+If you have a different naming convention, you can override the
+`amp_template` attribute on the model. For example:
+
+```python
+# <app>/models.py
+
+from .amp_utils import PageAMPTemplateMixin
+
+class MyPageModel(PageAMPTemplateMixin, Page):
+    amp_template = 'my_custom_amp_template.html'
+```
+
+## Overriding the `{% image %}` tag to output `<amp-img>` tags
+
+Finally, let's change Wagtail's `{% image %}` tag, so it renders an `<amp-img>`
+tags when rendering pages with AMP enabled. We'll make the change on the
+`Rendition` model itself so it applies to both images rendered with the
+`{% image %}` tag and images rendered in rich text fields as well.
+
+Doing this with a [Custom image model](custom_image_model) is easier, as
+you can override the `img_tag` method on your custom `Rendition` model to
+return a different tag.
+
+For example:
+
+```python
+from django.forms.utils import flatatt
+from django.utils.safestring import mark_safe
+
+from wagtail.images.models import AbstractRendition
+
+...
+
+class CustomRendition(AbstractRendition):
+    def img_tag(self, extra_attributes):
+        attrs = self.attrs_dict.copy()
+        attrs.update(extra_attributes)
+
+        if amp_mode_active():
+            return mark_safe('<amp-img{}>'.format(flatatt(attrs)))
+        else:
+            return mark_safe('<img{}>'.format(flatatt(attrs)))
+
+    ...
+```
+
+Without a custom image model, you will have to monkey-patch the builtin
+`Rendition` model.
+Add this anywhere in your project where it would be imported on start:
+
+```python
+from django.forms.utils import flatatt
+from django.utils.safestring import mark_safe
+
+from wagtail.images.models import Rendition
+
+def img_tag(rendition, extra_attributes={}):
+    """
+    Replacement implementation for Rendition.img_tag
+
+    When AMP mode is on, this returns an <amp-img> tag instead of an <img> tag
+    """
+    attrs = rendition.attrs_dict.copy()
+    attrs.update(extra_attributes)
+
+    if amp_mode_active():
+        return mark_safe('<amp-img{}>'.format(flatatt(attrs)))
+    else:
+        return mark_safe('<img{}>'.format(flatatt(attrs)))
+
+Rendition.img_tag = img_tag
+```

+ 0 - 340
docs/advanced_topics/amp.rst

@@ -1,340 +0,0 @@
-How to build a site with AMP support
-====================================
-
-This recipe document describes a method for creating an
-`AMP <https://amp.dev/>`_ version of a Wagtail site and hosting it separately
-to the rest of the site on a URL prefix. It also describes how to make Wagtail
-render images with the ``<amp-img>`` tag when a user is visiting a page on the
-AMP version of the site.
-
-Overview
---------
-
-In the next section, we will add a new URL entry that points at Wagtail's
-internal ``serve()`` view which will have the effect of rendering the whole
-site again under the ``/amp`` prefix.
-
-Then, we will add some utilities that will allow us to track whether the
-current request is in the ``/amp`` prefixed version of the site without needing
-a request object.
-
-After that, we will add a template context processor to allow us to check from
-within templates which version of the site is being rendered.
-
-Then, finally, we will modify the behaviour of the ``{% image %}`` tag to make it
-render ``<amp-img>`` tags when rendering the AMP version of the site.
-
-Creating the second page tree
------------------------------
-
-We can render the whole site at a different prefix by duplicating the Wagtail
-URL in the project ``urls.py`` file and giving it a prefix. This must be before
-the default URL from Wagtail, or it will try to find ``/amp`` as a page:
-
-.. code-block:: python
-
-    # <project>/urls.py
-
-    urlpatterns += [
-        # Add this line just before the default ``include(wagtail_urls)`` line
-        path('amp/', include(wagtail_urls)),
-
-        path('', include(wagtail_urls)),
-    ]
-
-If you now open ``http://localhost:8000/amp/`` in your browser, you should see
-the homepage.
-
-Making pages aware of "AMP mode"
---------------------------------
-
-All the pages will now render under the ``/amp`` prefix, but right now there
-isn't any difference between the AMP version and the normal version.
-
-To make changes, we need to add a way to detect which URL was used to render
-the page. To do this, we will have to wrap Wagtail's ``serve()`` view and
-set a thread-local to indicate to all downstream code that AMP mode is active.
-
-.. note:: Why a thread-local?
-
-    (feel free to skip this part if you're not interested)
-
-    Modifying the ``request`` object would be the most common way to do this.
-    However, the image tag rendering is performed in a part of Wagtail that
-    does not have access to the request.
-
-    Thread-locals are global variables that can have a different value for each
-    running thread. As each thread only handles one request at a time, we can
-    use it as a way to pass around data that is specific to that request
-    without having to pass the request object everywhere.
-
-    Django uses thread-locals internally to track the currently active language
-    for the request.
-
-    Python implements thread-local data through the ``threading.local`` class,
-    but as of Django 3.x, multiple requests can be handled in a single thread
-    and so thread-locals will no longer be unique to a single request. Django
-    therefore provides ``asgiref.Local`` as a drop-in replacement.
-
-
-Now let's create that thread-local and some utility functions to interact with it,
-save this module as ``amp_utils.py`` in an app in your project:
-
-.. code-block:: python
-
-    # <app>/amp_utils.py
-
-    from contextlib import contextmanager
-    from asgiref.local import Local
-
-    _amp_mode_active = Local()
-
-    @contextmanager
-    def activate_amp_mode():
-        """
-        A context manager used to activate AMP mode
-        """
-        _amp_mode_active.value = True
-        try:
-            yield
-        finally:
-            del _amp_mode_active.value
-
-    def amp_mode_active():
-        """
-        Returns True if AMP mode is currently active
-        """
-        return hasattr(_amp_mode_active, 'value')
-
-This module defines two functions:
-
- - ``activate_amp_mode`` is a context manager which can be invoked using Python's
-   ``with`` syntax. In the body of the ``with`` statement, AMP mode would be active.
-
- - ``amp_mode_active`` is a function that returns ``True`` when AMP mode is active.
-
-Next, we need to define a view that wraps Wagtail's builtin ``serve`` view and
-invokes the ``activate_amp_mode`` context manager:
-
-.. code-block:: python
-
-    # <app>/amp_views.py
-
-    from django.template.response import SimpleTemplateResponse
-    from wagtail.views import serve as wagtail_serve
-
-    from .amp_utils import activate_amp_mode
-
-    def serve(request, path):
-        with activate_amp_mode():
-            response = wagtail_serve(request, path)
-
-            # Render template responses now while AMP mode is still active
-            if isinstance(response, SimpleTemplateResponse):
-                response.render()
-
-            return response
-
-Then we need to create a ``amp_urls.py`` file in the same app:
-
-.. code-block:: python
-
-    # <app>/amp_urls.py
-
-    from django.urls import re_path
-    from wagtail.urls import serve_pattern
-
-    from . import amp_views
-
-    urlpatterns = [
-        re_path(serve_pattern, amp_views.serve, name='wagtail_amp_serve')
-    ]
-
-Finally, we need to update the project's main ``urls.py`` to use this new URLs
-file for the ``/amp`` prefix:
-
-.. code-block:: python
-
-    # <project>/urls.py
-
-    from myapp import amp_urls as wagtail_amp_urls
-
-    urlpatterns += [
-        # Change this line to point at your amp_urls instead of Wagtail's urls
-        path('amp/', include(wagtail_amp_urls)),
-
-        re_path(r'', include(wagtail_urls)),
-    ]
-
-After this, there shouldn't be any noticeable difference to the AMP version of
-the site.
-
-Write a template context processor so that AMP state can be checked in templates
---------------------------------------------------------------------------------
-
-This is optional, but worth doing so we can confirm that everything is working
-so far.
-
-Add a ``amp_context_processors.py`` file into your app that contains the
-following:
-
-.. code-block:: python
-
-    # <app>/amp_context_processors.py
-
-    from .amp_utils import amp_mode_active
-
-    def amp(request):
-        return {
-            'amp_mode_active': amp_mode_active(),
-        }
-
-Now add the path to this context processor to the
-``['OPTIONS']['context_processors']`` key of the ``TEMPLATES`` setting:
-
-.. code-block:: python
-
-    # Either <project>/settings.py or <project>/settings/base.py
-
-    TEMPLATES = [
-        {
-            ...
-
-            'OPTIONS': {
-                'context_processors': [
-                    ...
-                    # Add this after other context processors
-                    'myapp.amp_context_processors.amp',
-                ],
-            },
-        },
-    ]
-
-You should now be able to use the ``amp_mode_active`` variable in templates.
-For example:
-
-.. code-block:: html+Django
-
-    {% if amp_mode_active %}
-        AMP MODE IS ACTIVE!
-    {% endif %}
-
-Using a different page template when AMP mode is active
--------------------------------------------------------
-
-You're probably not going to want to use the same templates on the AMP site as
-you do on the normal web site. Let's add some logic in to make Wagtail use a
-separate template whenever a page is served with AMP enabled.
-
-We can use a mixin, which allows us to re-use the logic on different page types.
-Add the following to the bottom of the amp_utils.py file that you created earlier:
-
-.. code-block:: python
-
-    # <app>/amp_utils.py
-
-    import os.path
-
-    ...
-
-    class PageAMPTemplateMixin:
-
-        @property
-        def amp_template(self):
-            # Get the default template name and insert `_amp` before the extension
-            name, ext = os.path.splitext(self.template)
-            return name + '_amp' + ext
-
-        def get_template(self, request):
-            if amp_mode_active():
-                return self.amp_template
-
-            return super().get_template(request)
-
-Now add this mixin to any page model, for example:
-
-.. code-block:: python
-
-    # <app>/models.py
-
-    from .amp_utils import PageAMPTemplateMixin
-
-    class MyPageModel(PageAMPTemplateMixin, Page):
-        ...
-
-When AMP mode is active, the template at ``app_label/mypagemodel_amp.html``
-will be used instead of the default one.
-
-If you have a different naming convention, you can override the
-``amp_template`` attribute on the model. For example:
-
-.. code-block:: python
-
-    # <app>/models.py
-
-    from .amp_utils import PageAMPTemplateMixin
-
-    class MyPageModel(PageAMPTemplateMixin, Page):
-        amp_template = 'my_custom_amp_template.html'
-
-Overriding the ``{% image %}`` tag to output ``<amp-img>`` tags
----------------------------------------------------------------
-
-Finally, let's change Wagtail's ``{% image %}`` tag, so it renders an ``<amp-img>``
-tags when rendering pages with AMP enabled. We'll make the change on the
-`Rendition` model itself so it applies to both images rendered with the
-``{% image %}`` tag and images rendered in rich text fields as well.
-
-Doing this with a :ref:`Custom image model <custom_image_model>` is easier, as
-you can override the ``img_tag``  method on your custom ``Rendition`` model to
-return a different tag.
-
-For example:
-
-.. code-block:: python
-
-    from django.forms.utils import flatatt
-    from django.utils.safestring import mark_safe
-
-    from wagtail.images.models import AbstractRendition
-
-    ...
-
-    class CustomRendition(AbstractRendition):
-        def img_tag(self, extra_attributes):
-            attrs = self.attrs_dict.copy()
-            attrs.update(extra_attributes)
-
-            if amp_mode_active():
-                return mark_safe('<amp-img{}>'.format(flatatt(attrs)))
-            else:
-                return mark_safe('<img{}>'.format(flatatt(attrs)))
-
-        ...
-
-Without a custom image model, you will have to monkey-patch the builtin
-``Rendition`` model.
-Add this anywhere in your project where it would be imported on start:
-
-.. code-block:: python
-
-    from django.forms.utils import flatatt
-    from django.utils.safestring import mark_safe
-
-    from wagtail.images.models import Rendition
-
-    def img_tag(rendition, extra_attributes={}):
-        """
-        Replacement implementation for Rendition.img_tag
-
-        When AMP mode is on, this returns an <amp-img> tag instead of an <img> tag
-        """
-        attrs = rendition.attrs_dict.copy()
-        attrs.update(extra_attributes)
-
-        if amp_mode_active():
-            return mark_safe('<amp-img{}>'.format(flatatt(attrs)))
-        else:
-            return mark_safe('<img{}>'.format(flatatt(attrs)))
-
-    Rendition.img_tag = img_tag

+ 116 - 0
docs/advanced_topics/boundblocks_and_values.md

@@ -0,0 +1,116 @@
+(boundblocks_and_values)=
+
+# About StreamField BoundBlocks and values
+
+All StreamField block types accept a `template` parameter to determine how they will be rendered on a page. 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, since those built-in types (`str`, `int` and so on) cannot be 'taught' about their template rendering. As an example of this, consider the following block definition:
+
+```python
+class HeadingBlock(blocks.CharBlock):
+    class Meta:
+        template = 'blocks/heading.html'
+```
+
+where `blocks/heading.html` consists of:
+
+```html+django
+<h1>{{ value }}</h1>
+```
+
+This gives us a block that behaves as an ordinary text field, but wraps its output in `<h1>` tags whenever it is rendered:
+
+```python
+class BlogPage(Page):
+    body = StreamField([
+        # ...
+        ('heading', HeadingBlock()),
+        # ...
+    ], use_json_field=True)
+```
+
+```html+django
+{% load wagtailcore_tags %}
+
+{% for block in page.body %}
+    {% if block.block_type == 'heading' %}
+        {% include_block block %}  {# This block will output its own <h1>...</h1> tags. #}
+    {% endif %}
+{% endfor %}
+```
+
+This kind of arrangement - a value that supposedly represents a plain text string, but has its own custom HTML representation when output on a template - would normally be a very messy thing to achieve in Python, but it works here because the items you get when iterating over a StreamField are not actually the 'native' values of the blocks. Instead, each item is returned as an instance of `BoundBlock` - an object that represents the pairing of a value and its block definition. By keeping track of the block definition, a `BoundBlock` always knows which template to render. To get to the underlying value - in this case, the text content of the heading - you would need to access `block.value`. Indeed, if you were to output `{% include_block block.value %}` on the page, you would find that it renders as plain text, without the `<h1>` tags.
+
+(More precisely, the items returned when iterating over a StreamField are instances of a class `StreamChild`, which provides the `block_type` property as well as `value`.)
+
+Experienced Django developers may find it helpful to compare this to the `BoundField` class in Django's forms framework, which represents the pairing of a form field value with its corresponding form field definition, and therefore knows how to render the value as an HTML form field.
+
+Most of the time, you won't need to worry about these internal details; Wagtail will use the template rendering wherever you would expect it to. However, there are certain cases where the illusion isn't quite complete - namely, when accessing children of a `ListBlock` or `StructBlock`. In these cases, there is no `BoundBlock` wrapper, and so the item cannot be relied upon to know its own template rendering. For example, consider the following setup, where our `HeadingBlock` is a child of a StructBlock:
+
+```python
+class EventBlock(blocks.StructBlock):
+    heading = HeadingBlock()
+    description = blocks.TextBlock()
+    # ...
+
+    class Meta:
+        template = 'blocks/event.html'
+```
+
+In `blocks/event.html`:
+
+```html+django
+{% load wagtailcore_tags %}
+
+<div class="event {% if value.heading == 'Party!' %}lots-of-balloons{% endif %}">
+    {% include_block value.heading %}
+    - {% include_block value.description %}
+</div>
+```
+
+In this case, `value.heading` returns the plain string value rather than a `BoundBlock`; this is necessary because otherwise the comparison in `{% if value.heading == 'Party!' %}` would never succeed. This in turn means that `{% include_block value.heading %}` renders as the plain string, without the `<h1>` tags. To get the HTML rendering, you need to explicitly access the `BoundBlock` instance through `value.bound_blocks.heading`:
+
+```html+django
+{% load wagtailcore_tags %}
+
+<div class="event {% if value.heading == 'Party!' %}lots-of-balloons{% endif %}">
+    {% include_block value.bound_blocks.heading %}
+    - {% include_block value.description %}
+</div>
+```
+
+In practice, it would probably be more natural and readable to make the `<h1>` tag explicit in the EventBlock's template:
+
+```html+django
+{% load wagtailcore_tags %}
+
+<div class="event {% if value.heading == 'Party!' %}lots-of-balloons{% endif %}">
+    <h1>{{ value.heading }}</h1>
+    - {% include_block value.description %}
+</div>
+```
+
+This limitation does not apply to StructBlock and StreamBlock values as children of a StructBlock, because Wagtail implements these as complex objects that know their own template rendering, even when not wrapped in a `BoundBlock`. For example, if a StructBlock is nested in another StructBlock, as in:
+
+```python
+class EventBlock(blocks.StructBlock):
+    heading = HeadingBlock()
+    description = blocks.TextBlock()
+    guest_speaker = blocks.StructBlock([
+        ('first_name', blocks.CharBlock()),
+        ('surname', blocks.CharBlock()),
+        ('photo', ImageChooserBlock()),
+    ], template='blocks/speaker.html')
+```
+
+then `{% include_block value.guest_speaker %}` within the EventBlock's template will pick up the template rendering from `blocks/speaker.html` as intended.
+
+In summary, interactions between BoundBlocks and plain values work according to the following rules:
+
+1. When iterating over the value of a StreamField or StreamBlock (as in `{% for block in page.body %}`), you will get back a sequence of BoundBlocks.
+2. If you have a BoundBlock instance, you can access the plain value as `block.value`.
+3. Accessing a child of a StructBlock (as in `value.heading`) will return a plain value; to retrieve the BoundBlock instead, use `value.bound_blocks.heading`.
+4. Likewise, accessing children of a ListBlock (e.g. `for item in value`) will return plain values; to retrieve BoundBlocks instead, use `value.bound_blocks`.
+5. StructBlock and StreamBlock values always know how to render their own templates, even if you only have the plain value rather than the BoundBlock.
+
+```{versionchanged} 2.16
+The value of a ListBlock now provides a `bound_blocks` property; previously it was a plain Python list of child values.
+```

+ 0 - 117
docs/advanced_topics/boundblocks_and_values.rst

@@ -1,117 +0,0 @@
-.. _boundblocks_and_values:
-
-About StreamField BoundBlocks and values
-----------------------------------------
-
-All StreamField block types accept a ``template`` parameter to determine how they will be rendered on a page. 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, since those built-in types (``str``, ``int`` and so on) cannot be 'taught' about their template rendering. As an example of this, consider the following block definition:
-
-.. code-block:: python
-
-    class HeadingBlock(blocks.CharBlock):
-        class Meta:
-            template = 'blocks/heading.html'
-
-where ``blocks/heading.html`` consists of:
-
-.. code-block:: html+django
-
-    <h1>{{ value }}</h1>
-
-This gives us a block that behaves as an ordinary text field, but wraps its output in ``<h1>`` tags whenever it is rendered:
-
-.. code-block:: python
-
-    class BlogPage(Page):
-        body = StreamField([
-            # ...
-            ('heading', HeadingBlock()),
-            # ...
-        ], use_json_field=True)
-
-.. code-block:: html+django
-
-    {% load wagtailcore_tags %}
-
-    {% for block in page.body %}
-        {% if block.block_type == 'heading' %}
-            {% include_block block %}  {# This block will output its own <h1>...</h1> tags. #}
-        {% endif %}
-    {% endfor %}
-
-This kind of arrangement - a value that supposedly represents a plain text string, but has its own custom HTML representation when output on a template - would normally be a very messy thing to achieve in Python, but it works here because the items you get when iterating over a StreamField are not actually the 'native' values of the blocks. Instead, each item is returned as an instance of ``BoundBlock`` - an object that represents the pairing of a value and its block definition. By keeping track of the block definition, a ``BoundBlock`` always knows which template to render. To get to the underlying value - in this case, the text content of the heading - you would need to access ``block.value``. Indeed, if you were to output ``{% include_block block.value %}`` on the page, you would find that it renders as plain text, without the ``<h1>`` tags.
-
-(More precisely, the items returned when iterating over a StreamField are instances of a class ``StreamChild``, which provides the ``block_type`` property as well as ``value``.)
-
-Experienced Django developers may find it helpful to compare this to the ``BoundField`` class in Django's forms framework, which represents the pairing of a form field value with its corresponding form field definition, and therefore knows how to render the value as an HTML form field.
-
-Most of the time, you won't need to worry about these internal details; Wagtail will use the template rendering wherever you would expect it to. However, there are certain cases where the illusion isn't quite complete - namely, when accessing children of a ``ListBlock`` or ``StructBlock``. In these cases, there is no ``BoundBlock`` wrapper, and so the item cannot be relied upon to know its own template rendering. For example, consider the following setup, where our ``HeadingBlock`` is a child of a StructBlock:
-
-.. code-block:: python
-
-    class EventBlock(blocks.StructBlock):
-        heading = HeadingBlock()
-        description = blocks.TextBlock()
-        # ...
-
-        class Meta:
-            template = 'blocks/event.html'
-
-In ``blocks/event.html``:
-
-.. code-block:: html+django
-
-    {% load wagtailcore_tags %}
-
-    <div class="event {% if value.heading == 'Party!' %}lots-of-balloons{% endif %}">
-        {% include_block value.heading %}
-        - {% include_block value.description %}
-    </div>
-
-In this case, ``value.heading`` returns the plain string value rather than a ``BoundBlock``; this is necessary because otherwise the comparison in ``{% if value.heading == 'Party!' %}`` would never succeed. This in turn means that ``{% include_block value.heading %}`` renders as the plain string, without the ``<h1>`` tags. To get the HTML rendering, you need to explicitly access the ``BoundBlock`` instance through ``value.bound_blocks.heading``:
-
-.. code-block:: html+django
-
-    {% load wagtailcore_tags %}
-
-    <div class="event {% if value.heading == 'Party!' %}lots-of-balloons{% endif %}">
-        {% include_block value.bound_blocks.heading %}
-        - {% include_block value.description %}
-    </div>
-
-In practice, it would probably be more natural and readable to make the ``<h1>`` tag explicit in the EventBlock's template:
-
-.. code-block:: html+django
-
-    {% load wagtailcore_tags %}
-
-    <div class="event {% if value.heading == 'Party!' %}lots-of-balloons{% endif %}">
-        <h1>{{ value.heading }}</h1>
-        - {% include_block value.description %}
-    </div>
-
-This limitation does not apply to StructBlock and StreamBlock values as children of a StructBlock, because Wagtail implements these as complex objects that know their own template rendering, even when not wrapped in a ``BoundBlock``. For example, if a StructBlock is nested in another StructBlock, as in:
-
-.. code-block:: python
-
-    class EventBlock(blocks.StructBlock):
-        heading = HeadingBlock()
-        description = blocks.TextBlock()
-        guest_speaker = blocks.StructBlock([
-            ('first_name', blocks.CharBlock()),
-            ('surname', blocks.CharBlock()),
-            ('photo', ImageChooserBlock()),
-        ], template='blocks/speaker.html')
-
-then ``{% include_block value.guest_speaker %}`` within the EventBlock's template will pick up the template rendering from ``blocks/speaker.html`` as intended.
-
-In summary, interactions between BoundBlocks and plain values work according to the following rules:
-
-1. When iterating over the value of a StreamField or StreamBlock (as in ``{% for block in page.body %}``), you will get back a sequence of BoundBlocks.
-2. If you have a BoundBlock instance, you can access the plain value as ``block.value``.
-3. Accessing a child of a StructBlock (as in ``value.heading``) will return a plain value; to retrieve the BoundBlock instead, use ``value.bound_blocks.heading``.
-4. Likewise, accessing children of a ListBlock (e.g. ``for item in value``) will return plain values; to retrieve BoundBlocks instead, use ``value.bound_blocks``.
-5. StructBlock and StreamBlock values always know how to render their own templates, even if you only have the plain value rather than the BoundBlock.
-
-.. versionchanged:: 2.16
-
-  The value of a ListBlock now provides a ``bound_blocks`` property; previously it was a plain Python list of child values.

+ 117 - 0
docs/advanced_topics/deploying.md

@@ -0,0 +1,117 @@
+# Deploying Wagtail
+
+## On your server
+
+Wagtail is straightforward to deploy on modern Linux-based distributions, and should run with any of the combinations detailed in Django's [deployment documentation](django:howto/deployment/index).
+See the section on [performance](performance) for the non-Python services we recommend.
+
+## On Divio Cloud
+
+[Divio Cloud](https://divio.com/) is a Dockerised cloud hosting platform for Python/Django that allows you to launch and deploy Wagtail projects in minutes.
+With a free account, you can create a Wagtail project. Choose from a:
+
+-   [site based on the Wagtail Bakery project](https://divio.com/wagtail), or
+-   [brand new Wagtail project](https://control.divio.com/control/project/create) (see the [how to get started notes](https://docs.divio.com/en/latest/introduction/wagtail/)).
+
+Divio Cloud also hosts a [live Wagtail Bakery demo](https://divio.com/wagtail) (no account required).
+
+## On PythonAnywhere
+
+[PythonAnywhere](https://www.pythonanywhere.com/) is a Platform-as-a-Service (PaaS) focused on Python hosting and development.
+It allows developers to quickly develop, host, and scale applications in a cloud environment.
+Starting with a free plan they also provide MySQL and PostgreSQL databases as well as very flexible and affordable paid plans, so there's all you need to host a Wagtail site.
+To get quickly up and running you may use the [wagtail-pythonanywhere-quickstart](https://github.com/texperience/wagtail-pythonanywhere-quickstart).
+
+## On Google Cloud
+
+[Google Cloud](https://cloud.google.com) is an Infrastructure-as-a-Service (IaaS) that offers multiple managed products, supported by Python client libraries, to help you build, deploy, and monitor your applications.
+You can deploy Wagtail, or any Django application, in a number of ways, including on [App Engine](https://www.youtube.com/watch?v=uD9PTag2-PQ) or [Cloud Run](https://codelabs.developers.google.com/codelabs/cloud-run-wagtail/#0).
+
+## On alwaysdata
+
+[alwaysdata](https://www.alwaysdata.com/) is a Platform-as-a-Service (PaaS) providing Public and Private Cloud offers.
+Starting with a free plan they provide MySQL/PostgreSQL databases, emails, free SSL certificates, included backups, etc.
+
+To get your Wagtail application running you may:
+
+-   [Install Wagtail from alwaysdata Marketplace](https://www.alwaysdata.com/en/marketplace/wagtail/)
+-   [Configure a Django application](https://help.alwaysdata.com/en/languages/python/django/)
+
+## On other PAASs and IAASs
+
+We know of Wagtail sites running on [Heroku](https://spapas.github.io/2014/02/13/wagtail-tutorial/), Digital Ocean and elsewhere.
+If you have successfully installed Wagtail on your platform or infrastructure, please [contribute](../contributing/index) your notes to this documentation!
+
+(deployment_tips)=
+
+## Deployment tips
+
+### Static files
+
+As with all Django projects, static files are not served by the Django application server in production
+(i.e. outside of the `manage.py runserver` command); these need to be handled separately at the web server level.
+See [Django's documentation on deploying static files](django:howto/static-files/deployment).
+
+The JavaScript and CSS files used by the Wagtail admin frequently change between releases of Wagtail - it's important to avoid serving outdated versions of these files due to browser or server-side caching, as this can cause hard-to-diagnose issues.
+We recommend enabling [ManifestStaticFilesStorage](django.contrib.staticfiles.storage.ManifestStaticFilesStorage) in the `STATICFILES_STORAGE` setting - this ensures that different versions of files are assigned distinct URLs.
+
+### User Uploaded Files
+
+Wagtail follows [Django's conventions for managing uploaded files](django:topics/files).
+So by default, Wagtail uses Django's built-in `FileSystemStorage` class which stores files on your site's server, in the directory specified by the `MEDIA_ROOT` setting.
+Alternatively, Wagtail can be configured to store uploaded images and documents on a cloud storage service such as Amazon S3;
+this is done through the [DEFAULT_FILE_STORAGE](https://docs.djangoproject.com/en/stable/ref/settings/#std:setting-DEFAULT_FILE_STORAGE)
+setting in conjunction with an add-on package such as [django-storages](https://django-storages.readthedocs.io/).
+
+When using `FileSystemStorage`, image urls are constructed starting from the path specified by the `MEDIA_URL`.
+In most cases, you should configure your web server to serve image files directly (without passing through Django/Wagtail).
+When using one of the cloud storage backends, images urls go directly to the cloud storage file url.
+If you would like to serve your images from a separate asset server or CDN, you can [configure the image serve view](image_serve_view_redirect_action) to redirect instead.
+
+Document serving is controlled by the [WAGTAILDOCS_SERVE_METHOD](wagtaildocs_serve_method) method.
+When using `FileSystemStorage`, documents are stored in a `documents` subdirectory within your site's `MEDIA_ROOT`.
+If all your documents are public, you can set the `WAGTAILDOCS_SERVE_METHOD` to `direct` and configure your web server to serve the files itself.
+However, if you use Wagtail's [Collection Privacy settings](collection_privacy_settings) to restrict access to some or all of your documents, you may or may not want to configure your web server to serve the documents directly.
+The default setting is `redirect` which allows Wagtail to perform any configured privacy checks before offloading serving the actual document to your web server or CDN.
+This means that Wagtail constructs document links that pass through Wagtail, but the final url in the user's browser is served directly by your web server.
+If a user bookmarks this url, they will be able to access the file without passing through Wagtail's privacy checks.
+If this is not acceptable, you may want to set the `WAGTAILDOCS_SERVE_METHOD` to `serve_view` and configure your web server so it will not serve document files itself.
+If you are serving documents from the cloud and need to enforce privacy settings, you should make sure the documents are not publicly accessible using the cloud service's file url.
+
+### Cloud storage
+
+Be aware that setting up remote storage will not entirely offload file handling tasks from the application server - some Wagtail functionality requires files to be read back by the application server.
+In particular, original image files need to be read back whenever a new resized rendition is created, and documents may be configured to be served through a Django view in order to enforce permission checks (see [WAGTAILDOCS_SERVE_METHOD](wagtaildocs_serve_method)).
+
+Note that the django-storages Amazon S3 backends (`storages.backends.s3boto.S3BotoStorage` and `storages.backends.s3boto3.S3Boto3Storage`) **do not correctly handle duplicate filenames** in their default configuration. When using these backends, `AWS_S3_FILE_OVERWRITE` must be set to `False`.
+
+If you are also serving Wagtail's static files from remote storage (using Django's [STATICFILES_STORAGE](https://docs.djangoproject.com/en/stable/ref/settings/#std:setting-STATICFILES_STORAGE) setting), you'll need to ensure that it is configured to serve [CORS HTTP headers](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS), as current browsers will reject remotely-hosted font files that lack a valid header. For Amazon S3, refer to the documentation [Setting Bucket and Object Access Permissions](https://docs.aws.amazon.com/AmazonS3/latest/user-guide/set-permissions.html), or (for the `storages.backends.s3boto.S3Boto3Storage` backend only) add the following to your Django settings:
+
+```python
+AWS_S3_OBJECT_PARAMETERS = {
+    "ACL": "public-read"
+}
+```
+
+The `ACL` parameter accepts a list of predefined configurations for Amazon S3. For more information, refer to the documentation [Canned ACL](https://docs.aws.amazon.com/AmazonS3/latest/dev/acl-overview.html#canned-acl).
+
+For Google Cloud Storage, create a `cors.json` configuration:
+
+```json
+[
+    {
+        "origin": ["*"],
+        "responseHeader": ["Content-Type"],
+        "method": ["GET"],
+        "maxAgeSeconds": 3600
+    }
+]
+```
+
+Then, apply this CORS configuration to the storage bucket:
+
+```sh
+gsutil cors set cors.json gs://$GS_BUCKET_NAME
+```
+
+For other storage services, refer to your provider's documentation, or the documentation for the Django storage backend library you're using.

+ 0 - 102
docs/advanced_topics/deploying.rst

@@ -1,102 +0,0 @@
-Deploying Wagtail
------------------
-
-On your server
-~~~~~~~~~~~~~~
-
-Wagtail is straightforward to deploy on modern Linux-based distributions, and should run with any of the combinations detailed in Django's :doc:`deployment documentation <django:howto/deployment/index>`. See the section on :doc:`performance </advanced_topics/performance>` for the non-Python services we recommend.
-
-On Divio Cloud
-~~~~~~~~~~~~~~
-
-`Divio Cloud <https://divio.com/>`_ is a Dockerised cloud hosting platform for Python/Django that allows you to launch and deploy Wagtail projects in minutes. With a free account, you can create a Wagtail project. Choose from a:
-
-* `site based on the Wagtail Bakery project <https://divio.com/wagtail>`_, or
-* `brand new Wagtail project <https://control.divio.com/control/project/create>`_ (see the `how to get started notes <https://docs.divio.com/en/latest/introduction/wagtail/>`_).
-
-Divio Cloud also hosts a `live Wagtail Bakery demo <https://divio.com/wagtail>`_ (no account required).
-
-On PythonAnywhere
-~~~~~~~~~~~~~~~~~
-
-`PythonAnywhere <https://www.pythonanywhere.com/>`_ is a Platform-as-a-Service (PaaS) focused on Python hosting and development. It allows developers to quickly develop, host, and scale applications in a cloud environment. Starting with a free plan they also provide MySQL and PostgreSQL databases as well as very flexible and affordable paid plans, so there's all you need to host a Wagtail site. To get quickly up and running you may use the `wagtail-pythonanywhere-quickstart <https://github.com/texperience/wagtail-pythonanywhere-quickstart>`_.
-
-On Google Cloud
-~~~~~~~~~~~~~~~
-
-`Google Cloud <https://cloud.google.com>`_ is an Infrastructure-as-a-Service (IaaS) that offers multiple managed products, supported by Python client libraries, to help you build, deploy, and monitor your applications. You can deploy Wagtail, or any Django application, in a number of ways, including on `App Engine <https://www.youtube.com/watch?v=uD9PTag2-PQ>`_  or `Cloud Run <https://codelabs.developers.google.com/codelabs/cloud-run-wagtail/#0>`_.
-
-On alwaysdata
-~~~~~~~~~~~~~
-
-`alwaysdata <https://www.alwaysdata.com/>`_ is a Platform-as-a-Service (PaaS) providing Public and Private Cloud offers. Starting with a free plan they provide MySQL/PostgreSQL databases, emails, free SSL certificates, included backups, etc.
-
-To get your Wagtail application running you may:
-
-* `Install Wagtail from alwaysdata Marketplace <https://www.alwaysdata.com/en/marketplace/wagtail/>`_
-* `Configure a Django application <https://help.alwaysdata.com/en/languages/python/django/>`_
-
-On other PAASs and IAASs
-~~~~~~~~~~~~~~~~~~~~~~~~
-
-We know of Wagtail sites running on `Heroku <https://spapas.github.io/2014/02/13/wagtail-tutorial/>`_, Digital Ocean and elsewhere. If you have successfully installed Wagtail on your platform or infrastructure, please :doc:`contribute </contributing/index>` your notes to this documentation!
-
-.. _deployment_tips:
-
-Deployment tips
-~~~~~~~~~~~~~~~
-
-Static files
-++++++++++++
-
-As with all Django projects, static files are not served by the Django application server in production (i.e. outside of the ``manage.py runserver`` command); these need to be handled separately at the web server level. See :doc:`Django's documentation on deploying static files <django:howto/static-files/deployment>`.
-
-The JavaScript and CSS files used by the Wagtail admin frequently change between releases of Wagtail - it's important to avoid serving outdated versions of these files due to browser or server-side caching, as this can cause hard-to-diagnose issues. We recommend enabling :class:`~django.contrib.staticfiles.storage.ManifestStaticFilesStorage` in the ``STATICFILES_STORAGE`` setting - this ensures that different versions of files are assigned distinct URLs.
-
-User Uploaded Files
-+++++++++++++++++++
-
-Wagtail follows :doc:`Django's conventions for managing uploaded files <django:topics/files>`. So by default, Wagtail uses Django's built-in ``FileSystemStorage`` class which stores files on your site's server, in the directory specified by the ``MEDIA_ROOT`` setting. Alternatively, Wagtail can be configured to store uploaded images and documents on a cloud storage service such as Amazon S3; this is done through the `DEFAULT_FILE_STORAGE <https://docs.djangoproject.com/en/stable/ref/settings/#std:setting-DEFAULT_FILE_STORAGE>`_ setting in conjunction with an add-on package such as `django-storages <https://django-storages.readthedocs.io/>`_.
-
-When using ``FileSystemStorage``, image urls are constructed starting from the path specified by the ``MEDIA_URL``. In most cases, you should configure your web server to serve image files directly (without passing through Django/Wagtail). When using one of the cloud storage backends, images urls go directly to the cloud storage file url. If you would like to serve your images from a separate asset server or CDN, you can :ref:`configure the image serve view <image_serve_view_redirect_action>` to redirect instead.
-
-Document serving is controlled by the :ref:`WAGTAILDOCS_SERVE_METHOD <wagtaildocs_serve_method>` method. When using ``FileSystemStorage``, documents are stored in a ``documents`` subdirectory within your site's ``MEDIA_ROOT``. If all your documents are public, you can set the ``WAGTAILDOCS_SERVE_METHOD`` to ``direct`` and configure your web server to serve the files itself. However, if you use  Wagtail's :ref:`Collection Privacy settings <collection_privacy_settings>` to restrict access to some or all of your documents, you may or may not want to configure your web server to serve the documents directly. The default setting is ``redirect`` which allows Wagtail to perform any configured privacy checks before offloading serving the actual document to your web server or CDN. This means that Wagtail constructs document links that pass through Wagtail, but the final url in the user's browser is served directly by your web server. If a user bookmarks this url, they will be able to access the file without passing through Wagtail's privacy checks. If this is not acceptable, you may want to set the ``WAGTAILDOCS_SERVE_METHOD`` to ``serve_view`` and configure your web server so it will not serve document files itself. If you are serving documents from the cloud and need to enforce privacy settings, you should make sure the documents are not publicly accessible using the cloud service's file url.
-
-Cloud storage
-+++++++++++++
-
-Be aware that setting up remote storage will not entirely offload file handling tasks from the application server - some Wagtail functionality requires files to be read back by the application server. In particular, original image files need to be read back whenever a new resized rendition is created, and documents may be configured to be served through a Django view in order to enforce permission checks (see :ref:`WAGTAILDOCS_SERVE_METHOD <wagtaildocs_serve_method>`).
-
-Note that the django-storages Amazon S3 backends (``storages.backends.s3boto.S3BotoStorage`` and ``storages.backends.s3boto3.S3Boto3Storage``) **do not correctly handle duplicate filenames** in their default configuration. When using these backends, ``AWS_S3_FILE_OVERWRITE`` must be set to ``False``.
-
-If you are also serving Wagtail's static files from remote storage (using Django's `STATICFILES_STORAGE <https://docs.djangoproject.com/en/stable/ref/settings/#std:setting-STATICFILES_STORAGE>`_ setting), you'll need to ensure that it is configured to serve `CORS HTTP headers <https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS>`_, as current browsers will reject remotely-hosted font files that lack a valid header. For Amazon S3, refer to the documentation `Setting Bucket and Object Access Permissions <https://docs.aws.amazon.com/AmazonS3/latest/user-guide/set-permissions.html>`_, or (for the ``storages.backends.s3boto.S3Boto3Storage`` backend only) add the following to your Django settings:
-
-.. code-block:: python
-
-    AWS_S3_OBJECT_PARAMETERS = {
-        "ACL": "public-read"
-    }
-
-The ``ACL`` parameter accepts a list of predefined configurations for Amazon S3. For more information, refer to the documentation `Canned ACL <https://docs.aws.amazon.com/AmazonS3/latest/dev/acl-overview.html#canned-acl>`_.
-
-For Google Cloud Storage, create a ``cors.json`` configuration:
-
-.. code-block:: json
-
-    [
-        {
-          "origin": ["*"],
-          "responseHeader": ["Content-Type"],
-          "method": ["GET"],
-          "maxAgeSeconds": 3600
-        }
-    ]
-
-Then, apply this CORS configuration to the storage bucket:
-
-.. code-block:: shell
-
-    gsutil cors set cors.json gs://$GS_BUCKET_NAME
-
-
-For other storage services, refer to your provider's documentation, or the documentation for the Django storage backend library you're using.

+ 660 - 0
docs/advanced_topics/i18n.md

@@ -0,0 +1,660 @@
+# Internationalisation
+
+```{contents}
+---
+local:
+depth: 3
+---
+```
+
+(multi_language_content)=
+
+## Multi-language content
+
+### Overview
+
+Out of the box, Wagtail assumes all content will be authored in a single language.
+This document describes how to configure Wagtail for authoring content in
+multiple languages.
+
+```{note}
+Wagtail provides the infrastructure for creating and serving content in multiple languages.
+There are two options for managing translations across different languages in the admin interface:
+[wagtail.contrib.simple_translation](simple_translation) or the more advanced [wagtail-localize](https://github.com/wagtail/wagtail-localize) (third-party package).
+```
+
+This document only covers the internationalisation of content managed by Wagtail.
+For information on how to translate static content in template files, JavaScript
+code, etc, refer to the [Django internationalisation docs](https://docs.djangoproject.com/en/3.1/topics/i18n/translation/).
+Or, if you are building a headless site, refer to the docs of the frontend framework you are using.
+
+### Wagtail's approach to multi-lingual content
+
+This section provides an explanation of Wagtail's internationalisation approach.
+If you're in a hurry, you can skip to [](Configuration).
+
+In summary:
+
+-   Wagtail stores content in a separate page tree for each locale
+-   It has a built-in `Locale` model and all pages are linked to a `Locale` with the `locale` foreign key field
+-   It records which pages are translations of each other using a shared UUID stored in the `translation_key` field
+-   It automatically routes requests through translations of the site's homepage
+-   It uses Django's `i18n_patterns` and `LocaleMiddleware` for language detection
+
+#### Page structure
+
+Wagtail stores content in a separate page tree for each locale.
+
+For example, if you have two sites in two locales, then you will see four
+homepages at the top level of the page hierarchy in the explorer.
+
+This approach has some advantages for the editor experience as well:
+
+-   There is no default language for editing, so content can be authored in any
+    language and then translated to any other.
+-   Translations of a page are separate pages so they can be published at
+    different times.
+-   Editors can be given permission to edit content in one locale and not others.
+
+#### How locales and translations are recorded in the database
+
+All pages (and any snippets that have translation enabled) have a `locale` and
+`translation_key` field:
+
+-   `locale` is a foreign key to the `Locale` model
+-   `translation_key` is a UUID that's used to find translations of a piece of content.
+    Translations of the same page/snippet share the same value in this field
+
+These two fields have a 'unique together' constraint so you can't have more than
+one translation in the same locale.
+
+#### Translated homepages
+
+When you set up a site in Wagtail, you select the site's homepage in the 'root page'
+field and all requests to that site's root URL will be routed to that page.
+
+Multi-lingual sites have a separate homepage for each locale that exist as siblings
+in the page tree. Wagtail finds the other homepages by looking for translations of
+the site's 'root page'.
+
+This means that to make a site available in another locale, you just need to
+translate and publish its homepage in that new locale.
+
+If Wagtail can't find a homepage that matches the user's language, it will fall back
+to the page that is selected as the 'root page' on the site record, so you can use
+this field to specify the default language of your site.
+
+#### Language detection and routing
+
+For detecting the user's language and adding a prefix to the URLs
+(`/en/`, `/fr-fr/`, for example), Wagtail is designed to work with Django's
+built-in internationalisation utilities such as `i18n_patterns` and
+`LocaleMiddleware`. This means that Wagtail should work seamlessly with any
+other internationalised Django applications on your site.
+
+#### Locales
+
+The locales that are enabled on a site are recorded in the `Locale` model in
+`wagtailcore`. This model has just two fields: ID and `language_code` which
+stores the [BCP-47 language tag](https://en.wikipedia.org/wiki/IETF_language_tag)
+that represents this locale.
+
+The locale records can be set up with an [optional management UI](enabling_locale_management) or created
+in the shell. The possible values of the `language_code` field are controlled
+by the `WAGTAIL_CONTENT_LANGUAGES` setting.
+
+```{note}
+Read this if you've changed ``LANGUAGE_CODE`` before enabling internationalisation
+
+On initial migration, Wagtail creates a ``Locale`` record for the language that
+was set in the ``LANGUAGE_CODE`` setting at the time the migration was run. All
+pages will be assigned to this ``Locale`` when Wagtail's internationalisation is disabled.
+
+If you have changed the ``LANGUAGE_CODE`` setting since updating to Wagtail 2.11,
+you will need to manually update the record in the ``Locale`` model too before
+enabling internationalisation, as your existing content will be assigned to the old code.
+```
+
+(configuration)=
+
+### Configuration
+
+In this section, we will go through the minimum configuration required to enable
+content to be authored in multiple languages.
+
+```{contents}
+---
+local:
+depth: 1
+---
+```
+
+(enabling_internationalisation)=
+
+#### Enabling internationalisation
+
+To enable internationalisation in both Django and Wagtail, set the following
+settings to `True`:
+
+```python
+# my_project/settings.py
+
+USE_I18N = True
+WAGTAIL_I18N_ENABLED = True
+```
+
+In addition, you might also want to enable Django's localisation support. This
+will make dates and numbers display in the user's local format:
+
+```python
+# my_project/settings.py
+
+USE_L10N = True
+```
+
+(configuring_available_languages)=
+
+#### Configuring available languages
+
+Next we need to configure the available languages. There are two settings
+for this that are each used for different purposes:
+
+-   `LANGUAGES` - This sets which languages are available on the frontend of the site.
+-   `WAGTAIL_CONTENT_LANGUAGES` - This sets which the languages Wagtail content
+    can be authored in.
+
+You can set both of these settings to the exact same value. For example, to
+enable English, French, and Spanish:
+
+```python
+# my_project/settings.py
+
+WAGTAIL_CONTENT_LANGUAGES = LANGUAGES = [
+    ('en', "English"),
+    ('fr', "French"),
+    ('es', "Spanish"),
+]
+```
+
+```{note}
+Whenever ``WAGTAIL_CONTENT_LANGUAGES`` is changed, the ``Locale`` model needs
+to be updated as well to match.
+
+This can either be done with a data migration or with the optional locale
+management UI described in the next section.
+```
+
+You can also set these to different values. You might want to do this if you
+want to have some programmatic localisation (like date formatting or currency,
+for example) but use the same Wagtail content in multiple regions:
+
+```python
+# my_project/settings.py
+
+LANGUAGES = [
+    ('en-GB', "English (Great Britain)"),
+    ('en-US', "English (United States)"),
+    ('en-CA', "English (Canada)"),
+    ('fr-FR', "French (France)"),
+    ('fr-CA', "French (Canada)"),
+]
+
+WAGTAIL_CONTENT_LANGUAGES = [
+    ('en-GB', "English"),
+    ('fr-FR', "French"),
+]
+```
+
+When configured like this, the site will be available in all the different
+locales in the first list, but there will only be two language trees in
+Wagtail.
+
+All the `en-` locales will use the "English" language tree, and the `fr-`
+locales will use the "French" language tree. The differences between each locale
+in a language would be programmatic. For example: which date/number format to
+use, and what currency to display prices in.
+
+(enabling_locale_management)=
+
+#### Enabling the locale management UI (optional)
+
+An optional locale management app exists to allow a Wagtail administrator to
+set up the locales from the Wagtail admin interface.
+
+To enable it, add `wagtail.locales` into `INSTALLED_APPS`:
+
+```python
+# my_project/settings.py
+
+INSTALLED_APPS = [
+    # ...
+    'wagtail.locales',
+    # ...
+]
+```
+
+#### Adding a language prefix to URLs
+
+To allow all of the page trees to be served at the same domain, we need
+to add a URL prefix for each language.
+
+To implement this, we can use Django's built-in
+[i18n_patterns](https://docs.djangoproject.com/en/3.1/topics/i18n/translation/#language-prefix-in-url-patterns)
+function, which adds a language prefix to all of the URL patterns passed into it.
+This activates the language code specified in the URL and Wagtail takes this into
+account when it decides how to route the request.
+
+In your project's `urls.py` add Wagtail's core URLs (and any other URLs you
+want to be translated) into an `i18n_patterns` block:
+
+```python
+# /my_project/urls.py
+
+...
+
+from django.conf.urls.i18n import i18n_patterns
+
+# Non-translatable URLs
+# Note: if you are using the Wagtail API or sitemaps,
+# these should not be added to `i18n_patterns` either
+urlpatterns = [
+    path('django-admin/', admin.site.urls),
+
+    path('admin/', include(wagtailadmin_urls)),
+    path('documents/', include(wagtaildocs_urls)),
+]
+
+# Translatable URLs
+# These will be available under a language code prefix. For example /en/search/
+urlpatterns += i18n_patterns(
+    path('search/', search_views.search, name='search'),
+    path("", include(wagtail_urls)),
+)
+```
+
+#### User language auto-detection
+
+After wrapping your URL patterns with `i18n_patterns`, your site will now
+respond on URL prefixes. But now it won't respond on the root path.
+
+To fix this, we need to detect the user's browser language and redirect them
+to the best language prefix. The recommended approach to do this is with
+Django's `LocaleMiddleware`:
+
+```python
+# my_project/settings.py
+
+MIDDLEWARE = [
+    # ...
+    'django.middleware.locale.LocaleMiddleware',
+    # ...
+]
+```
+
+#### Custom routing/language detection
+
+You don't strictly have to use `i18n_patterns` or `LocaleMiddleware` for
+this and you can write your own logic if you need to.
+
+All Wagtail needs is the language to be activated (using Django's
+`django.utils.translation.activate` function) before the
+`wagtail.views.serve` view is called.
+
+### Recipes for internationalised sites
+
+#### Language/region selector
+
+Perhaps the most important bit of internationalisation-related UI you can add
+to your site is a selector to allow users to switch between different
+languages.
+
+If you're not convinced that you need this, have a look at [https://www.w3.org/International/questions/qa-site-conneg#yyyshortcomings](https://www.w3.org/International/questions/qa-site-conneg#yyyshortcomings) for some rationale.
+
+(basic_example)=
+
+##### Basic example
+
+Here is a basic example of how to add links between translations of a page.
+
+This example, however, will only include languages defined in
+`WAGTAIL_CONTENT_LANGUAGES` and not any extra languages that might be defined
+in `LANGUAGES`. For more information on what both of these settings mean, see
+[Configuring available languages](configuring_available_languages).
+
+If both settings are set to the same value, this example should work well for you,
+otherwise skip to the next section that has a more complicated example which takes
+this into account.
+
+```html+Django
+
+{# make sure these are at the top of the file #}
+{% load i18n wagtailcore_tags %}
+
+{% if page %}
+    {% for translation in page.get_translations.live %}
+        {% get_language_info for translation.locale.language_code as lang %}
+        <a href="{% pageurl translation %}" rel="alternate" hreflang="{{ language_code }}">
+            {{ lang.name_local }}
+        </a>
+    {% endfor %}
+{% endif %}
+```
+
+Let's break this down:
+
+```html+Django
+{% if page %}
+    ...
+{% endif %}
+```
+
+If this is part of a shared base template it may be used in situations where no page object is available, such as 404 error responses, so check that we have a page before proceeding.
+
+```html+Django
+{% for translation in page.get_translations.live %}
+    ...
+{% endfor %}
+```
+
+This `for` block iterates through all published translations of the current page.
+
+```html+Django
+{% get_language_info for translation.locale.language_code as lang %}
+```
+
+This is a Django built-in tag that gets info about the language of the translation.
+For more information, see [get_language_info() in the Django docs](https://docs.djangoproject.com/en/3.1/topics/i18n/translation/#django.utils.translation.get_language_info).
+
+```html+Django
+<a href="{% pageurl translation %}" rel="alternate" hreflang="{{ language_code }}">
+    {{ lang.name_local }}
+</a>
+```
+
+This adds a link to the translation. We use `{{ lang.name_local }}` to display
+the name of the locale in its own language. We also add `rel` and `hreflang`
+attributes to the `<a>` tag for SEO.
+
+##### Handling locales that share content
+
+Rather than iterating over pages, this example iterates over all of the configured
+languages and finds the page for each one. This works better than the [Basic example](basic_example)
+above on sites that have extra Django `LANGUAGES` that share the same Wagtail content.
+
+For this example to work, you firstly need to add Django's
+[django.template.context_processors.i18n](https://docs.djangoproject.com/en/3.1/ref/templates/api/#django-template-context-processors-i18n)
+context processor to your `TEMPLATES` setting:
+
+```python
+# myproject/settings.py
+
+TEMPLATES = [
+    {
+        # ...
+        'OPTIONS': {
+            'context_processors': [
+                # ...
+                'django.template.context_processors.i18n',
+            ],
+        },
+    },
+]
+```
+
+Now for the example itself:
+
+```html+Django
+{% for language_code, language_name in LANGUAGES %}
+    {% get_language_info for language_code as lang %}
+
+    {% language language_code %}
+        <a href="{% pageurl page.localized %}" rel="alternate" hreflang="{{ language_code }}">
+            {{ lang.name_local }}
+        </a>
+    {% endlanguage %}
+{% endfor %}
+```
+
+Let's break this down too:
+
+```html+Django
+{% for language_code, language_name in LANGUAGES %}
+    ...
+{% endfor %}
+```
+
+This `for` block iterates through all of the configured languages on the site.
+The `LANGUAGES` variable comes from the `django.template.context_processors.i18n`
+context processor.
+
+```html+Django
+{% get_language_info for language_code as lang %}
+```
+
+Does exactly the same as the previous example.
+
+```html+Django
+{% language language_code %}
+    ...
+{% endlanguage %}
+```
+
+This `language` tag comes from Django's `i18n` tag library. It changes the
+active language for just the code contained within it.
+
+```html+Django
+<a href="{% pageurl page.localized %}" rel="alternate" hreflang="{{ language_code }}">
+    {{ lang.name_local }}
+</a>
+```
+
+The only difference with the `<a>` tag here from the `<a>` tag in the previous example
+is how we're getting the page's URL: `{% pageurl page.localized %}`.
+
+All page instances in Wagtail have a `.localized` attribute which fetches the translation
+of the page in the current active language. This is why we activated the language previously.
+
+Another difference here is that if the same translated page is shared in two locales, Wagtail
+will generate the correct URL for the page based on the current active locale. This is the
+key difference between this example and the previous one as the previous one can only get the
+URL of the page in its default locale.
+
+#### API filters for headless sites
+
+For headless sites, the Wagtail API supports two extra filters for internationalised sites:
+
+-   `?locale=` Filters pages by the given locale
+-   `?translation_of=` Filters pages to only include translations of the given page ID
+
+For more information, see [](apiv2_i18n_filters).
+
+#### Translatable snippets
+
+You can make a snippet translatable by making it inherit from `wagtail.models.TranslatableMixin`.
+For example:
+
+```python
+# myapp/models.py
+
+from django.db import models
+
+from wagtail.models import TranslatableMixin
+from wagtail.snippets.models import register_snippet
+
+
+@register_snippet
+class Advert(TranslatableMixin, models.Model):
+    name = models.CharField(max_length=255)
+```
+
+The `TranslatableMixin` model adds the `locale` and `translation_key` fields to the model.
+
+##### Making snippets with existing data translatable
+
+For snippets with existing data, it's not possible to just add `TranslatableMixin`,
+make a migration, and run it. This is because the `locale` and `translation_key`
+fields are both required and `translation_key` needs a unique value for each
+instance.
+
+To migrate the existing data properly, we firstly need to use `BootstrapTranslatableMixin`,
+which excludes these constraints, then add a data migration to set the two fields, then
+switch to `TranslatableMixin`.
+
+This is only needed if there are records in the database. So if the model is empty, you can
+go straight to adding `TranslatableMixin` and skip this.
+
+###### Step 1: Add `BootstrapTranslatableMixin` to the model
+
+This will add the two fields without any constraints:
+
+```python
+# myapp/models.py
+
+from django.db import models
+
+from wagtail.models import BootstrapTranslatableMixin
+from wagtail.snippets.models import register_snippet
+
+
+@register_snippet
+class Advert(BootstrapTranslatableMixin, models.Model):
+    name = models.CharField(max_length=255)
+
+    # if the model has a Meta class, ensure it inherits from
+    # BootstrapTranslatableMixin.Meta too
+    class Meta(BootstrapTranslatableMixin.Meta):
+        verbose_name = 'adverts'
+```
+
+Run `python manage.py makemigrations myapp` to generate the schema migration.
+
+###### Step 2: Create a data migration
+
+Create a data migration with the following command:
+
+```sh
+python manage.py makemigrations myapp --empty
+```
+
+This will generate a new empty migration in the app's `migrations` folder. Edit
+that migration and add a `BootstrapTranslatableModel` for each model to bootstrap
+in that app:
+
+```python
+
+from django.db import migrations
+from wagtail.models import BootstrapTranslatableModel
+
+class Migration(migrations.Migration):
+    dependencies = [
+        ('myapp', '0002_bootstraptranslations'),
+    ]
+
+    # Add one operation for each model to bootstrap here
+    # Note: Only include models that are in the same app!
+    operations = [
+        BootstrapTranslatableModel('myapp.Advert'),
+    ]
+```
+
+Repeat this for any other apps that contain a model to be bootstrapped.
+
+###### Step 3: Change `BootstrapTranslatableMixin` to `TranslatableMixin`
+
+Now that we have a migration that fills in the required fields, we can swap out
+`BootstrapTranslatableMixin` for `TranslatableMixin` that has all the
+constraints:
+
+```python
+# myapp/models.py
+
+from wagtail.models import TranslatableMixin  # Change this line
+
+@register_snippet
+class Advert(TranslatableMixin, models.Model):  # Change this line
+    name = models.CharField(max_length=255)
+
+    class Meta(TranslatableMixin.Meta):  # Change this line, if present
+        verbose_name = 'adverts'
+```
+
+###### Step 4: Run `makemigrations` to generate schema migrations, then migrate!
+
+Run `makemigrations` to generate the schema migration that adds the
+constraints into the database, then run `migrate` to run all of the
+migrations:
+
+```sh
+python manage.py makemigrations myapp
+python manage.py migrate
+```
+
+When prompted to select a fix for the nullable field 'locale' being changed to
+non-nullable, select the option "Ignore for now" (as this has been handled by the
+data migration).
+
+### Translation workflow
+
+As mentioned at the beginning, Wagtail does supply `wagtail.contrib.simple_translation`.
+
+The simple_translation module provides a user interface that allows users to copy pages and translatable snippets into another language.
+
+-   Copies are created in the source language (not translated)
+-   Copies of pages are in draft status
+
+Content editors need to translate the content and publish the pages.
+
+To enable add `"wagtail.contrib.simple_translation"` to `INSTALLED_APPS`
+and run `python manage.py migrate` to create the `submit_translation` permissions.
+In the Wagtail admin, go to settings and give some users or groups the "Can submit translations" permission.
+
+```{note}
+Simple Translation is optional. It can be switched out by third-party packages. Like the more advanced [wagtail-localize](https://github.com/wagtail/wagtail-localize).
+```
+
+#### Wagtail Localize
+
+As part of the initial work on implementing internationalisation for Wagtail core,
+we also created a translation package called `wagtail-localize`. This supports
+translating pages within Wagtail, using PO files, machine translation, and external
+integration with translation services.
+
+Github: [https://github.com/wagtail/wagtail-localize](https://github.com/wagtail/wagtail-localize)
+
+## Alternative internationalisation plugins
+
+Before official multi-language support was added into Wagtail, site implementors
+had to use external plugins. These have not been replaced by Wagtail's own
+implementation as they use slightly different approaches, one of them might
+fit your use case better:
+
+-   [Wagtailtrans](https://github.com/wagtail/wagtailtrans)
+-   [wagtail-modeltranslation](https://github.com/infoportugal/wagtail-modeltranslation)
+
+For a comparison of these options, see AccordBox's blog post
+[How to support multi-language in Wagtail CMS](https://www.accordbox.com/blog/how-support-multi-language-wagtail-cms/).
+
+## Wagtail admin translations
+
+The Wagtail admin backend has been translated into many different languages. You can find a list of currently available translations on Wagtail's [Transifex page](https://www.transifex.com/torchbox/wagtail/). (Note: if you're using an old version of Wagtail, this page may not accurately reflect what languages you have available).
+
+If your language isn't listed on that page, you can easily contribute new languages or correct mistakes. Sign up and submit changes to [Transifex](https://www.transifex.com/torchbox/wagtail/). Translation updates are typically merged into an official release within one month of being submitted.
+
+## Change Wagtail admin language on a per-user basis
+
+Logged-in users can set their preferred language from `/admin/account/`.
+By default, Wagtail provides a list of languages that have a >= 90% translation coverage.
+It is possible to override this list via the [WAGTAILADMIN_PERMITTED_LANGUAGES](WAGTAILADMIN_PERMITTED_LANGUAGES) setting.
+
+In case there is zero or one language permitted, the form will be hidden.
+
+If there is no language selected by the user, the `LANGUAGE_CODE` will be used.
+
+## Changing the primary language of your Wagtail installation
+
+The default language of Wagtail is `en-us` (American English). You can change this by tweaking a couple of Django settings:
+
+-   Make sure [USE_I18N](https://docs.djangoproject.com/en/stable/ref/settings/#use-i18n) is set to `True`
+-   Set [LANGUAGE_CODE](https://docs.djangoproject.com/en/stable/ref/settings/#std:setting-LANGUAGE_CODE) to your websites' primary language
+
+If there is a translation available for your language, the Wagtail admin backend should now be in the language you've chosen.

+ 0 - 679
docs/advanced_topics/i18n.rst

@@ -1,679 +0,0 @@
-====================
-Internationalisation
-====================
-
-.. contents::
-    :local:
-    :depth: 3
-
-.. _multi_language_content:
-
-Multi-language content
-======================
-
-Overview
---------
-
-Out of the box, Wagtail assumes all content will be authored in a single language.
-This document describes how to configure Wagtail for authoring content in
-multiple languages.
-
-.. note::
-    Wagtail provides the infrastructure for creating and serving content in multiple languages.
-    There are two options for managing translations across different languages in the admin interface:
-    :ref:`wagtail.contrib.simple_translation<simple_translation>` or the more advanced `wagtail-localize <https://github.com/wagtail/wagtail-localize>`_ (third-party package).
-
-This document only covers the internationalisation of content managed by Wagtail.
-For information on how to translate static content in template files, JavaScript
-code, etc, refer to the `Django internationalisation docs <https://docs.djangoproject.com/en/3.1/topics/i18n/translation/>`_.
-Or, if you are building a headless site, refer to the docs of the frontend framework you are using.
-
-Wagtail's approach to multi-lingual content
--------------------------------------------
-
-This section provides an explanation of Wagtail's internationalisation approach.
-If you're in a hurry, you can skip to `Configuration`_.
-
-In summary:
-
-- Wagtail stores content in a separate page tree for each locale
-- It has a built-in ``Locale`` model and all pages are linked to a ``Locale`` with the ``locale`` foreign key field
-- It records which pages are translations of each other using a shared UUID stored in the ``translation_key`` field
-- It automatically routes requests through translations of the site's homepage
-- It uses Django's ``i18n_patterns`` and ``LocaleMiddleware`` for language detection
-
-Page structure
-^^^^^^^^^^^^^^
-
-Wagtail stores content in a separate page tree for each locale.
-
-For example, if you have two sites in two locales, then you will see four
-homepages at the top level of the page hierarchy in the explorer.
-
-This approach has some advantages for the editor experience as well:
-
-- There is no default language for editing, so content can be authored in any
-  language and then translated to any other.
-- Translations of a page are separate pages so they can be published at
-  different times.
-- Editors can be given permission to edit content in one locale and not others.
-
-How locales and translations are recorded in the database
-^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
-
-All pages (and any snippets that have translation enabled) have a ``locale`` and
-``translation_key`` field:
-
-- ``locale`` is a foreign key to the ``Locale`` model
-- ``translation_key`` is a UUID that's used to find translations of a piece of content.
-  Translations of the same page/snippet share the same value in this field
-
-These two fields have a 'unique together' constraint so you can't have more than
-one translation in the same locale.
-
-Translated homepages
-^^^^^^^^^^^^^^^^^^^^
-
-When you set up a site in Wagtail, you select the site's homepage in the 'root page'
-field and all requests to that site's root URL will be routed to that page.
-
-Multi-lingual sites have a separate homepage for each locale that exist as siblings
-in the page tree. Wagtail finds the other homepages by looking for translations of
-the site's 'root page'.
-
-This means that to make a site available in another locale, you just need to
-translate and publish its homepage in that new locale.
-
-If Wagtail can't find a homepage that matches the user's language, it will fall back
-to the page that is selected as the 'root page' on the site record, so you can use
-this field to specify the default language of your site.
-
-Language detection and routing
-^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
-
-For detecting the user's language and adding a prefix to the URLs
-(``/en/``, ``/fr-fr/``, for example), Wagtail is designed to work with Django's
-built-in internationalisation utilities such as ``i18n_patterns`` and
-`LocaleMiddleware`. This means that Wagtail should work seamlessly with any
-other internationalised Django applications on your site.
-
-Locales
-~~~~~~~
-
-The locales that are enabled on a site are recorded in the ``Locale`` model in
-``wagtailcore``. This model has just two fields: ID and ``language_code`` which
-stores the `BCP-47 language tag <https://en.wikipedia.org/wiki/IETF_language_tag>`_
-that represents this locale.
-
-The locale records can be set up with an :ref:`optional management UI <enabling_locale_management>` or created
-in the shell. The possible values of the ``language_code`` field are controlled
-by the ``WAGTAIL_CONTENT_LANGUAGES`` setting.
-
-.. note:: Read this if you've changed ``LANGUAGE_CODE`` before enabling internationalisation
-
-  On initial migration, Wagtail creates a ``Locale`` record for the language that
-  was set in the ``LANGUAGE_CODE`` setting at the time the migration was run. All
-  pages will be assigned to this ``Locale`` when Wagtail's internationalisation is disabled.
-
-  If you have changed the ``LANGUAGE_CODE`` setting since updating to Wagtail 2.11,
-  you will need to manually update the record in the ``Locale`` model too before
-  enabling internationalisation, as your existing content will be assigned to the old code.
-
-Configuration
--------------
-
-In this section, we will go through the minimum configuration required to enable
-content to be authored in multiple languages.
-
-.. contents::
-    :local:
-    :depth: 1
-
-.. _enabling_internationalisation:
-
-Enabling internationalisation
-^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
-
-To enable internationalisation in both Django and Wagtail, set the following
-settings to ``True``:
-
-.. code-block:: python
-
-    # my_project/settings.py
-
-    USE_I18N = True
-    WAGTAIL_I18N_ENABLED = True
-
-In addition, you might also want to enable Django's localisation support. This
-will make dates and numbers display in the user's local format:
-
-.. code-block:: python
-
-    # my_project/settings.py
-
-    USE_L10N = True
-
-Configuring available languages
-^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
-
-Next we need to configure the available languages. There are two settings
-for this that are each used for different purposes:
-
-- ``LANGUAGES`` - This sets which languages are available on the frontend of the site.
-- ``WAGTAIL_CONTENT_LANGUAGES`` - This sets which the languages Wagtail content
-  can be authored in.
-
-You can set both of these settings to the exact same value. For example, to
-enable English, French, and Spanish:
-
-.. code-block:: python
-
-    # my_project/settings.py
-
-    WAGTAIL_CONTENT_LANGUAGES = LANGUAGES = [
-        ('en', "English"),
-        ('fr', "French"),
-        ('es', "Spanish"),
-    ]
-
-.. note::
-
-    Whenever ``WAGTAIL_CONTENT_LANGUAGES`` is changed, the ``Locale`` model needs
-    to be updated as well to match.
-
-    This can either be done with a data migration or with the optional locale
-    management UI described in the next section.
-
-You can also set these to different values. You might want to do this if you
-want to have some programmatic localisation (like date formatting or currency,
-for example) but use the same Wagtail content in multiple regions:
-
-.. code-block:: python
-
-    # my_project/settings.py
-
-    LANGUAGES = [
-        ('en-GB', "English (Great Britain)"),
-        ('en-US', "English (United States)"),
-        ('en-CA', "English (Canada)"),
-        ('fr-FR', "French (France)"),
-        ('fr-CA', "French (Canada)"),
-    ]
-
-    WAGTAIL_CONTENT_LANGUAGES = [
-        ('en-GB', "English"),
-        ('fr-FR', "French"),
-    ]
-
-When configured like this, the site will be available in all the different
-locales in the first list, but there will only be two language trees in
-Wagtail.
-
-All the ``en-`` locales will use the "English" language tree, and the ``fr-``
-locales will use the "French" language tree. The differences between each locale
-in a language would be programmatic. For example: which date/number format to
-use, and what currency to display prices in.
-
-.. _enabling_locale_management:
-
-Enabling the locale management UI (optional)
-^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
-
-An optional locale management app exists to allow a Wagtail administrator to
-set up the locales from the Wagtail admin interface.
-
-To enable it, add ``wagtail.locales`` into ``INSTALLED_APPS``:
-
-.. code-block:: python
-
-    # my_project/settings.py
-
-    INSTALLED_APPS = [
-        # ...
-        'wagtail.locales',
-        # ...
-    ]
-
-Adding a language prefix to URLs
-^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
-
-To allow all of the page trees to be served at the same domain, we need
-to add a URL prefix for each language.
-
-To implement this, we can use Django's built-in
-`i18n_patterns <https://docs.djangoproject.com/en/3.1/topics/i18n/translation/#language-prefix-in-url-patterns>`_
-function, which adds a language prefix to all of the URL patterns passed into it.
-This activates the language code specified in the URL and Wagtail takes this into
-account when it decides how to route the request.
-
-In your project's ``urls.py`` add Wagtail's core URLs (and any other URLs you
-want to be translated) into an ``i18n_patterns`` block:
-
-.. code-block:: python
-
-    # /my_project/urls.py
-
-    ...
-
-    from django.conf.urls.i18n import i18n_patterns
-
-    # Non-translatable URLs
-    # Note: if you are using the Wagtail API or sitemaps,
-    # these should not be added to `i18n_patterns` either
-    urlpatterns = [
-        path('django-admin/', admin.site.urls),
-
-        path('admin/', include(wagtailadmin_urls)),
-        path('documents/', include(wagtaildocs_urls)),
-    ]
-
-    # Translatable URLs
-    # These will be available under a language code prefix. For example /en/search/
-    urlpatterns += i18n_patterns(
-        path('search/', search_views.search, name='search'),
-        path("", include(wagtail_urls)),
-    )
-
-User language auto-detection
-^^^^^^^^^^^^^^^^^^^^^^^^^^^^
-
-After wrapping your URL patterns with ``i18n_patterns``, your site will now
-respond on URL prefixes. But now it won't respond on the root path.
-
-To fix this, we need to detect the user's browser language and redirect them
-to the best language prefix. The recommended approach to do this is with
-Django's ``LocaleMiddleware``:
-
-.. code-block:: python
-
-    # my_project/settings.py
-
-    MIDDLEWARE = [
-        # ...
-        'django.middleware.locale.LocaleMiddleware',
-        # ...
-    ]
-
-Custom routing/language detection
-^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
-
-You don't strictly have to use ``i18n_patterns`` or ``LocaleMiddleware`` for
-this and you can write your own logic if you need to.
-
-All Wagtail needs is the language to be activated (using Django's
-``django.utils.translation.activate`` function) before the
-``wagtail.views.serve`` view is called.
-
-Recipes for internationalised sites
------------------------------------
-
-Language/region selector
-^^^^^^^^^^^^^^^^^^^^^^^^
-
-Perhaps the most important bit of internationalisation-related UI you can add
-to your site is a selector to allow users to switch between different
-languages.
-
-If you're not convinced that you need this, have a look at https://www.w3.org/International/questions/qa-site-conneg#yyyshortcomings for some rationale.
-
-Basic example
-~~~~~~~~~~~~~
-
-Here is a basic example of how to add links between translations of a page.
-
-This example, however, will only include languages defined in
-``WAGTAIL_CONTENT_LANGUAGES`` and not any extra languages that might be defined
-in ``LANGUAGES``. For more information on what both of these settings mean, see
-`Configuring available languages`_.
-
-If both settings are set to the same value, this example should work well for you,
-otherwise skip to the next section that has a more complicated example which takes
-this into account.
-
-.. code-block:: html+Django
-
-    {# make sure these are at the top of the file #}
-    {% load i18n wagtailcore_tags %}
-
-    {% if page %}
-        {% for translation in page.get_translations.live %}
-            {% get_language_info for translation.locale.language_code as lang %}
-            <a href="{% pageurl translation %}" rel="alternate" hreflang="{{ language_code }}">
-                {{ lang.name_local }}
-            </a>
-        {% endfor %}
-    {% endif %}
-
-Let's break this down:
-
-.. code-block:: html+Django
-
-    {% if page %}
-        ...
-    {% endif %}
-
-If this is part of a shared base template it may be used in situations where no page object is available, such as 404 error responses, so check that we have a page before proceeding.
-
-.. code-block:: html+Django
-
-    {% for translation in page.get_translations.live %}
-        ...
-    {% endfor %}
-
-This ``for`` block iterates through all published translations of the current page.
-
-.. code-block:: html+Django
-
-    {% get_language_info for translation.locale.language_code as lang %}
-
-This is a Django built-in tag that gets info about the language of the translation.
-For more information, see `get_language_info() in the Django docs <https://docs.djangoproject.com/en/3.1/topics/i18n/translation/#django.utils.translation.get_language_info>`_.
-
-.. code-block:: html+Django
-
-    <a href="{% pageurl translation %}" rel="alternate" hreflang="{{ language_code }}">
-        {{ lang.name_local }}
-    </a>
-
-This adds a link to the translation. We use ``{{ lang.name_local }}`` to display
-the name of the locale in its own language. We also add ``rel`` and ``hreflang``
-attributes to the ``<a>`` tag for SEO.
-
-Handling locales that share content
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-Rather than iterating over pages, this example iterates over all of the configured
-languages and finds the page for each one. This works better than the `Basic example`_
-above on sites that have extra Django ``LANGUAGES`` that share the same Wagtail content.
-
-For this example to work, you firstly need to add Django's
-`django.template.context_processors.i18n <https://docs.djangoproject.com/en/3.1/ref/templates/api/#django-template-context-processors-i18n>`_
-context processor to your ``TEMPLATES`` setting:
-
-.. code-block:: python
-
-    # myproject/settings.py
-
-    TEMPLATES = [
-        {
-            # ...
-            'OPTIONS': {
-                'context_processors': [
-                    # ...
-                    'django.template.context_processors.i18n',
-                ],
-            },
-        },
-    ]
-
-Now for the example itself:
-
-.. code-block:: html+Django
-
-    {% for language_code, language_name in LANGUAGES %}
-        {% get_language_info for language_code as lang %}
-
-        {% language language_code %}
-            <a href="{% pageurl page.localized %}" rel="alternate" hreflang="{{ language_code }}">
-                {{ lang.name_local }}
-            </a>
-        {% endlanguage %}
-    {% endfor %}
-
-Let's break this down too:
-
-.. code-block:: html+Django
-
-    {% for language_code, language_name in LANGUAGES %}
-        ...
-    {% endfor %}
-
-This ``for`` block iterates through all of the configured languages on the site.
-The ``LANGUAGES`` variable comes from the ``django.template.context_processors.i18n``
-context processor.
-
-.. code-block:: html+Django
-
-    {% get_language_info for language_code as lang %}
-
-Does exactly the same as the previous example.
-
-.. code-block:: html+Django
-
-    {% language language_code %}
-        ...
-    {% endlanguage %}
-
-This ``language`` tag comes from Django's ``i18n`` tag library. It changes the
-active language for just the code contained within it.
-
-.. code-block:: html+Django
-
-    <a href="{% pageurl page.localized %}" rel="alternate" hreflang="{{ language_code }}">
-        {{ lang.name_local }}
-    </a>
-
-The only difference with the ``<a>`` tag here from the ``<a>`` tag in the previous example
-is how we're getting the page's URL: ``{% pageurl page.localized %}``.
-
-All page instances in Wagtail have a ``.localized`` attribute which fetches the translation
-of the page in the current active language. This is why we activated the language previously.
-
-Another difference here is that if the same translated page is shared in two locales, Wagtail
-will generate the correct URL for the page based on the current active locale. This is the
-key difference between this example and the previous one as the previous one can only get the
-URL of the page in its default locale.
-
-API filters for headless sites
-^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
-
-For headless sites, the Wagtail API supports two extra filters for internationalised sites:
-
- - ``?locale=`` Filters pages by the given locale
- - ``?translation_of=`` Filters pages to only include translations of the given page ID
-
-For more information, see :ref:`apiv2_i18n_filters`.
-
-Translatable snippets
-^^^^^^^^^^^^^^^^^^^^^
-
-You can make a snippet translatable by making it inherit from ``wagtail.models.TranslatableMixin``.
-For example:
-
-.. code-block:: python
-
-    # myapp/models.py
-
-    from django.db import models
-
-    from wagtail.models import TranslatableMixin
-    from wagtail.snippets.models import register_snippet
-
-
-    @register_snippet
-    class Advert(TranslatableMixin, models.Model):
-        name = models.CharField(max_length=255)
-
-The ``TranslatableMixin`` model adds the ``locale`` and ``translation_key`` fields to the model.
-
-Making snippets with existing data translatable
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-For snippets with existing data, it's not possible to just add ``TranslatableMixin``,
-make a migration, and run it. This is because the ``locale`` and ``translation_key``
-fields are both required and ``translation_key`` needs a unique value for each
-instance.
-
-To migrate the existing data properly, we firstly need to use ``BootstrapTranslatableMixin``,
-which excludes these constraints, then add a data migration to set the two fields, then
-switch to ``TranslatableMixin``.
-
-This is only needed if there are records in the database. So if the model is empty, you can
-go straight to adding ``TranslatableMixin`` and skip this.
-
-Step 1: Add ``BootstrapTranslatableMixin`` to the model
-*******************************************************
-
-This will add the two fields without any constraints:
-
-.. code-block:: python
-
-    # myapp/models.py
-
-    from django.db import models
-
-    from wagtail.models import BootstrapTranslatableMixin
-    from wagtail.snippets.models import register_snippet
-
-
-    @register_snippet
-    class Advert(BootstrapTranslatableMixin, models.Model):
-        name = models.CharField(max_length=255)
-
-        # if the model has a Meta class, ensure it inherits from
-        # BootstrapTranslatableMixin.Meta too
-        class Meta(BootstrapTranslatableMixin.Meta):
-            verbose_name = 'adverts'
-
-Run ``python manage.py makemigrations myapp`` to generate the schema migration.
-
-Step 2: Create a data migration
-*******************************
-
-Create a data migration with the following command:
-
-.. code-block:: bash
-
-    python manage.py makemigrations myapp --empty
-
-This will generate a new empty migration in the app's ``migrations`` folder. Edit
-that migration and add a ``BootstrapTranslatableModel`` for each model to bootstrap
-in that app:
-
-.. code-block:: python
-
-    from django.db import migrations
-    from wagtail.models import BootstrapTranslatableModel
-
-    class Migration(migrations.Migration):
-        dependencies = [
-            ('myapp', '0002_bootstraptranslations'),
-        ]
-
-        # Add one operation for each model to bootstrap here
-        # Note: Only include models that are in the same app!
-        operations = [
-            BootstrapTranslatableModel('myapp.Advert'),
-        ]
-
-Repeat this for any other apps that contain a model to be bootstrapped.
-
-Step 3: Change ``BootstrapTranslatableMixin`` to ``TranslatableMixin``
-**********************************************************************
-
-Now that we have a migration that fills in the required fields, we can swap out
-``BootstrapTranslatableMixin`` for ``TranslatableMixin`` that has all the
-constraints:
-
-.. code-block:: python
-
-    # myapp/models.py
-
-    from wagtail.models import TranslatableMixin  # Change this line
-
-    @register_snippet
-    class Advert(TranslatableMixin, models.Model):  # Change this line
-        name = models.CharField(max_length=255)
-
-        class Meta(TranslatableMixin.Meta):  # Change this line, if present
-            verbose_name = 'adverts'
-
-Step 4: Run ``makemigrations`` to generate schema migrations, then migrate!
-***************************************************************************
-
-Run ``makemigrations`` to generate the schema migration that adds the
-constraints into the database, then run ``migrate`` to run all of the
-migrations:
-
-.. code-block:: bash
-
-    python manage.py makemigrations myapp
-    python manage.py migrate
-
-When prompted to select a fix for the nullable field 'locale' being changed to
-non-nullable, select the option "Ignore for now" (as this has been handled by the
-data migration).
-
-
-Translation workflow
---------------------
-
-As mentioned at the beginning, Wagtail does supply ``wagtail.contrib.simple_translation``.
-
-The simple_translation module provides a user interface that allows users to copy pages and translatable snippets into another language.
-
-- Copies are created in the source language (not translated)
-- Copies of pages are in draft status
-
-Content editors need to translate the content and publish the pages.
-
-To enable add ``"wagtail.contrib.simple_translation"`` to ``INSTALLED_APPS``
-and run ``python manage.py migrate`` to create the ``submit_translation`` permissions.
-In the Wagtail admin, go to settings and give some users or groups the "Can submit translations" permission.
-
-.. note::
-   Simple Translation is optional. It can be switched out by third-party packages. Like the more advanced `wagtail-localize <https://github.com/wagtail/wagtail-localize>`_.
-
-
-Wagtail Localize
-^^^^^^^^^^^^^^^^
-
-As part of the initial work on implementing internationalisation for Wagtail core,
-we also created a translation package called ``wagtail-localize``. This supports
-translating pages within Wagtail, using PO files, machine translation, and external
-integration with translation services.
-
-Github: https://github.com/wagtail/wagtail-localize
-
-Alternative internationalisation plugins
-========================================
-
-Before official multi-language support was added into Wagtail, site implementors
-had to use external plugins. These have not been replaced by Wagtail's own
-implementation as they use slightly different approaches, one of them might
-fit your use case better:
-
-- `Wagtailtrans <https://github.com/wagtail/wagtailtrans>`_
-- `wagtail-modeltranslation <https://github.com/infoportugal/wagtail-modeltranslation>`_
-
-For a comparison of these options, see AccordBox's blog post
-`How to support multi-language in Wagtail CMS <https://www.accordbox.com/blog/how-support-multi-language-wagtail-cms/>`_.
-
-Wagtail admin translations
-==========================
-
-The Wagtail admin backend has been translated into many different languages. You can find a list of currently available translations on Wagtail's `Transifex page <https://www.transifex.com/torchbox/wagtail/>`_. (Note: if you're using an old version of Wagtail, this page may not accurately reflect what languages you have available).
-
-If your language isn't listed on that page, you can easily contribute new languages or correct mistakes. Sign up and submit changes to `Transifex <https://www.transifex.com/torchbox/wagtail/>`_. Translation updates are typically merged into an official release within one month of being submitted.
-
-Change Wagtail admin language on a per-user basis
-=================================================
-
-Logged-in users can set their preferred language from ``/admin/account/``.
-By default, Wagtail provides a list of languages that have a >= 90% translation coverage.
-It is possible to override this list via the :ref:`WAGTAILADMIN_PERMITTED_LANGUAGES <WAGTAILADMIN_PERMITTED_LANGUAGES>` setting.
-
-In case there is zero or one language permitted, the form will be hidden.
-
-If there is no language selected by the user, the ``LANGUAGE_CODE`` will be used.
-
-
-Changing the primary language of your Wagtail installation
-==========================================================
-
-The default language of Wagtail is ``en-us`` (American English). You can change this by tweaking a couple of Django settings:
-
-- Make sure `USE_I18N <https://docs.djangoproject.com/en/stable/ref/settings/#use-i18n>`_ is set to ``True``
-- Set `LANGUAGE_CODE <https://docs.djangoproject.com/en/stable/ref/settings/#std:setting-LANGUAGE_CODE>`_ to your websites' primary language
-
-If there is a translation available for your language, the Wagtail admin backend should now be in the language you've chosen.

+ 24 - 0
docs/advanced_topics/index.md

@@ -0,0 +1,24 @@
+# Advanced topics
+
+```{toctree}
+---
+maxdepth: 2
+---
+images/index
+documents/index
+embeds
+add_to_django_project
+deploying
+performance
+i18n
+privacy
+customisation/index
+third_party_tutorials
+testing
+api/index
+amp
+accessibility_considerations
+boundblocks_and_values
+multi_site_multi_instance_multi_tenancy
+formbuilder_routablepage_redirect
+```

+ 0 - 24
docs/advanced_topics/index.rst

@@ -1,24 +0,0 @@
-Advanced topics
-===============
-
-
-.. toctree::
-    :maxdepth: 2
-
-    images/index
-    documents/index
-    embeds
-    add_to_django_project
-    deploying
-    performance
-    i18n
-    privacy
-    customisation/index
-    third_party_tutorials
-    testing
-    api/index
-    amp
-    accessibility_considerations
-    boundblocks_and_values
-    multi_site_multi_instance_multi_tenancy
-    formbuilder_routablepage_redirect

+ 40 - 40
docs/advanced_topics/multi_site_multi_instance_multi_tenancy.rst → docs/advanced_topics/multi_site_multi_instance_multi_tenancy.md

@@ -1,47 +1,48 @@
-Multi-site, multi-instance and multi-tenancy
-============================================
+# Multi-site, multi-instance and multi-tenancy
 
 This page gives background information on how to run multiple Wagtail sites (with the same source code).
 
-.. contents::
-   :local:
+```{contents}
+---
+local:
+---
+```
 
-Multi-site
-----------
+## Multi-site
 
 Multi-site is a Wagtail project configuration where content creators go into a single admin interface and manage the content of multiple websites. Permission to manage specific content, and restricting access to other content, is possible to some extent.
 
 Multi-site configuration is a single code base, on a single server, connecting to a single database. Media is stored in a single media root directory. Content can be shared between sites.
 
-Wagtail supports multi-site out of the box: Wagtail comes with a :class:`site model <wagtail.models.Site>`. The site model contains a hostname, port, and root page field. When a URL is requested, the request comes in, the domain name and port are taken from the request object to look up the correct site object. The root page is used as starting point to resolve the URL and serve the correct page.
+Wagtail supports multi-site out of the box: Wagtail comes with a [site model](wagtail.models.Site). The site model contains a hostname, port, and root page field. When a URL is requested, the request comes in, the domain name and port are taken from the request object to look up the correct site object. The root page is used as starting point to resolve the URL and serve the correct page.
 
-Wagtail also comes with :ref:`site settings <settings>`. *Site settings* are 'singletons' that let you store additional information on a site. For example, social media settings, a field to upload a logo, or a choice field to select a theme.
+Wagtail also comes with [site settings](settings). _Site settings_ are 'singletons' that let you store additional information on a site. For example, social media settings, a field to upload a logo, or a choice field to select a theme.
 
 Model objects can be linked to a site by placing a foreign key field on the model pointing to the site object. A request object can be used to look up the current site. This way, content belonging to a specific site can be served.
 
-User, groups, and permissions can be configured in such a way that content creators can only manage the pages, images, and documents of a specific site. Wagtail can have multiple *site objects* and multiple *page trees*. Permissions can be linked to a specific page tree or a subsection thereof. Collections are used to categorize images and documents. A collection can be restricted to users who are in a specific group.
+User, groups, and permissions can be configured in such a way that content creators can only manage the pages, images, and documents of a specific site. Wagtail can have multiple _site objects_ and multiple _page trees_. Permissions can be linked to a specific page tree or a subsection thereof. Collections are used to categorize images and documents. A collection can be restricted to users who are in a specific group.
 
-Some projects require content editors to have permissions on specific sites and restrict access to other sites. Splitting *all* content per site and guaranteeing that no content 'leaks' is difficult to realize in a multi-site project. If you require full separation of content, then multi-instance might be a better fit...
+Some projects require content editors to have permissions on specific sites and restrict access to other sites. Splitting _all_ content per site and guaranteeing that no content 'leaks' is difficult to realize in a multi-site project. If you require full separation of content, then multi-instance might be a better fit...
 
-Multi-instance
---------------
+## Multi-instance
 
-Multi-instance is a Wagtail project configuration where a single set of project files is used by multiple websites. Each website has its own settings file, and a dedicated database and media directory. Each website runs in its own server process. This guarantees the *total separation* of *all content*.
+Multi-instance is a Wagtail project configuration where a single set of project files is used by multiple websites. Each website has its own settings file, and a dedicated database and media directory. Each website runs in its own server process. This guarantees the _total separation_ of _all content_.
 
 Assume the domains a.com and b.com. Settings files can be `base.py`, `acom.py`, and `bcom.py`. The base settings will contain all settings like normal. The contents of site-specific settings override the base settings:
 
-.. code-block:: python
+```python
+# settings/acom.py
 
-   # settings/acom.py
-   from base import *  # noqa
+from base import \* # noqa
 
-   ALLOWED_HOSTS = ['a.com']
-   DATABASES["NAME"] = "acom"
-   DATABASES["PASSWORD"] = "password-for-acom"
-   MEDIA_DIR = BASE_DIR / "acom-media"
+ALLOWED_HOSTS = ['a.com']
+DATABASES["NAME"] = "acom"
+DATABASES["PASSWORD"] = "password-for-acom"
+MEDIA_DIR = BASE_DIR / "acom-media"
+```
 
-Each site can be started with its own settings file. In development ``./manage.py runserver --settings settings.acom``.
-In production, for example with uWSGI, specify the correct settings with ``env = DJANGO_SETTINGS_MODULE=settings.acom``.
+Each site can be started with its own settings file. In development `./manage.py runserver --settings settings.acom`.
+In production, for example with uWSGI, specify the correct settings with `env = DJANGO_SETTINGS_MODULE=settings.acom`.
 
 Because each site has its own database and media folder, nothing can 'leak' to another site. But this also means that content cannot be shared between sites as one can do when using the multi-site option.
 
@@ -51,40 +52,39 @@ This multi-instance configuration isn't that different from deploying the projec
 
 In a multi-instance configuration, each instance requires a certain amount of server resources (CPU and memory). That means adding sites will increase server load. This only scales up to a certain point.
 
-Multi-tenancy
--------------
+## Multi-tenancy
 
 Multi-tenancy is a project configuration in which a single instance of the software serves multiple tenants. A tenant is a group of users who have access and permission to a single site. Multitenant software is designed to provide every tenant its configuration, data, and user management.
 
-Wagtail supports *multi-site*, where user management and content are shared. Wagtail can run *multi-instance* where there is full separation of content at the cost of running multiple instances. Multi-tenancy combines the best of both worlds: a single instance, and the full separation of content per site and user management.
+Wagtail supports _multi-site_, where user management and content are shared. Wagtail can run _multi-instance_ where there is full separation of content at the cost of running multiple instances. Multi-tenancy combines the best of both worlds: a single instance, and the full separation of content per site and user management.
 
-Wagtail does not support full multi-tenancy at this moment. But it is on our radar, we would like to improve Wagtail to add multi-tenancy - while still supporting the existing multi-site option. If you have ideas or like to contribute, join us on :ref:`Slack <slack>` in the multi-tenancy channel.
+Wagtail does not support full multi-tenancy at this moment. But it is on our radar, we would like to improve Wagtail to add multi-tenancy - while still supporting the existing multi-site option. If you have ideas or like to contribute, join us on [Slack](slack) in the multi-tenancy channel.
 
 Wagtail currently has the following features to support multi-tenancy:
 
-- A Site model mapping a hostname to a root page
-- Permissions to allow groups of users to manage:
+-   A Site model mapping a hostname to a root page
+-   Permissions to allow groups of users to manage:
 
-  - arbitrary sections of the page tree
-  - sections of the collection tree (coming soon)
-  - one or more collections of documents and images
+    -   arbitrary sections of the page tree
+    -   sections of the collection tree (coming soon)
+    -   one or more collections of documents and images
 
-- The page API is automatically scoped to the host used for the request
+-   The page API is automatically scoped to the host used for the request
 
 But several features do not currently support multi-tenancy:
 
-- Snippets are global pieces of content so not suitable for multi-tenancy but any model that can be registered as a snippet can also be managed via the Wagtail model admin. You can add a site_id to the model and then use the model admin get_queryset method to determine which site can manage each object. The built-in snippet choosers can be replaced by `modelchooser <https://pypi.org/project/wagtail-modelchooser/>`_ that allows filtering the queryset to restrict which sites may display which objects.
-- Site, site setting, user, and group management. At the moment, your best bet is to only allow superusers to manage these objects.
-- Workflows and workflow tasks
-- Site history
-- Redirects
+-   Snippets are global pieces of content so not suitable for multi-tenancy but any model that can be registered as a snippet can also be managed via the Wagtail model admin. You can add a site_id to the model and then use the model admin get_queryset method to determine which site can manage each object. The built-in snippet choosers can be replaced by [modelchooser](https://pypi.org/project/wagtail-modelchooser/) that allows filtering the queryset to restrict which sites may display which objects.
+-   Site, site setting, user, and group management. At the moment, your best bet is to only allow superusers to manage these objects.
+-   Workflows and workflow tasks
+-   Site history
+-   Redirects
 
 Permission configuration for built-in models like Sites, Site settings and Users is not site-specific, so any user with permission to edit a single entry can edit them all. This limitation can be mostly circumvented by only allowing superusers to manage these models.
 
 Python, Django, and Wagtail allow you to override, extend and customise functionality. Here are some ideas that may help you create a multi-tenancy solution for your site:
 
-- Django allows to override templates, this also works in the Wagtail admin.
-- A custom user model can be used to link users to a specific site.
-- Custom admin views can provide more restrictive user management.
+-   Django allows to override templates, this also works in the Wagtail admin.
+-   A custom user model can be used to link users to a specific site.
+-   Custom admin views can provide more restrictive user management.
 
 We welcome interested members of the Wagtail community to contribute code and ideas.

+ 85 - 0
docs/advanced_topics/performance.md

@@ -0,0 +1,85 @@
+# Performance
+
+Wagtail is designed for speed, both in the editor interface and on the front-end, but if you want even better performance or you need to handle very high volumes of traffic, here are some tips on eking out the most from your installation.
+
+## Editor interface
+
+We have tried to minimise external dependencies for a working installation of Wagtail, in order to make it as simple as possible to get going. However, a number of default settings can be configured for better performance:
+
+### Cache
+
+We recommend [Redis](https://redis.io/) as a fast, persistent cache. Install Redis through your package manager (on Debian or Ubuntu: `sudo apt-get install redis-server`), add `django-redis` to your `requirements.txt`, and enable it as a cache backend:
+
+```python
+CACHES = {
+    'default': {
+        'BACKEND': 'django_redis.cache.RedisCache',
+        'LOCATION': 'redis://127.0.0.1:6379/dbname',
+        # for django-redis < 3.8.0, use:
+        # 'LOCATION': '127.0.0.1:6379',
+        'OPTIONS': {
+            'CLIENT_CLASS': 'django_redis.client.DefaultClient',
+        }
+    }
+}
+```
+
+### Caching image renditions
+
+If you define a cache named 'renditions' (typically alongside your 'default' cache),
+Wagtail will cache image rendition lookups, which may improve the performance of pages
+which include many images.
+
+```python
+CACHES = {
+    'default': {...},
+    'renditions': {
+        'BACKEND': 'django.core.cache.backends.memcached.MemcachedCache',
+        'LOCATION': '127.0.0.1:11211',
+        'TIMEOUT': 600,
+        'OPTIONS': {
+            'MAX_ENTRIES': 1000
+        }
+    }
+}
+```
+
+### Search
+
+Wagtail has strong support for [Elasticsearch](https://www.elastic.co) - both in the editor interface and for users of your site - but can fall back to a database search if Elasticsearch isn't present. Elasticsearch is faster and more powerful than the Django ORM for text search, so we recommend installing it or using a hosted service like [Searchly](http://www.searchly.com/).
+
+For details on configuring Wagtail for Elasticsearch, see [](wagtailsearch_backends_elasticsearch).
+
+### Database
+
+Wagtail is tested on PostgreSQL, SQLite and MySQL. It may work on some third-party database backends as well, but this is not guaranteed. We recommend PostgreSQL for production use.
+
+### Templates
+
+The overhead from reading and compiling templates adds up. Django wraps its default loaders with [cached template loader](django.template.loaders.cached.Loader) which stores the compiled `Template` in memory and returns it for subsequent requests. The cached loader is automatically enabled when `DEBUG` is `False`. If you are using custom loaders, update your settings to use it:
+
+```python
+TEMPLATES = [{
+    'BACKEND': 'django.template.backends.django.DjangoTemplates',
+    'DIRS': [os.path.join(BASE_DIR, 'templates')],
+    'OPTIONS': {
+        'loaders': [
+            ('django.template.loaders.cached.Loader', [
+                'django.template.loaders.filesystem.Loader',
+                'django.template.loaders.app_directories.Loader',
+                'path.to.custom.Loader',
+            ]),
+        ],
+    },
+}]
+```
+
+## Public users
+
+(caching_proxy)=
+
+### Caching proxy
+
+To support high volumes of traffic with excellent response times, we recommend a caching proxy. Both [Varnish](https://varnish-cache.org/) and [Squid](http://www.squid-cache.org/) have been tested in production. Hosted proxies like [Cloudflare](https://www.cloudflare.com/) should also work well.
+
+Wagtail supports automatic cache invalidation for Varnish/Squid. See [](frontend_cache_purging) for more information.

+ 0 - 101
docs/advanced_topics/performance.rst

@@ -1,101 +0,0 @@
-Performance
-===========
-
-Wagtail is designed for speed, both in the editor interface and on the front-end, but if you want even better performance or you need to handle very high volumes of traffic, here are some tips on eking out the most from your installation.
-
-
-Editor interface
-~~~~~~~~~~~~~~~~
-
-We have tried to minimise external dependencies for a working installation of Wagtail, in order to make it as simple as possible to get going. However, a number of default settings can be configured for better performance:
-
-
-Cache
------
-
-We recommend `Redis <https://redis.io/>`_ as a fast, persistent cache. Install Redis through your package manager (on Debian or Ubuntu: ``sudo apt-get install redis-server``), add ``django-redis`` to your ``requirements.txt``, and enable it as a cache backend:
-
-.. code-block:: python
-
-    CACHES = {
-        'default': {
-            'BACKEND': 'django_redis.cache.RedisCache',
-            'LOCATION': 'redis://127.0.0.1:6379/dbname',
-            # for django-redis < 3.8.0, use:
-            # 'LOCATION': '127.0.0.1:6379',
-            'OPTIONS': {
-                'CLIENT_CLASS': 'django_redis.client.DefaultClient',
-            }
-        }
-    }
-
-
-Caching image renditions
-------------------------
-
-If you define a cache named 'renditions' (typically alongside your 'default' cache),
-Wagtail will cache image rendition lookups, which may improve the performance of pages
-which include many images.
-
-.. code-block:: python
-
-    CACHES = {
-        'default': {...},
-        'renditions': {
-            'BACKEND': 'django.core.cache.backends.memcached.MemcachedCache',
-            'LOCATION': '127.0.0.1:11211',
-            'TIMEOUT': 600,
-            'OPTIONS': {
-                'MAX_ENTRIES': 1000
-            }
-        }
-    }
-
-
-Search
-------
-
-Wagtail has strong support for `Elasticsearch <https://www.elastic.co>`_ - both in the editor interface and for users of your site - but can fall back to a database search if Elasticsearch isn't present. Elasticsearch is faster and more powerful than the Django ORM for text search, so we recommend installing it or using a hosted service like `Searchly <http://www.searchly.com/>`_.
-
-For details on configuring Wagtail for Elasticsearch, see :ref:`wagtailsearch_backends_elasticsearch`.
-
-
-Database
---------
-
-Wagtail is tested on PostgreSQL, SQLite and MySQL. It may work on some third-party database backends as well, but this is not guaranteed. We recommend PostgreSQL for production use.
-
-
-Templates
----------
-
-The overhead from reading and compiling templates adds up. Django wraps its default loaders with :class:`cached template loader <django.template.loaders.cached.Loader>`: which stores the compiled ``Template`` in memory and returns it for subsequent requests. The cached loader is automatically enabled when ``DEBUG`` is ``False``. If you are using custom loaders, update your settings to use it:
-
-.. code-block:: python
-
-    TEMPLATES = [{
-        'BACKEND': 'django.template.backends.django.DjangoTemplates',
-        'DIRS': [os.path.join(BASE_DIR, 'templates')],
-        'OPTIONS': {
-            'loaders': [
-                ('django.template.loaders.cached.Loader', [
-                    'django.template.loaders.filesystem.Loader',
-                    'django.template.loaders.app_directories.Loader',
-                    'path.to.custom.Loader',
-                ]),
-            ],
-        },
-    }]
-
-
-Public users
-~~~~~~~~~~~~
-
-.. _caching_proxy:
-
-Caching proxy
--------------
-
-To support high volumes of traffic with excellent response times, we recommend a caching proxy. Both `Varnish <https://varnish-cache.org/>`_ and `Squid <http://www.squid-cache.org/>`_ have been tested in production. Hosted proxies like `Cloudflare <https://www.cloudflare.com/>`_ should also work well.
-
- Wagtail supports automatic cache invalidation for Varnish/Squid. See :ref:`frontend_cache_purging` for more information.

+ 90 - 0
docs/advanced_topics/privacy.md

@@ -0,0 +1,90 @@
+(private_pages)=
+
+# Private pages
+
+Users with publish permission on a page can set it to be private by clicking the 'Privacy' control in the top right corner of the page explorer or editing interface. This sets a restriction on who is allowed to view the page and its sub-pages. Several different kinds of restriction are available:
+
+-   **Accessible to logged-in users:** The user must log in to view the page. All user accounts are granted access, regardless of permission level.
+-   **Accessible with the following password:** The user must enter the given password to view the page. This is appropriate for situations where you want to share a page with a trusted group of people, but giving them individual user accounts would be overkill. The same password is shared between all users, and this works independently of any user accounts that exist on the site.
+-   **Accessible to users in specific groups:** The user must be logged in, and a member of one or more of the specified groups, in order to view the page.
+
+Similarly, documents can be made private by placing them in a collection with appropriate privacy settings (see: [](image_document_permissions)).
+
+Private pages and documents work on Wagtail out of the box - the site implementer does not need to do anything to set them up. However, the default "log in" and "password required" forms are only bare-bones HTML pages, and site implementers may wish to replace them with a page customised to their site design.
+
+(login_page)=
+
+## Setting up a login page
+
+The basic login page can be customised by setting `WAGTAIL_FRONTEND_LOGIN_TEMPLATE` to the path of a template you wish to use:
+
+```python
+WAGTAIL_FRONTEND_LOGIN_TEMPLATE = 'myapp/login.html'
+```
+
+Wagtail uses Django's standard `django.contrib.auth.views.LoginView` view here, and so the context variables available on the template are as detailed in [Django's login view documentation](django.contrib.auth.views.LoginView).
+
+If the stock Django login view is not suitable - for example, you wish to use an external authentication system, or you are integrating Wagtail into an existing Django site that already has a working login view - you can specify the URL of the login view via the `WAGTAIL_FRONTEND_LOGIN_URL` setting:
+
+```python
+WAGTAIL_FRONTEND_LOGIN_URL = '/accounts/login/'
+```
+
+To integrate Wagtail into a Django site with an existing login mechanism, setting `WAGTAIL_FRONTEND_LOGIN_URL = LOGIN_URL` will usually be sufficient.
+
+## Setting up a global "password required" page
+
+By setting `PASSWORD_REQUIRED_TEMPLATE` in your Django settings file, you can specify the path of a template which will be used for all "password required" forms on the site (except for page types that specifically override it - see below):
+
+```python
+PASSWORD_REQUIRED_TEMPLATE = 'myapp/password_required.html'
+```
+
+This template will receive the same set of context variables that the blocked page would pass to its own template via `get_context()` - including `page` to refer to the page object itself - plus the following additional variables (which override any of the page's own context variables of the same name):
+
+-   **form** - A Django form object for the password prompt; this will contain a field named `password` as its only visible field. A number of hidden fields may also be present, so the page must loop over `form.hidden_fields` if not using one of Django's rendering helpers such as `form.as_p`.
+-   **action_url** - The URL that the password form should be submitted to, as a POST request.
+
+A basic template suitable for use as `PASSWORD_REQUIRED_TEMPLATE` might look like this:
+
+```html+django
+<!DOCTYPE HTML>
+<html>
+    <head>
+        <title>Password required</title>
+    </head>
+    <body>
+        <h1>Password required</h1>
+        <p>You need a password to access this page.</p>
+        <form action="{{ action_url }}" method="POST">
+            {% csrf_token %}
+
+            {{ form.non_field_errors }}
+
+            <div>
+                {{ form.password.errors }}
+                {{ form.password.label_tag }}
+                {{ form.password }}
+            </div>
+
+            {% for field in form.hidden_fields %}
+                {{ field }}
+            {% endfor %}
+            <input type="submit" value="Continue" />
+        </form>
+    </body>
+</html>
+```
+
+Password restrictions on documents use a separate template, specified through the setting `DOCUMENT_PASSWORD_REQUIRED_TEMPLATE`; this template also receives the context variables `form` and `action_url` as described above.
+
+## Setting a "password required" page for a specific page type
+
+The attribute `password_required_template` can be defined on a page model to use a custom template for the "password required" view, for that page type only. For example, if a site had a page type for displaying embedded videos along with a description, it might choose to use a custom "password required" template that displays the video description as usual, but shows the password form in place of the video embed.
+
+```python
+class VideoPage(Page):
+    ...
+
+    password_required_template = 'video/password_required.html'
+```

+ 0 - 97
docs/advanced_topics/privacy.rst

@@ -1,97 +0,0 @@
-.. _private_pages:
-
-Private pages
-=============
-
-Users with publish permission on a page can set it to be private by clicking the 'Privacy' control in the top right corner of the page explorer or editing interface. This sets a restriction on who is allowed to view the page and its sub-pages. Several different kinds of restriction are available:
-
-* **Accessible to logged-in users:** The user must log in to view the page. All user accounts are granted access, regardless of permission level.
-* **Accessible with the following password:** The user must enter the given password to view the page. This is appropriate for situations where you want to share a page with a trusted group of people, but giving them individual user accounts would be overkill. The same password is shared between all users, and this works independently of any user accounts that exist on the site.
-* **Accessible to users in specific groups:** The user must be logged in, and a member of one or more of the specified groups, in order to view the page.
-
-Similarly, documents can be made private by placing them in a collection with appropriate privacy settings (see :ref:`image_document_permissions`).
-
-Private pages and documents work on Wagtail out of the box - the site implementer does not need to do anything to set them up. However, the default "log in" and "password required" forms are only bare-bones HTML pages, and site implementers may wish to replace them with a page customised to their site design.
-
-.. _login_page:
-
-Setting up a login page
-~~~~~~~~~~~~~~~~~~~~~~~
-
-The basic login page can be customised by setting ``WAGTAIL_FRONTEND_LOGIN_TEMPLATE`` to the path of a template you wish to use:
-
-.. code-block:: python
-
-  WAGTAIL_FRONTEND_LOGIN_TEMPLATE = 'myapp/login.html'
-
-Wagtail uses Django's standard ``django.contrib.auth.views.LoginView`` view here, and so the context variables available on the template are as detailed in :class:`Django's login view documentation <django.contrib.auth.views.LoginView>`.
-
-If the stock Django login view is not suitable - for example, you wish to use an external authentication system, or you are integrating Wagtail into an existing Django site that already has a working login view - you can specify the URL of the login view via the ``WAGTAIL_FRONTEND_LOGIN_URL`` setting:
-
-.. code-block:: python
-
-  WAGTAIL_FRONTEND_LOGIN_URL = '/accounts/login/'
-
-To integrate Wagtail into a Django site with an existing login mechanism, setting ``WAGTAIL_FRONTEND_LOGIN_URL = LOGIN_URL`` will usually be sufficient.
-
-
-Setting up a global "password required" page
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-By setting ``PASSWORD_REQUIRED_TEMPLATE`` in your Django settings file, you can specify the path of a template which will be used for all "password required" forms on the site (except for page types that specifically override it - see below):
-
-.. code-block:: python
-
-  PASSWORD_REQUIRED_TEMPLATE = 'myapp/password_required.html'
-
-This template will receive the same set of context variables that the blocked page would pass to its own template via ``get_context()`` - including ``page`` to refer to the page object itself - plus the following additional variables (which override any of the page's own context variables of the same name):
-
-- **form** - A Django form object for the password prompt; this will contain a field named ``password`` as its only visible field. A number of hidden fields may also be present, so the page must loop over ``form.hidden_fields`` if not using one of Django's rendering helpers such as ``form.as_p``.
-- **action_url** - The URL that the password form should be submitted to, as a POST request.
-
-A basic template suitable for use as ``PASSWORD_REQUIRED_TEMPLATE`` might look like this:
-
- .. code-block:: html+django
-
-    <!DOCTYPE HTML>
-    <html>
-        <head>
-            <title>Password required</title>
-        </head>
-        <body>
-            <h1>Password required</h1>
-            <p>You need a password to access this page.</p>
-            <form action="{{ action_url }}" method="POST">
-                {% csrf_token %}
-
-                {{ form.non_field_errors }}
-
-                <div>
-                    {{ form.password.errors }}
-                    {{ form.password.label_tag }}
-                    {{ form.password }}
-                </div>
-
-                {% for field in form.hidden_fields %}
-                    {{ field }}
-                {% endfor %}
-                <input type="submit" value="Continue" />
-            </form>
-        </body>
-    </html>
-
-
-Password restrictions on documents use a separate template, specified through the setting ``DOCUMENT_PASSWORD_REQUIRED_TEMPLATE``; this template also receives the context variables ``form`` and ``action_url`` as described above.
-
-
-Setting a "password required" page for a specific page type
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-The attribute ``password_required_template`` can be defined on a page model to use a custom template for the "password required" view, for that page type only. For example, if a site had a page type for displaying embedded videos along with a description, it might choose to use a custom "password required" template that displays the video description as usual, but shows the password form in place of the video embed.
-
-.. code-block:: python
-
-    class VideoPage(Page):
-        ...
-
-        password_required_template = 'video/password_required.html'

+ 157 - 0
docs/advanced_topics/testing.md

@@ -0,0 +1,157 @@
+(reference)=
+
+# Testing your Wagtail site
+
+Wagtail comes with some utilities that simplify writing tests for your site.
+
+## WagtailPageTests
+
+**_class_ wagtail.test.utils.WagtailPageTests**  
+`WagtailPageTests` extends `django.test.TestCase`, adding a few new `assert` methods. You should extend this class to make use of its methods:
+
+```python
+from wagtail.test.utils import WagtailPageTests
+from myapp.models import MyPage
+
+class MyPageTests(WagtailPageTests):
+    def test_can_create_a_page(self):
+        ...
+```
+
+**assertCanCreateAt(_parent_model, child_model, msg=None_)**  
+Assert a particular child Page type can be created under a parent Page type. `parent_model` and `child_model` should be the Page classes being tested.
+
+```python
+def test_can_create_under_home_page(self):
+    # You can create a ContentPage under a HomePage
+    self.assertCanCreateAt(HomePage, ContentPage)
+```
+
+**assertCanNotCreateAt(_parent_model, child_model, msg=None_)**  
+Assert a particular child Page type can not be created under a parent Page type. `parent_model` and `child_model` should be the Page classes being tested.
+
+```python
+def test_cant_create_under_event_page(self):
+    # You can not create a ContentPage under an EventPage
+    self.assertCanNotCreateAt(EventPage, ContentPage)
+```
+
+**assertCanCreate(_parent, child_model, data, msg=None_)**  
+Assert that a child of the given Page type can be created under the parent, using the supplied POST data.
+
+`parent` should be a Page instance, and `child_model` should be a Page subclass. `data` should be a dict that will be POSTed at the Wagtail admin Page creation method.
+
+```python
+from wagtail.test.utils.form_data import nested_form_data, streamfield
+
+def test_can_create_content_page(self):
+    # Get the HomePage
+    root_page = HomePage.objects.get(pk=2)
+
+    # Assert that a ContentPage can be made here, with this POST data
+    self.assertCanCreate(root_page, ContentPage, nested_form_data({
+        'title': 'About us',
+        'body': streamfield([
+            ('text', 'Lorem ipsum dolor sit amet'),
+        ])
+    }))
+```
+
+See [](form_data_test_helpers) for a set of functions useful for constructing POST data.
+
+**assertAllowedParentPageTypes(_child_model, parent_models, msg=None_)**  
+Test that the only page types that `child_model` can be created under are `parent_models`.
+
+The list of allowed parent models may differ from those set in `Page.parent_page_types`, if the parent models have set `Page.subpage_types`.
+
+```python
+def test_content_page_parent_pages(self):
+    # A ContentPage can only be created under a HomePage
+    # or another ContentPage
+    self.assertAllowedParentPageTypes(
+        ContentPage, {HomePage, ContentPage})
+
+    # An EventPage can only be created under an EventIndex
+    self.assertAllowedParentPageTypes(
+        EventPage, {EventIndex})
+```
+
+**assertAllowedSubpageTypes(_parent_model, child_models, msg=None_)**  
+Test that the only page types that can be created under `parent_model` are `child_models`.
+
+The list of allowed child models may differ from those set in `Page.subpage_types`, if the child models have set `Page.parent_page_types`.
+
+```python
+def test_content_page_subpages(self):
+    # A ContentPage can only have other ContentPage children
+    self.assertAllowedSubpageTypes(
+        ContentPage, {ContentPage})
+
+    # A HomePage can have ContentPage and EventIndex children
+    self.assertAllowedParentPageTypes(
+        HomePage, {ContentPage, EventIndex})
+```
+
+(form_data_test_helpers)=
+
+## Form data helpers
+
+```{eval-rst}
+.. automodule:: wagtail.test.utils.form_data
+
+   .. autofunction:: nested_form_data
+
+   .. autofunction:: rich_text
+
+   .. autofunction:: streamfield
+
+   .. autofunction:: inline_formset
+```
+
+## Fixtures
+
+### Using `dumpdata`
+
+Creating [fixtures](django:howto/initial-data) for tests is best done by creating content in a development
+environment, and using Django's [dumpdata](https://docs.djangoproject.com/en/stable/ref/django-admin/#django-admin-dumpdata) command.
+
+Note that by default `dumpdata` will represent `content_type` by the primary key; this may cause consistency issues when adding / removing models, as content types are populated separately from fixtures. To prevent this, use the `--natural-foreign` switch, which represents content types by `["app", "model"]` instead.
+
+### Manual modification
+
+You could modify the dumped fixtures manually, or even write them all by hand.
+Here are a few things to be wary of.
+
+#### Custom Page models
+
+When creating customised Page models in fixtures, you will need to add both a
+`wagtailcore.page` entry, and one for your custom Page model.
+
+Let's say you have a `website` module which defines a `Homepage(Page)` class.
+You could create such a homepage in a fixture with:
+
+```json
+[
+    {
+        "model": "wagtailcore.page",
+        "pk": 3,
+        "fields": {
+            "title": "My Customer's Homepage",
+            "content_type": ["website", "homepage"],
+            "depth": 2
+        }
+    },
+    {
+        "model": "website.homepage",
+        "pk": 3,
+        "fields": {}
+    }
+]
+```
+
+#### Treebeard fields
+
+Filling in the `path` / `numchild` / `depth` fields is necessary in order for tree operations like `get_parent()` to work correctly.
+`url_path` is another field that can cause errors in some uncommon cases if it isn't filled in.
+
+The [Treebeard docs](https://django-treebeard.readthedocs.io/en/latest/mp_tree.html) might help in understanding how this works.

+ 0 - 166
docs/advanced_topics/testing.rst

@@ -1,166 +0,0 @@
-.. _reference:
-
-=========================
-Testing your Wagtail site
-=========================
-
-Wagtail comes with some utilities that simplify writing tests for your site.
-
-.. automodule:: wagtail.test.utils
-
-
-WagtailPageTests
-================
-
-.. class:: WagtailPageTests
-
-    ``WagtailPageTests`` extends ``django.test.TestCase``, adding a few new ``assert`` methods. You should extend this class to make use of its methods:
-
-    .. code-block:: python
-
-        from wagtail.test.utils import WagtailPageTests
-        from myapp.models import MyPage
-
-        class MyPageTests(WagtailPageTests):
-            def test_can_create_a_page(self):
-                ...
-
-    .. automethod:: assertCanCreateAt
-
-        .. code-block:: python
-
-            def test_can_create_under_home_page(self):
-                # You can create a ContentPage under a HomePage
-                self.assertCanCreateAt(HomePage, ContentPage)
-
-    .. automethod:: assertCanNotCreateAt
-
-        .. code-block:: python
-
-            def test_cant_create_under_event_page(self):
-                # You can not create a ContentPage under an EventPage
-                self.assertCanNotCreateAt(EventPage, ContentPage)
-
-    .. automethod:: assertCanCreate
-
-        .. code-block:: python
-
-            from wagtail.test.utils.form_data import nested_form_data, streamfield
-
-            def test_can_create_content_page(self):
-                # Get the HomePage
-                root_page = HomePage.objects.get(pk=2)
-
-                # Assert that a ContentPage can be made here, with this POST data
-                self.assertCanCreate(root_page, ContentPage, nested_form_data({
-                    'title': 'About us',
-                    'body': streamfield([
-                        ('text', 'Lorem ipsum dolor sit amet'),
-                    ])
-                }))
-
-        See :ref:`form_data_test_helpers` for a set of functions useful for constructing POST data.
-
-    .. automethod:: assertAllowedParentPageTypes
-
-        .. code-block:: python
-
-            def test_content_page_parent_pages(self):
-                # A ContentPage can only be created under a HomePage
-                # or another ContentPage
-                self.assertAllowedParentPageTypes(
-                    ContentPage, {HomePage, ContentPage})
-
-                # An EventPage can only be created under an EventIndex
-                self.assertAllowedParentPageTypes(
-                    EventPage, {EventIndex})
-
-    .. automethod:: assertAllowedSubpageTypes
-
-        .. code-block:: python
-
-            def test_content_page_subpages(self):
-                # A ContentPage can only have other ContentPage children
-                self.assertAllowedSubpageTypes(
-                    ContentPage, {ContentPage})
-
-                # A HomePage can have ContentPage and EventIndex children
-                self.assertAllowedParentPageTypes(
-                    HomePage, {ContentPage, EventIndex})
-
-
-.. _form_data_test_helpers:
-
-Form data helpers
-=================
-
-.. automodule:: wagtail.test.utils.form_data
-
-   .. autofunction:: nested_form_data
-
-   .. autofunction:: rich_text
-
-   .. autofunction:: streamfield
-
-   .. autofunction:: inline_formset
-
-
-
-Fixtures
-========
-
-Using ``dumpdata``
-------------------
-
-Creating :doc:`fixtures <django:howto/initial-data>` for tests is best done by creating content in a development
-environment, and using Django's dumpdata_ command.
-
-Note that by default ``dumpdata`` will represent ``content_type`` by the primary key; this may cause consistency issues when adding / removing models, as content types are populated separately from fixtures. To prevent this, use the ``--natural-foreign`` switch, which represents content types by ``["app", "model"]`` instead.
-
-
-Manual modification
--------------------
-
-You could modify the dumped fixtures manually, or even write them all by hand.
-Here are a few things to be wary of.
-
-
-Custom Page models
-~~~~~~~~~~~~~~~~~~
-
-When creating customised Page models in fixtures, you will need to add both a
-``wagtailcore.page`` entry, and one for your custom Page model.
-
-Let's say you have a ``website`` module which defines a ``Homepage(Page)`` class.
-You could create such a homepage in a fixture with:
-
-.. code-block:: json
-
-    [
-      {
-        "model": "wagtailcore.page",
-        "pk": 3,
-        "fields": {
-          "title": "My Customer's Homepage",
-          "content_type": ["website", "homepage"],
-          "depth": 2
-        }
-      },
-      {
-        "model": "website.homepage",
-        "pk": 3,
-        "fields": {}
-      }
-    ]
-
-
-Treebeard fields
-~~~~~~~~~~~~~~~~
-
-Filling in the ``path`` / ``numchild`` / ``depth`` fields is necessary in order for tree operations like ``get_parent()`` to work correctly.
-``url_path`` is another field that can cause errors in some uncommon cases if it isn't filled in.
-
-The `Treebeard docs`_ might help in understanding how this works.
-
-.. _dumpdata: https://docs.djangoproject.com/en/stable/ref/django-admin/#django-admin-dumpdata
-.. _Treebeard docs: https://django-treebeard.readthedocs.io/en/latest/mp_tree.html