Browse Source

Documentation - migrate reference/** files to markdown content

- relates to #8383
SilvestriStefano 2 years ago
parent
commit
f535a0cc43

+ 1 - 1
CHANGELOG.txt

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

+ 1 - 0
CONTRIBUTORS.rst

@@ -611,6 +611,7 @@ Contributors
 * Igor Strapko
 * Sandeep M A
 * Bernd de Ridder
+* Stefano Silvestri
 
 Translators
 ===========

+ 1 - 1
docs/advanced_topics/customisation/page_editing_interface.md

@@ -51,7 +51,7 @@ class BookPage(Page):
 
 `RichTextField` inherits from Django's basic `TextField` field, so you can pass any field parameters into `RichTextField` as if using a normal Django field. This field does not need a special panel and can be defined with `FieldPanel`.
 
-However, template output from `RichTextField` is special and needs to be filtered in order to preserve embedded content. See [](rich-text-filter).
+However, template output from `RichTextField` is special and needs to be filtered in order to preserve embedded content. See [](rich_text_filter).
 
 (rich_text_features)=
 

+ 2 - 0
docs/extending/custom_account_settings.md

@@ -1,3 +1,5 @@
+(custom_account_settings)=
+
 # Customising the user account settings form
 
 This document describes how to customise the user account settings form that can be found by clicking "Account settings"

+ 1 - 1
docs/extending/extending_draftail.md

@@ -335,7 +335,7 @@ document.querySelectorAll('[data-stock]').forEach((elt) => {
 });
 ```
 
-Custom block entities can also be created (have a look at the separate [Draftail documentation](https://www.draftail.org/docs/blocks)), but these are not detailed here since [StreamField](streamfield) is the go-to way to create block-level rich text in Wagtail.
+Custom block entities can also be created (have a look at the separate [Draftail documentation](https://www.draftail.org/docs/blocks)), but these are not detailed here since [StreamField](streamfield_topic) is the go-to way to create block-level rich text in Wagtail.
 
 ## Integration of the Draftail widgets
 

+ 1 - 1
docs/extending/rich_text_internals.md

@@ -21,7 +21,7 @@ Rich text data (as handled by [RichTextField](rich-text), and `RichTextBlock` wi
 <p><a linktype="page" id="3">Contact us</a> for more information.</p>
 ```
 
-Here, the `linktype` attribute identifies a rule that shall be used to rewrite the tag. When rendered on a template through the `|richtext` filter (see [rich text filter](rich-text-filter)), this is converted into valid HTML:
+Here, the `linktype` attribute identifies a rule that shall be used to rewrite the tag. When rendered on a template through the `|richtext` filter (see [rich text filter](rich_text_filter)), this is converted into valid HTML:
 
 ```html
 <p><a href="/contact-us/">Contact us</a> for more information.</p>

+ 2 - 0
docs/extending/template_components.md

@@ -1,3 +1,5 @@
+(template_components)=
+
 # Template components
 
 Working with objects that know how to render themselves as elements on an HTML template is a common pattern seen throughout the Wagtail admin. For example, the admin homepage is a view provided by the central `wagtail.admin` app, but brings together information panels sourced from various other modules of Wagtail, such as images and documents (potentially along with others provided by third-party packages). These panels are passed to the homepage via the [`construct_homepage_panels`](construct_homepage_panels) hook, and each one is responsible for providing its own HTML rendering. In this way, the module providing the panel has full control over how it appears on the homepage.

+ 1 - 1
docs/reference/contrib/forms/index.md

@@ -5,7 +5,7 @@
 The `wagtailforms` module allows you to set up single-page forms, such as a 'Contact us' form, as pages of a Wagtail site. It provides a set of base models that site implementers can extend to create their own `FormPage` type with their own site-specific templates. Once a page type has been set up in this way, editors can build forms within the usual page editor, consisting of any number of fields. Form submissions are stored for later retrieval through a new 'Forms' section within the Wagtail admin interface; in addition, they can be optionally e-mailed to an address specified by the editor.
 
 ```{note}
-**wagtailforms is not a replacement for** {doc}`Django's form support <django:topics/forms/index>`. It is designed as a way for page authors to build general-purpose data collection forms without having to write code. If you intend to build a form that assigns specific behaviour to individual fields (such as creating user accounts), or needs a custom HTML layout, you will almost certainly be better served by a standard Django form, where the fields are fixed in code rather than defined on-the-fly by a page author. See the [wagtail-form-example project](https://github.com/gasman/wagtail-form-example/commits/master) for an example of integrating a Django form into a Wagtail page.
+**wagtailforms is not a replacement for** [Django's form support](django:topics/forms/index). It is designed as a way for page authors to build general-purpose data collection forms without having to write code. If you intend to build a form that assigns specific behaviour to individual fields (such as creating user accounts), or needs a custom HTML layout, you will almost certainly be better served by a standard Django form, where the fields are fixed in code rather than defined on-the-fly by a page author. See the [wagtail-form-example project](https://github.com/gasman/wagtail-form-example/commits/master) for an example of integrating a Django form into a Wagtail page.
 ```
 
 (form_builder_usage)=

+ 1 - 1
docs/reference/contrib/searchpromotions.md

@@ -1,4 +1,4 @@
-(editors-picks)=
+(editors_picks)=
 
 # Promoted search results
 

+ 899 - 1070
docs/reference/hooks.md

@@ -1,149 +1,145 @@
+(admin_hooks)=
 
-.. _admin_hooks:
+# Hooks
 
-Hooks
-=====
+On loading, Wagtail will search for any app with the file `wagtail_hooks.py` and execute the contents. This provides a way to register your own functions to execute at certain points in Wagtail's execution, such as when a page is saved or when the main menu is constructed.
 
-On loading, Wagtail will search for any app with the file ``wagtail_hooks.py`` and execute the contents. This provides a way to register your own functions to execute at certain points in Wagtail's execution, such as when a page is saved or when the main menu is constructed.
+```{note}
+Hooks are typically used to customise the view-level behaviour of the Wagtail admin and front-end.
+For customisations that only deal with model-level behaviour - such as calling an external service when a page or document is added - it is often better to use [Django's signal mechanism](django:topics/signals) (see also: [Wagtail signals](signals)), as these are not dependent on a user taking a particular path through the admin interface.
+```
 
-.. note::
-   Hooks are typically used to customise the view-level behaviour of the Wagtail admin and front-end. For customisations that only deal with model-level behaviour - such as calling an external service when a page or document is added - it is often better to use :doc:`Django's signal mechanism <django:topics/signals>` (see also: :ref:`Wagtail signals <signals>`), as these are not dependent on a user taking a particular path through the admin interface.
+Registering functions with a Wagtail hook is done through the `@hooks.register` decorator:
 
+```python
+from wagtail import hooks
 
-Registering functions with a Wagtail hook is done through the ``@hooks.register`` decorator:
+@hooks.register('name_of_hook')
+def my_hook_function(arg1, arg2...)
+    # your code here
+```
 
-.. code-block:: python
+Alternatively, `hooks.register` can be called as an ordinary function, passing in the name of the hook and a handler function defined elsewhere:
 
-  from wagtail import hooks
-
-  @hooks.register('name_of_hook')
-  def my_hook_function(arg1, arg2...)
-      # your code here
-
-
-Alternatively, ``hooks.register`` can be called as an ordinary function, passing in the name of the hook and a handler function defined elsewhere:
-
-.. code-block:: python
+```python
+hooks.register('name_of_hook', my_hook_function)
+```
 
-  hooks.register('name_of_hook', my_hook_function)
+If you need your hooks to run in a particular order, you can pass the `order` parameter. If order is not specified then the hooks proceed in the order given by `INSTALLED_APPS`. Wagtail uses hooks internally, too, so you need to be aware of order when overriding built-in Wagtail functionality (i.e. removing default summary items):
 
-If you need your hooks to run in a particular order, you can pass the ``order`` parameter.  If order is not specified then the hooks proceed in the order given by INSTALLED_APPS. Wagtail uses hooks internally, too, so you need to be aware of order when overriding built-in Wagtail functionality (i.e. removing default summary items):
+```python
+@hooks.register('name_of_hook', order=1)  # This will run after every hook in the wagtail core
+def my_hook_function(arg1, arg2...)
+    # your code here
 
-.. code-block:: python
+@hooks.register('name_of_hook', order=-1)  # This will run before every hook in the wagtail core
+def my_other_hook_function(arg1, arg2...)
+    # your code here
 
-  @hooks.register('name_of_hook', order=1)  # This will run after every hook in the wagtail core
-  def my_hook_function(arg1, arg2...)
-      # your code here
+@hooks.register('name_of_hook', order=2)  # This will run after `my_hook_function`
+def yet_another_hook_function(arg1, arg2...)
+    # your code here
+```
 
-  @hooks.register('name_of_hook', order=-1)  # This will run before every hook in the wagtail core
-  def my_other_hook_function(arg1, arg2...)
-      # your code here
-
-  @hooks.register('name_of_hook', order=2)  # This will run after `my_hook_function`
-  def yet_another_hook_function(arg1, arg2...)
-      # your code here
-
-Unit testing hooks
-------------------
+## Unit testing hooks
 
 Hooks are usually registered on startup and can't be changed at runtime. But when writing unit tests, you might want to register a hook
 function just for a single test or block of code and unregister it so that it doesn't run when other tests are run.
 
-You can register hooks temporarily using the ``hooks.register_temporarily`` function, this can be used as both a decorator and a context
+You can register hooks temporarily using the `hooks.register_temporarily` function, this can be used as both a decorator and a context
 manager. Here's an example of how to register a hook function for just a single test:
 
-.. code-block:: python
+```python
+def my_hook_function():
+    pass
 
-  def my_hook_function():
-      ...
+class MyHookTest(TestCase):
 
-  class MyHookTest(TestCase):
-
-      @hooks.register_temporarily('name_of_hook', my_hook_function)
-      def test_my_hook_function(self):
-          # Test with the hook registered here
-          ...
+    @hooks.register_temporarily('name_of_hook', my_hook_function)
+    def test_my_hook_function(self):
+        # Test with the hook registered here
+        pass
+```
 
 And here's an example of registering a hook function for a single block of code:
 
-.. code-block:: python
+```python
+def my_hook_function():
+    pass
 
+with hooks.register_temporarily('name_of_hook', my_hook_function):
+    # Hook is registered here
+    ..
 
-  def my_hook_function():
-      ...
+# Hook is unregistered here
+```
 
-  with hooks.register_temporarily('name_of_hook', my_hook_function):
-      # Hook is registered here
-      ..
+If you need to register multiple hooks in a `with` block, you can pass the hooks in as a list of tuples:
 
-  # Hook is unregistered here
+```python
+def my_hook(...):
+    pass
 
-If you need to register multiple hooks in a ``with`` block, you can pass the hooks in as a list of tuples:
+def my_other_hook(...):
+    pass
 
-.. code-block:: python
+with hooks.register_temporarily([
+    ('hook_name', my_hook),
+    ('hook_name', my_other_hook),
+]):
+    # All hooks are registered here
+    ..
 
-    def my_hook(...):
-        pass
-
-    def my_other_hook(...):
-        pass
-
-    with hooks.register_temporarily([
-        ('hook_name', my_hook),
-        ('hook_name', my_other_hook),
-    ]):
-        # All hooks are registered here
-        ..
-
-    # All hooks are unregistered here
+# All hooks are unregistered here
+```
 
 The available hooks are listed below.
 
-.. contents::
-    :local:
-    :depth: 1
+```{contents}
+---
+local:
+depth: 1
+---
+```
 
-Admin modules
--------------
+## Admin modules
 
 Hooks for building new areas of the admin interface (alongside pages, images, documents and so on).
 
-.. _construct_homepage_panels:
+(construct_homepage_panels)=
 
-``construct_homepage_panels``
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+### `construct_homepage_panels`
 
-  Add or remove panels from the Wagtail admin homepage. The callable passed into this hook should take a ``request`` object and a list of panel objects, and should modify this list in-place as required. Panel objects are :doc:`components </extending/template_components>` with an additional ``order`` property, an integer that determines the panel's position in the final ordered list. The default panels use integers between ``100`` and ``300``.
+Add or remove panels from the Wagtail admin homepage. The callable passed into this hook should take a `request` object and a list of panel objects, and should modify this list in-place as required. Panel objects are [](template_components) with an additional `order` property, an integer that determines the panel's position in the final ordered list. The default panels use integers between `100` and `300`.
 
-  .. code-block:: python
+```python
+from django.utils.safestring import mark_safe
 
-    from django.utils.safestring import mark_safe
+from wagtail.admin.ui.components import Component
+from wagtail import hooks
 
-    from wagtail.admin.ui.components import Component
-    from wagtail import hooks
+class WelcomePanel(Component):
+    order = 50
 
-    class WelcomePanel(Component):
-        order = 50
+    def render_html(self, parent_context):
+        return mark_safe("""
+        <section class="panel summary nice-padding">
+          <h3>No, but seriously -- welcome to the admin homepage.</h3>
+        </section>
+        """)
 
-        def render_html(self, parent_context):
-            return mark_safe("""
-            <section class="panel summary nice-padding">
-              <h3>No, but seriously -- welcome to the admin homepage.</h3>
-            </section>
-            """)
+@hooks.register('construct_homepage_panels')
+def add_another_welcome_panel(request, panels):
+    panels.append(WelcomePanel())
+```
 
-    @hooks.register('construct_homepage_panels')
-    def add_another_welcome_panel(request, panels):
-        panels.append(WelcomePanel())
+(construct_homepage_summary_items)=
 
+### `construct_homepage_summary_items`
 
-.. _construct_homepage_summary_items:
-
-``construct_homepage_summary_items``
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-  Add or remove items from the 'site summary' bar on the admin homepage (which shows the number of pages and other object that exist on the site). The callable passed into this hook should take a ``request`` object and a list of summary item objects, and should modify this list in-place as required. Summary item objects are instances of ``wagtail.admin.site_summary.SummaryItem``, which extends :ref:`the Component class <creating_template_components>` with the following additional methods and properties:
+Add or remove items from the 'site summary' bar on the admin homepage (which shows the number of pages and other object that exist on the site). The callable passed into this hook should take a `request` object and a list of summary item objects, and should modify this list in-place as required. Summary item objects are instances of `wagtail.admin.site_summary.SummaryItem`, which extends :ref:`the Component class <creating_template_components>` with the following additional methods and properties:
 
+```{eval-rst}
   .. method:: SummaryItem(request)
 
     Constructor; receives the request object its argument
@@ -155,1410 +151,1243 @@ Hooks for building new areas of the admin interface (alongside pages, images, do
   .. method:: is_shown()
 
     Returns a boolean indicating whether the summary item should be shown on this request.
+```
 
+(construct_main_menu)=
 
-.. _construct_main_menu:
-
-``construct_main_menu``
-~~~~~~~~~~~~~~~~~~~~~~~
-
-  Called just before the Wagtail admin menu is output, to allow the list of menu items to be modified. The callable passed to this hook will receive a ``request`` object and a list of ``menu_items``, and should modify ``menu_items`` in-place as required. Adding menu items should generally be done through the ``register_admin_menu_item`` hook instead - items added through ``construct_main_menu`` will not have their ``is_shown`` check applied.
+### `construct_main_menu`
 
-  .. code-block:: python
+Called just before the Wagtail admin menu is output, to allow the list of menu items to be modified. The callable passed to this hook will receive a `request` object and a list of `menu_items`, and should modify `menu_items` in-place as required. Adding menu items should generally be done through the `register_admin_menu_item` hook instead - items added through `construct_main_menu` will not have their `is_shown` check applied.
 
-    from wagtail import hooks
+```python
+from wagtail import hooks
 
-    @hooks.register('construct_main_menu')
-    def hide_explorer_menu_item_from_frank(request, menu_items):
-      if request.user.username == 'frank':
-        menu_items[:] = [item for item in menu_items if item.name != 'explorer']
+@hooks.register('construct_main_menu')
+def hide_explorer_menu_item_from_frank(request, menu_items):
+  if request.user.username == 'frank':
+    menu_items[:] = [item for item in menu_items if item.name != 'explorer']
+```
 
+(describe_collection_contents)=
 
-.. _describe_collection_contents:
+### `describe_collection_contents`
 
-``describe_collection_contents``
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+Called when Wagtail needs to find out what objects exist in a collection, if any. Currently this happens on the confirmation before deleting a collection, to ensure that non-empty collections cannot be deleted. The callable passed to this hook will receive a `collection` object, and should return either `None` (to indicate no objects in this collection), or a dict containing the following keys:
 
-  Called when Wagtail needs to find out what objects exist in a collection, if any. Currently this happens on the confirmation before deleting a collection, to ensure that non-empty collections cannot be deleted. The callable passed to this hook will receive a ``collection`` object, and should return either ``None`` (to indicate no objects in this collection), or a dict containing the following keys:
+-   `count` - A numeric count of items in this collection
+-   `count_text` - A human-readable string describing the number of items in this collection, such as "3 documents". (Sites with multi-language support should return a translatable string here, most likely using the `django.utils.translation.ngettext` function.)
+-   `url` (optional) - A URL to an index page that lists the objects being described.
 
-``count``
-  A numeric count of items in this collection
-
-``count_text``
-  A human-readable string describing the number of items in this collection, such as "3 documents". (Sites with multi-language support should return a translatable string here, most likely using the ``django.utils.translation.ngettext`` function.)
-
-``url`` (optional)
-  A URL to an index page that lists the objects being described.
-
-
-``register_account_settings_panel``
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+### `register_account_settings_panel`
 
 Registers a new settings panel class to add to the "Account" view in the admin.
 
-This hook can be added to a sub-class of ``BaseSettingsPanel``. For example:
-
-  .. code-block:: python
+This hook can be added to a sub-class of `BaseSettingsPanel`. For example:
 
-    from wagtail.admin.views.account import BaseSettingsPanel
-    from wagtail import hooks
+```python
+from wagtail.admin.views.account import BaseSettingsPanel
+from wagtail import hooks
 
-    @hooks.register('register_account_settings_panel')
-    class CustomSettingsPanel(BaseSettingsPanel):
-        name = 'custom'
-        title = "My custom settings"
-        order = 500
-        form_class = CustomSettingsForm
+@hooks.register('register_account_settings_panel')
+class CustomSettingsPanel(BaseSettingsPanel):
+    name = 'custom'
+    title = "My custom settings"
+    order = 500
+    form_class = CustomSettingsForm
+```
 
 Alternatively, it can also be added to a function. For example, this function is equivalent to the above:
 
-  .. code-block:: python
-
-    from wagtail.admin.views.account import BaseSettingsPanel
-    from wagtail import hooks
-
-    class CustomSettingsPanel(BaseSettingsPanel):
-        name = 'custom'
-        title = "My custom settings"
-        order = 500
-        form_class = CustomSettingsForm
-
-    @hooks.register('register_account_settings_panel')
-    def register_custom_settings_panel(request, user, profile):
-        return CustomSettingsPanel(request, user, profile)
-
-More details about the options that are available can be found at :doc:`/extending/custom_account_settings`.
-
-
-.. _register_account_menu_item:
-
-``register_account_menu_item``
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-  Add an item to the "More actions" tab on the "Account" page within the Wagtail admin.
-  The callable for this hook should return a dict with the keys
-  ``url``, ``label`` and ``help_text``. For example:
-
-  .. code-block:: python
-
-    from django.urls import reverse
-    from wagtail import hooks
-
-    @hooks.register('register_account_menu_item')
-    def register_account_delete_account(request):
-        return {
-            'url': reverse('delete-account'),
-            'label': 'Delete account',
-            'help_text': 'This permanently deletes your account.'
-        }
-
+```python
+from wagtail.admin.views.account import BaseSettingsPanel
+from wagtail import hooks
 
+class CustomSettingsPanel(BaseSettingsPanel):
+    name = 'custom'
+    title = "My custom settings"
+    order = 500
+    form_class = CustomSettingsForm
 
-.. _register_admin_menu_item:
+@hooks.register('register_account_settings_panel')
+def register_custom_settings_panel(request, user, profile):
+    return CustomSettingsPanel(request, user, profile)
+```
 
-``register_admin_menu_item``
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+More details about the options that are available can be found at [](custom_account_settings).
 
-  Add an item to the Wagtail admin menu. The callable passed to this hook must return an instance of ``wagtail.admin.menu.MenuItem``. New items can be constructed from the ``MenuItem`` class by passing in a ``label`` which will be the text in the menu item, and the URL of the admin page you want the menu item to link to (usually by calling ``reverse()`` on the admin view you've set up). Additionally, the following keyword arguments are accepted:
+(register_account_menu_item)=
 
-  :name: an internal name used to identify the menu item; defaults to the slugified form of the label.
-  :icon_name: icon to display against the menu item; no defaults, optional, but should be set for top-level menu items so they can be identified when collapsed.
-  :classnames: additional classnames applied to the link
-  :order: an integer which determines the item's position in the menu
+### `register_account_menu_item`
 
-  For menu items that are only available to superusers, the subclass ``wagtail.admin.menu.AdminOnlyMenuItem`` can be used in place of ``MenuItem``.
+Add an item to the "More actions" tab on the "Account" page within the Wagtail admin.
+The callable for this hook should return a dict with the keys
+`url`, `label` and `help_text`. For example:
 
-  ``MenuItem`` can be further subclassed to customise its initialisation or conditionally show or hide the item for specific requests (for example, to apply permission checks); see the source code (``wagtail/admin/menu.py``) for details.
+```python
+from django.urls import reverse
+from wagtail import hooks
 
-  .. code-block:: python
+@hooks.register('register_account_menu_item')
+def register_account_delete_account(request):
+    return {
+        'url': reverse('delete-account'),
+        'label': 'Delete account',
+        'help_text': 'This permanently deletes your account.'
+    }
+```
 
-    from django.urls import reverse
+(register_admin_menu_item)=
 
-    from wagtail import hooks
-    from wagtail.admin.menu import MenuItem
+### `register_admin_menu_item`
 
-    @hooks.register('register_admin_menu_item')
-    def register_frank_menu_item():
-      return MenuItem('Frank', reverse('frank'), icon_name='folder-inverse', order=10000)
+Add an item to the Wagtail admin menu. The callable passed to this hook must return an instance of `wagtail.admin.menu.MenuItem`. New items can be constructed from the `MenuItem` class by passing in a `label` which will be the text in the menu item, and the URL of the admin page you want the menu item to link to (usually by calling `reverse()` on the admin view you've set up). Additionally, the following keyword arguments are accepted:
 
+-   `name` - an internal name used to identify the menu item; defaults to the slugified form of the label.
+-   `icon_name` - icon to display against the menu item; no defaults, optional, but should be set for top-level menu items so they can be identified when collapsed.
+-   `classnames` - additional classnames applied to the link
+-   `order` - an integer which determines the item's position in the menu
 
-.. _register_admin_urls:
+For menu items that are only available to superusers, the subclass `wagtail.admin.menu.AdminOnlyMenuItem` can be used in place of `MenuItem`.
 
-``register_admin_urls``
-~~~~~~~~~~~~~~~~~~~~~~~
+`MenuItem` can be further subclassed to customise its initialisation or conditionally show or hide the item for specific requests (for example, to apply permission checks); see the source code (`wagtail/admin/menu.py`) for details.
 
-  Register additional admin page URLs. The callable fed into this hook should return a list of Django URL patterns which define the structure of the pages and endpoints of your extension to the Wagtail admin. For more about vanilla Django URLconfs and views, see :doc:`url dispatcher <django:topics/http/urls>`.
+```python
+from django.urls import reverse
 
-  .. code-block:: python
+from wagtail import hooks
+from wagtail.admin.menu import MenuItem
 
-    from django.http import HttpResponse
-    from django.urls import path
+@hooks.register('register_admin_menu_item')
+def register_frank_menu_item():
+  return MenuItem('Frank', reverse('frank'), icon_name='folder-inverse', order=10000)
+```
 
-    from wagtail import hooks
+(register_admin_urls)=
 
-    def admin_view(request):
-      return HttpResponse(
-        "I have approximate knowledge of many things!",
-        content_type="text/plain")
+### `register_admin_urls`
 
-    @hooks.register('register_admin_urls')
-    def urlconf_time():
-      return [
-        path('how_did_you_almost_know_my_name/', admin_view, name='frank'),
-      ]
+Register additional admin page URLs. The callable fed into this hook should return a list of Django URL patterns which define the structure of the pages and endpoints of your extension to the Wagtail admin. For more about vanilla Django URLconfs and views, see [url dispatcher](django:topics/http/urls).
 
+```python
+from django.http import HttpResponse
+from django.urls import path
 
-.. _register_group_permission_panel:
+from wagtail import hooks
 
-``register_group_permission_panel``
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+def admin_view(request):
+  return HttpResponse(
+    "I have approximate knowledge of many things!",
+    content_type="text/plain")
 
-  Add a new panel to the Groups form in the 'settings' area. The callable passed to this hook must return a ModelForm / ModelFormSet-like class, with a constructor that accepts a group object as its ``instance`` keyword argument, and which implements the methods ``save``, ``is_valid``, and ``as_admin_panel`` (which returns the HTML to be included on the group edit page).
+@hooks.register('register_admin_urls')
+def urlconf_time():
+  return [
+    path('how_did_you_almost_know_my_name/', admin_view, name='frank'),
+  ]
+```
 
+(register_group_permission_panel)=
 
-.. _register_settings_menu_item:
+### `register_group_permission_panel`
 
-``register_settings_menu_item``
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+Add a new panel to the Groups form in the 'settings' area. The callable passed to this hook must return a ModelForm / ModelFormSet-like class, with a constructor that accepts a group object as its `instance` keyword argument, and which implements the methods `save`, `is_valid`, and `as_admin_panel` (which returns the HTML to be included on the group edit page).
 
-  As ``register_admin_menu_item``, but registers menu items into the 'Settings' sub-menu rather than the top-level menu.
+(register_settings_menu_item)=
 
+### `register_settings_menu_item`
 
-.. _construct_settings_menu:
+As `register_admin_menu_item`, but registers menu items into the 'Settings' sub-menu rather than the top-level menu.
 
-``construct_settings_menu``
-~~~~~~~~~~~~~~~~~~~~~~~~~~~
+(construct_settings_menu)=
 
-  As ``construct_main_menu``, but modifies the 'Settings' sub-menu rather than the top-level menu.
+### `construct_settings_menu`
 
+As `construct_main_menu`, but modifies the 'Settings' sub-menu rather than the top-level menu.
 
-.. _register_reports_menu_item:
+(register_reports_menu_item)=
 
-``register_reports_menu_item``
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+### `register_reports_menu_item`
 
-  As ``register_admin_menu_item``, but registers menu items into the 'Reports' sub-menu rather than the top-level menu.
+As `register_admin_menu_item`, but registers menu items into the 'Reports' sub-menu rather than the top-level menu.
 
+(construct_reports_menu)=
 
-.. _construct_reports_menu:
+### `construct_reports_menu`
 
-``construct_reports_menu``
-~~~~~~~~~~~~~~~~~~~~~~~~~~~
+As `construct_main_menu`, but modifies the 'Reports' sub-menu rather than the top-level menu.
 
-  As ``construct_main_menu``, but modifies the 'Reports' sub-menu rather than the top-level menu.
+(register_admin_search_area)=
 
+### `register_admin_search_area`
 
-.. _register_admin_search_area:
+Add an item to the Wagtail admin search "Other Searches". Behaviour of this hook is similar to `register_admin_menu_item`. The callable passed to this hook must return an instance of `wagtail.admin.search.SearchArea`. New items can be constructed from the `SearchArea` class by passing the following parameters:
 
-``register_admin_search_area``
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+-   `label` - text displayed in the "Other Searches" option box.
+-   `name` - an internal name used to identify the search option; defaults to the slugified form of the label.
+-   `url` - the URL of the target search page.
+-   `classnames` - arbitrary CSS classnames applied to the link
+-   `icon_name` - icon to display next to the label.
+-   `attrs` - additional HTML attributes to apply to the link.
+-   `order` - an integer which determines the item's position in the list of options.
 
-  Add an item to the Wagtail admin search "Other Searches". Behaviour of this hook is similar to ``register_admin_menu_item``. The callable passed to this hook must return an instance of ``wagtail.admin.search.SearchArea``. New items can be constructed from the ``SearchArea`` class by passing the following parameters:
+Setting the URL can be achieved using reverse() on the target search page. The GET parameter 'q' will be appended to the given URL.
 
-  :label: text displayed in the "Other Searches" option box.
-  :name: an internal name used to identify the search option; defaults to the slugified form of the label.
-  :url: the URL of the target search page.
-  :classnames: arbitrary CSS classnames applied to the link
-  :icon_name: icon to display next to the label.
-  :attrs: additional HTML attributes to apply to the link.
-  :order: an integer which determines the item's position in the list of options.
+A template tag, `search_other` is provided by the `wagtailadmin_tags` template module. This tag takes a single, optional parameter, `current`, which allows you to specify the `name` of the search option currently active. If the parameter is not given, the hook defaults to a reverse lookup of the page's URL for comparison against the `url` parameter.
 
-  Setting the URL can be achieved using reverse() on the target search page. The GET parameter 'q' will be appended to the given URL.
+`SearchArea` can be subclassed to customise the HTML output, specify JavaScript files required by the option, or conditionally show or hide the item for specific requests (for example, to apply permission checks); see the source code (`wagtail/admin/search.py`) for details.
 
-  A template tag, ``search_other`` is provided by the ``wagtailadmin_tags`` template module. This tag takes a single, optional parameter, ``current``, which allows you to specify the ``name`` of the search option currently active. If the parameter is not given, the hook defaults to a reverse lookup of the page's URL for comparison against the ``url`` parameter.
+```python
+from django.urls import reverse
+from wagtail import hooks
+from wagtail.admin.search import SearchArea
 
+@hooks.register('register_admin_search_area')
+def register_frank_search_area():
+    return SearchArea('Frank', reverse('frank'), icon_name='folder-inverse', order=10000)
+```
 
-  ``SearchArea`` can be subclassed to customise the HTML output, specify JavaScript files required by the option, or conditionally show or hide the item for specific requests (for example, to apply permission checks); see the source code (``wagtail/admin/search.py``) for details.
+(register_permissions)=
 
-  .. code-block:: python
+### `register_permissions`
 
-    from django.urls import reverse
-    from wagtail import hooks
-    from wagtail.admin.search import SearchArea
+Return a QuerySet of `Permission` objects to be shown in the Groups administration area.
 
-    @hooks.register('register_admin_search_area')
-    def register_frank_search_area():
-        return SearchArea('Frank', reverse('frank'), icon_name='folder-inverse', order=10000)
-
-
-.. _register_permissions:
-
-``register_permissions``
-~~~~~~~~~~~~~~~~~~~~~~~~
-
-  Return a QuerySet of ``Permission`` objects to be shown in the Groups administration area.
-
-  .. code-block:: python
-
-      from django.contrib.auth.models import Permission
-      from wagtail import hooks
-
-
-      @hooks.register('register_permissions')
-      def register_permissions():
-          app = 'blog'
-          model = 'extramodelset'
+```python
+  from django.contrib.auth.models import Permission
+  from wagtail import hooks
 
-          return Permission.objects.filter(content_type__app_label=app, codename__in=[
-              f"view_{model}", f"add_{model}", f"change_{model}", f"delete_{model}"
-          ])
 
+  @hooks.register('register_permissions')
+  def register_permissions():
+      app = 'blog'
+      model = 'extramodelset'
 
-.. _filter_form_submissions_for_user:
+      return Permission.objects.filter(content_type__app_label=app, codename__in=[
+          f"view_{model}", f"add_{model}", f"change_{model}", f"delete_{model}"
+      ])
+```
 
-``filter_form_submissions_for_user``
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+(filter_form_submissions_for_user)=
 
-  Allows access to form submissions to be customised on a per-user, per-form basis.
+### `filter_form_submissions_for_user`
 
-  This hook takes two parameters:
-   - The user attempting to access form submissions
-   - A ``QuerySet`` of form pages
+Allows access to form submissions to be customised on a per-user, per-form basis.
 
-  The hook must return a ``QuerySet`` containing a subset of these form pages which the user is allowed to access the submissions for.
+This hook takes two parameters:
 
-  For example, to prevent non-superusers from accessing form submissions:
+-   The user attempting to access form submissions
+-   A `QuerySet` of form pages
 
-  .. code-block:: python
+The hook must return a `QuerySet` containing a subset of these form pages which the user is allowed to access the submissions for.
 
-    from wagtail import hooks
+For example, to prevent non-superusers from accessing form submissions:
 
+```python
+from wagtail import hooks
 
-    @hooks.register('filter_form_submissions_for_user')
-    def construct_forms_for_user(user, queryset):
-        if not user.is_superuser:
-            queryset = queryset.none()
 
-        return queryset
+@hooks.register('filter_form_submissions_for_user')
+def construct_forms_for_user(user, queryset):
+    if not user.is_superuser:
+        queryset = queryset.none()
 
+    return queryset
+```
 
-Editor interface
-----------------
+## Editor interface
 
 Hooks for customising the editing interface for pages and snippets.
 
+(register_rich_text_features)=
 
-.. _register_rich_text_features:
-
-``register_rich_text_features``
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-  Rich text fields in Wagtail work with a list of 'feature' identifiers that determine which editing controls are available in the editor, and which elements are allowed in the output; for example, a rich text field defined as ``RichTextField(features=['h2', 'h3', 'bold', 'italic', 'link'])`` would allow headings, bold / italic formatting and links, but not (for example) bullet lists or images. The ``register_rich_text_features`` hook allows new feature identifiers to be defined - see :ref:`rich_text_features` for details.
-
-
-.. _insert_editor_css:
-
-``insert_editor_css``
-~~~~~~~~~~~~~~~~~~~~~
-
-  Add additional CSS files or snippets to the page editor.
-
-  .. code-block:: python
-
-    from django.templatetags.static import static
-    from django.utils.html import format_html
+### `register_rich_text_features`
 
-    from wagtail import hooks
+Rich text fields in Wagtail work with a list of 'feature' identifiers that determine which editing controls are available in the editor, and which elements are allowed in the output; for example, a rich text field defined as `RichTextField(features=['h2', 'h3', 'bold', 'italic', 'link'])` would allow headings, bold / italic formatting and links, but not (for example) bullet lists or images. The `register_rich_text_features` hook allows new feature identifiers to be defined - see [](rich_text_features) for details.
 
-    @hooks.register('insert_editor_css')
-    def editor_css():
-        return format_html(
-            '<link rel="stylesheet" href="{}">',
-            static('demo/css/vendor/font-awesome/css/font-awesome.min.css')
-        )
+(insert_editor_css)=
 
+### `insert_editor_css`
 
-.. _insert_global_admin_css:
+Add additional CSS files or snippets to the page editor.
 
-``insert_global_admin_css``
-~~~~~~~~~~~~~~~~~~~~~~~~~~~
+```python
+from django.templatetags.static import static
+from django.utils.html import format_html
 
-  Add additional CSS files or snippets to all admin pages.
+from wagtail import hooks
 
-  .. code-block:: python
+@hooks.register('insert_editor_css')
+def editor_css():
+    return format_html(
+        '<link rel="stylesheet" href="{}">',
+        static('demo/css/vendor/font-awesome/css/font-awesome.min.css')
+    )
+```
 
-    from django.utils.html import format_html
-    from django.templatetags.static import static
+(insert_global_admin_css)=
 
-    from wagtail import hooks
+### `insert_global_admin_css`
 
-    @hooks.register('insert_global_admin_css')
-    def global_admin_css():
-        return format_html('<link rel="stylesheet" href="{}">', static('my/wagtail/theme.css'))
+Add additional CSS files or snippets to all admin pages.
 
+```python
+from django.utils.html import format_html
+from django.templatetags.static import static
 
-.. _insert_editor_js:
+from wagtail import hooks
 
-``insert_editor_js``
-~~~~~~~~~~~~~~~~~~~~
+@hooks.register('insert_global_admin_css')
+def global_admin_css():
+    return format_html('<link rel="stylesheet" href="{}">', static('my/wagtail/theme.css'))
+```
 
-  Add additional JavaScript files or code snippets to the page editor.
+(insert_editor_js)=
 
-  .. code-block:: python
+### `insert_editor_js`
 
-    from django.utils.html import format_html_join
-    from django.utils.safestring import mark_safe
-    from django.templatetags.static import static
+Add additional JavaScript files or code snippets to the page editor.
 
-    from wagtail import hooks
+```python
+from django.utils.html import format_html_join
+from django.utils.safestring import mark_safe
+from django.templatetags.static import static
 
-    @hooks.register('insert_editor_js')
-    def editor_js():
-        js_files = [
-            'js/fireworks.js', # https://fireworks.js.org
-        ]
-        js_includes = format_html_join('\n', '<script src="{0}"></script>',
-            ((static(filename),) for filename in js_files)
-        )
-        return js_includes + mark_safe(
-            """
-            <script>
-                window.addEventListener('DOMContentLoaded', (event) => {
-                    var container = document.createElement('div');
-                    container.style.cssText = 'position: fixed; width: 100%; height: 100%; z-index: 100; top: 0; left: 0; pointer-events: none;';
-                    container.id = 'fireworks';
-                    document.getElementById('main').prepend(container);
-                    var options = { "acceleration": 1.2, "autoresize": true, "mouse": { "click": true, "max": 3 } };
-                    var fireworks = new Fireworks(document.getElementById('fireworks'), options);
-                    fireworks.start();
-                });
-            </script>
-            """
-        )
+from wagtail import hooks
 
+@hooks.register('insert_editor_js')
+def editor_js():
+    js_files = [
+        'js/fireworks.js', # https://fireworks.js.org
+    ]
+    js_includes = format_html_join('\n', '<script src="{0}"></script>',
+        ((static(filename),) for filename in js_files)
+    )
+    return js_includes + mark_safe(
+        """
+        <script>
+            window.addEventListener('DOMContentLoaded', (event) => {
+                var container = document.createElement('div');
+                container.style.cssText = 'position: fixed; width: 100%; height: 100%; z-index: 100; top: 0; left: 0; pointer-events: none;';
+                container.id = 'fireworks';
+                document.getElementById('main').prepend(container);
+                var options = { "acceleration": 1.2, "autoresize": true, "mouse": { "click": true, "max": 3 } };
+                var fireworks = new Fireworks(document.getElementById('fireworks'), options);
+                fireworks.start();
+            });
+        </script>
+        """
+    )
+```
 
-.. _insert_global_admin_js:
+(insert_global_admin_js)=
 
-``insert_global_admin_js``
-~~~~~~~~~~~~~~~~~~~~~~~~~~
+### `insert_global_admin_js`
 
-  Add additional JavaScript files or code snippets to all admin pages.
+Add additional JavaScript files or code snippets to all admin pages.
 
-  .. code-block:: python
+```python
+from django.utils.safestring import mark_safe
 
-    from django.utils.safestring import mark_safe
+from wagtail import hooks
 
-    from wagtail import hooks
+@hooks.register('insert_global_admin_js')
+def global_admin_js():
+    return mark_safe(
+        '<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r74/three.js"></script>',
+    )
+```
 
-    @hooks.register('insert_global_admin_js')
-    def global_admin_js():
-        return mark_safe(
-            '<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r74/three.js"></script>',
-        )
+(register_page_header_buttons)=
 
+### `register_page_header_buttons`
 
-.. register_page_header_buttons:
+Add buttons to the secondary dropdown menu in the page edit view. This works similarly to the `register_page_listing_buttons` hook.
 
-``register_page_header_buttons``
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+This example will add a simple button to the secondary dropdown menu:
 
-  Add buttons to the secondary dropdown menu in the page edit view. This works similarly to the ``register_page_listing_buttons`` hook.
+```python
+from wagtail.admin import widgets as wagtailadmin_widgets
 
-  This example will add a simple button to the secondary dropdown menu:
+@hooks.register('register_page_header_buttons')
+def page_header_buttons(page, page_perms, next_url=None):
+    yield wagtailadmin_widgets.Button(
+        'A dropdown button',
+        '/goes/to/a/url/',
+        priority=60
+    )
+```
 
-  .. code-block:: python
+The arguments passed to the hook are as follows:
 
-    from wagtail.admin import widgets as wagtailadmin_widgets
+-   `page` - the page object to generate the button for
+-   `page_perms` - a `PagePermissionTester` object that can be queried to determine the current user's permissions on the given page
+-   `next_url` - the URL that the linked action should redirect back to on completion of the action, if the view supports it
 
-    @hooks.register('register_page_header_buttons')
-    def page_header_buttons(page, page_perms, next_url=None):
-        yield wagtailadmin_widgets.Button(
-            'A dropdown button',
-            '/goes/to/a/url/',
-            priority=60
-        )
+The `priority` argument controls the order the buttons are displayed in the dropdown. Buttons are ordered from low to high priority, so a button with `priority=10` will be displayed before a button with `priority=60`.
 
-  The arguments passed to the hook are as follows:
-
-  * ``page`` - the page object to generate the button for
-  * ``page_perms`` - a ``PagePermissionTester`` object that can be queried to determine the current user's permissions on the given page
-  * ``next_url`` - the URL that the linked action should redirect back to on completion of the action, if the view supports it
-
-  The ``priority`` argument controls the order the buttons are displayed in the dropdown. Buttons are ordered from low to high priority, so a button with ``priority=10`` will be displayed before a button with ``priority=60``.
-
-
-Editor workflow
----------------
+## Editor workflow
 
 Hooks for customising the way users are directed through the process of creating page content.
 
+(after_create_page)=
 
-.. _after_create_page:
-
-``after_create_page``
-~~~~~~~~~~~~~~~~~~~~~
-
-  Do something with a ``Page`` object after it has been saved to the database (as a published page or a revision). The callable passed to this hook should take a ``request`` object and a ``page`` object. The function does not have to return anything, but if an object with a ``status_code`` property is returned, Wagtail will use it as a response object. By default, Wagtail will instead redirect to the Explorer page for the new page's parent.
-
-  .. code-block:: python
-
-    from django.http import HttpResponse
-
-    from wagtail import hooks
-
-    @hooks.register('after_create_page')
-    def do_after_page_create(request, page):
-        return HttpResponse("Congrats on making content!", content_type="text/plain")
-
-  If you set attributes on a ``Page`` object, you should also call ``save_revision()``, since the edit and index view pick up their data from the revisions table rather than the actual saved page record.
-
-  .. code-block:: python
-
-      @hooks.register('after_create_page')
-      def set_attribute_after_page_create(request, page):
-         page.title = 'Persistent Title'
-         new_revision = page.save_revision()
-         if page.live:
-             # page has been created and published at the same time,
-             # so ensure that the updated title is on the published version too
-             new_revision.publish()
-
-.. _before_create_page:
-
-``before_create_page``
-~~~~~~~~~~~~~~~~~~~~~~
-
-  Called at the beginning of the "create page" view passing in the request, the parent page and page model class.
-
-  The function does not have to return anything, but if an object with a ``status_code`` property is returned, Wagtail will use it as a response object and skip the rest of the view.
+### `after_create_page`
 
-  Unlike, ``after_create_page``, this is run both for both ``GET`` and ``POST`` requests.
+Do something with a `Page` object after it has been saved to the database (as a published page or a revision). The callable passed to this hook should take a `request` object and a `page` object. The function does not have to return anything, but if an object with a `status_code` property is returned, Wagtail will use it as a response object. By default, Wagtail will instead redirect to the Explorer page for the new page's parent.
 
-  This can be used to completely override the editor on a per-view basis:
+```python
+from django.http import HttpResponse
 
-  .. code-block:: python
+from wagtail import hooks
 
-    from wagtail import hooks
+@hooks.register('after_create_page')
+def do_after_page_create(request, page):
+    return HttpResponse("Congrats on making content!", content_type="text/plain")
+```
 
-    from .models import AwesomePage
-    from .admin_views import edit_awesome_page
+If you set attributes on a `Page` object, you should also call `save_revision()`, since the edit and index view pick up their data from the revisions table rather than the actual saved page record.
 
-    @hooks.register('before_create_page')
-    def before_create_page(request, parent_page, page_class):
-        # Use a custom create view for the AwesomePage model
-        if page_class == AwesomePage:
-            return create_awesome_page(request, parent_page)
+```python
+  @hooks.register('after_create_page')
+  def set_attribute_after_page_create(request, page):
+      page.title = 'Persistent Title'
+      new_revision = page.save_revision()
+      if page.live:
+          # page has been created and published at the same time,
+          # so ensure that the updated title is on the published version too
+          new_revision.publish()
+```
 
-.. _after_delete_page:
+(before_create_page)=
 
-``after_delete_page``
-~~~~~~~~~~~~~~~~~~~~~
+### `before_create_page`
 
-  Do something after a ``Page`` object is deleted. Uses the same behaviour as ``after_create_page``.
+Called at the beginning of the "create page" view passing in the request, the parent page and page model class.
 
+The function does not have to return anything, but if an object with a `status_code` property is returned, Wagtail will use it as a response object and skip the rest of the view.
 
-.. _before_delete_page:
+Unlike, `after_create_page`, this is run both for both `GET` and `POST` requests.
 
-``before_delete_page``
-~~~~~~~~~~~~~~~~~~~~~~
+This can be used to completely override the editor on a per-view basis:
 
-  Called at the beginning of the "delete page" view passing in the request and the page object.
+```python
+from wagtail import hooks
 
-  Uses the same behaviour as ``before_create_page``, is is run both for both ``GET`` and ``POST`` requests.
+from .models import AwesomePage
+from .admin_views import edit_awesome_page
 
- .. code-block:: python
+@hooks.register('before_create_page')
+def before_create_page(request, parent_page, page_class):
+    # Use a custom create view for the AwesomePage model
+    if page_class == AwesomePage:
+        return create_awesome_page(request, parent_page)
+```
 
-    from django.shortcuts import redirect
-    from django.utils.html import format_html
+(after_delete_page)=
 
-    from wagtail.admin import messages
-    from wagtail import hooks
+### `after_delete_page`
 
-    from .models import AwesomePage
+Do something after a `Page` object is deleted. Uses the same behaviour as `after_create_page`.
 
+(before_delete_page)=
 
-    @hooks.register('before_delete_page')
-    def before_delete_page(request, page):
-        """Block awesome page deletion and show a message."""
+### `before_delete_page`
 
-        if request.method == 'POST' and page.specific_class in [AwesomePage]:
-            messages.warning(request, "Awesome pages cannot be deleted, only unpublished")
-            return redirect('wagtailadmin_pages:delete', page.pk)
+Called at the beginning of the "delete page" view passing in the request and the page object.
 
-.. _after_edit_page:
+Uses the same behaviour as `before_create_page`, is is run both for both `GET` and `POST` requests.
 
-``after_edit_page``
-~~~~~~~~~~~~~~~~~~~
+```python
+from django.shortcuts import redirect
+from django.utils.html import format_html
 
-  Do something with a ``Page`` object after it has been updated. Uses the same behaviour as ``after_create_page``.
+from wagtail.admin import messages
+from wagtail import hooks
 
+from .models import AwesomePage
 
-.. _before_edit_page:
 
-``before_edit_page``
-~~~~~~~~~~~~~~~~~~~~~
+@hooks.register('before_delete_page')
+def before_delete_page(request, page):
+    """Block awesome page deletion and show a message."""
 
-  Called at the beginning of the "edit page" view passing in the request and the page object.
+    if request.method == 'POST' and page.specific_class in [AwesomePage]:
+        messages.warning(request, "Awesome pages cannot be deleted, only unpublished")
+        return redirect('wagtailadmin_pages:delete', page.pk)
+```
 
-  Uses the same behaviour as ``before_create_page``.
+(after_edit_page)=
 
+### `after_edit_page`
 
-.. _after_publish_page:
+Do something with a `Page` object after it has been updated. Uses the same behaviour as `after_create_page`.
 
-``after_publish_page``
-~~~~~~~~~~~~~~~~~~~~~~~~
+(before_edit_page)=
 
-  Do something with a ``Page`` object after it has been published via page create view or page edit view.
+### `before_edit_page`
 
-  The function does not have to return anything, but if an object with a ``status_code`` property is returned, Wagtail will use it as a response object and skip the rest of the view.
+Called at the beginning of the "edit page" view passing in the request and the page object.
 
+Uses the same behaviour as `before_create_page`.
 
-.. _before_publish_page:
+(after_publish_page)=
 
-``before_publish_page``
-~~~~~~~~~~~~~~~~~~~~~~~~~
+### `after_publish_page`
 
-  Do something with a ``Page`` object before it has been published via page create view or page edit view.
+Do something with a `Page` object after it has been published via page create view or page edit view.
 
-  The function does not have to return anything, but if an object with a ``status_code`` property is returned, Wagtail will use it as a response object and skip the rest of the view.
+The function does not have to return anything, but if an object with a `status_code` property is returned, Wagtail will use it as a response object and skip the rest of the view.
 
+(before_publish_page)=
 
-.. _after_unpublish_page:
+### `before_publish_page`
 
-``after_unpublish_page``
-~~~~~~~~~~~~~~~~~~~~~~~~
+Do something with a `Page` object before it has been published via page create view or page edit view.
 
-  Called after unpublish action in "unpublish" view passing in the request and the page object.
+The function does not have to return anything, but if an object with a `status_code` property is returned, Wagtail will use it as a response object and skip the rest of the view.
 
-  The function does not have to return anything, but if an object with a ``status_code`` property is returned, Wagtail will use it as a response object and skip the rest of the view.
+(after_unpublish_page)=
 
+### `after_unpublish_page`
 
-.. _before_unpublish_page:
+Called after unpublish action in "unpublish" view passing in the request and the page object.
 
-``before_unpublish_page``
-~~~~~~~~~~~~~~~~~~~~~~~~~
+The function does not have to return anything, but if an object with a `status_code` property is returned, Wagtail will use it as a response object and skip the rest of the view.
 
-  Called before unpublish action in "unpublish" view passing in the request and the page object.
+(before_unpublish_page)=
 
-  The function does not have to return anything, but if an object with a ``status_code`` property is returned, Wagtail will use it as a response object and skip the rest of the view.
+### `before_unpublish_page`
 
+Called before unpublish action in "unpublish" view passing in the request and the page object.
 
-.. _after_copy_page:
+The function does not have to return anything, but if an object with a `status_code` property is returned, Wagtail will use it as a response object and skip the rest of the view.
 
-``after_copy_page``
-~~~~~~~~~~~~~~~~~~~
+(after_copy_page)=
 
-  Do something with a ``Page`` object after it has been copied passing in the request, page object and the new copied page. Uses the same behaviour as ``after_create_page``.
+### `after_copy_page`
 
+Do something with a `Page` object after it has been copied passing in the request, page object and the new copied page. Uses the same behaviour as `after_create_page`.
 
-.. _before_copy_page:
+(before_copy_page)=
 
-``before_copy_page``
-~~~~~~~~~~~~~~~~~~~~~
+### `before_copy_page`
 
-  Called at the beginning of the "copy page" view passing in the request and the page object.
+Called at the beginning of the "copy page" view passing in the request and the page object.
 
-  Uses the same behaviour as ``before_create_page``.
+Uses the same behaviour as `before_create_page`.
 
-.. _after_move_page:
+(after_move_page)=
 
-``after_move_page``
-~~~~~~~~~~~~~~~~~~~
+### `after_move_page`
 
-  Do something with a ``Page`` object after it has been moved passing in the request and page object. Uses the same behaviour as ``after_create_page``.
+Do something with a `Page` object after it has been moved passing in the request and page object. Uses the same behaviour as `after_create_page`.
 
+(before_move_page)=
 
-.. _before_move_page:
+### `before_move_page`
 
-``before_move_page``
-~~~~~~~~~~~~~~~~~~~~~
+Called at the beginning of the "move page" view passing in the request, the page object and the destination page object.
 
-  Called at the beginning of the "move page" view passing in the request, the page object and the destination page object.
+Uses the same behaviour as `before_create_page`.
 
-  Uses the same behaviour as ``before_create_page``.
+(before_convert_alias_page)=
 
+### `before_convert_alias_page`
 
-.. _before_convert_alias_page:
+Called at the beginning of the `convert_alias` view, which is responsible for converting alias pages into normal Wagtail pages.
 
-``before_convert_alias_page``
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+The request and the page being converted are passed in as arguments to the hook.
 
-  Called at the beginning of the ``convert_alias`` view, which is responsible for converting alias pages into normal Wagtail pages.
+The function does not have to return anything, but if an object with a `status_code` property is returned, Wagtail will use it as a response object and skip the rest of the view.
 
-  The request and the page being converted are passed in as arguments to the hook.
+(after_convert_alias_page)=
 
-  The function does not have to return anything, but if an object with a ``status_code`` property is returned, Wagtail will use it as a response object and skip the rest of the view.
+### `after_convert_alias_page`
 
+Do something with a `Page` object after it has been converted from an alias.
 
-.. _after_convert_alias_page:
+The request and the page that was just converted are passed in as arguments to the hook.
 
-``after_convert_alias_page``
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+The function does not have to return anything, but if an object with a `status_code` property is returned, Wagtail will use it as a response object and skip the rest of the view.
 
-  Do something with a ``Page`` object after it has been converted from an alias.
+(construct_translated_pages_to_cascade_actions)=
 
-  The request and the page that was just converted are passed in as arguments to the hook.
+### `construct_translated_pages_to_cascade_actions`
 
-  The function does not have to return anything, but if an object with a ``status_code`` property is returned, Wagtail will use it as a response object and skip the rest of the view.
+Return additional pages to process in a synced tree setup.
 
+This hook is only triggered on unpublishing a page when `WAGTAIL_I18N_ENABLED = True`.
 
-.. _construct_translated_pages_to_cascade_actions:
+The list of pages and the action are passed in as arguments to the hook.
 
-``construct_translated_pages_to_cascade_actions``
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+The function should return a dictionary with the page from the pages list as key, and a list of additional pages to perform the action on.
+We recommend they are non-aliased, direct translations of the pages from the function argument.
 
-  Return additional pages to process in a synced tree setup.
+(register_page_action_menu_item)=
 
-  This hook is only triggered on unpublishing a page when ``WAGTAIL_I18N_ENABLED = True``.
+### `register_page_action_menu_item`
 
-  The list of pages and the action are passed in as arguments to the hook.
+Add an item to the popup menu of actions on the page creation and edit views. The callable passed to this hook must return an instance of `wagtail.admin.action_menu.ActionMenuItem`. `ActionMenuItem` is a subclass of [Component](creating_template_components) and so the rendering of the menu item can be customised through `template_name`, `get_context_data`, `render_html` and `Media`. In addition, the following attributes and methods are available to be overridden:
 
-  The function should return a dictionary with the page from the pages list as key, and a list of additional pages to perform the action on.
-  We recommend they are non-aliased, direct translations of the pages from the function argument.
+-   `order` - an integer (default 100) which determines the item's position in the menu. Can also be passed as a keyword argument to the object constructor. The lowest-numbered item in this sequence will be selected as the default menu item; as standard, this is "Save draft" (which has an `order` of 0).
+-   `label` - the displayed text of the menu item
+-   `get_url` - a method which returns a URL for the menu item to link to; by default, returns `None` which causes the menu item to behave as a form submit button instead
+-   `name` - value of the `name` attribute of the submit button, if no URL is specified
+-   `icon_name` - icon to display against the menu item
+-   `classname` - a `class` attribute value to add to the button element
+-   `is_shown` - a method which returns a boolean indicating whether the menu item should be shown; by default, true except when editing a locked page
 
+The `get_url`, `is_shown`, `get_context_data` and `render_html` methods all accept a context dictionary containing the following fields:
 
-.. _register_page_action_menu_item:
+-   `view` - name of the current view: `'create'`, `'edit'` or `'revisions_revert'`
+-   `page` - for `view` = `'edit'` or `'revisions_revert'`, the page being edited
+-   `parent_page` - for `view` = `'create'`, the parent page of the page being created
+-   `request` - the current request object
+-   `user_page_permissions` - a `UserPagePermissionsProxy` object for the current user, to test permissions against
 
-``register_page_action_menu_item``
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+```python
+from wagtail import hooks
+from wagtail.admin.action_menu import ActionMenuItem
 
-  Add an item to the popup menu of actions on the page creation and edit views. The callable passed to this hook must return an instance of ``wagtail.admin.action_menu.ActionMenuItem``. ``ActionMenuItem`` is a subclass of :ref:`Component <creating_template_components>` and so the rendering of the menu item can be customised through ``template_name``, ``get_context_data``, ``render_html`` and ``Media``. In addition, the following attributes and methods are available to be overridden:
+class GuacamoleMenuItem(ActionMenuItem):
+    name = 'action-guacamole'
+    label = "Guacamole"
 
-  :order: an integer (default 100) which determines the item's position in the menu. Can also be passed as a keyword argument to the object constructor. The lowest-numbered item in this sequence will be selected as the default menu item; as standard, this is "Save draft" (which has an ``order`` of 0).
-  :label: the displayed text of the menu item
-  :get_url: a method which returns a URL for the menu item to link to; by default, returns ``None`` which causes the menu item to behave as a form submit button instead
-  :name: value of the ``name`` attribute of the submit button, if no URL is specified
-  :icon_name: icon to display against the menu item
-  :classname: a ``class`` attribute value to add to the button element
-  :is_shown: a method which returns a boolean indicating whether the menu item should be shown; by default, true except when editing a locked page
+    def get_url(self, context):
+        return "https://www.youtube.com/watch?v=dNJdJIwCF_Y"
 
-  The ``get_url``, ``is_shown``, ``get_context_data`` and ``render_html`` methods all accept a context dictionary containing the following fields:
 
-  :view: name of the current view: ``'create'``, ``'edit'`` or ``'revisions_revert'``
-  :page: For ``view`` = ``'edit'`` or ``'revisions_revert'``, the page being edited
-  :parent_page: For ``view`` = ``'create'``, the parent page of the page being created
-  :request: The current request object
-  :user_page_permissions: a ``UserPagePermissionsProxy`` object for the current user, to test permissions against
+@hooks.register('register_page_action_menu_item')
+def register_guacamole_menu_item():
+    return GuacamoleMenuItem(order=10)
+```
 
-  .. code-block:: python
+(construct_page_action_menu)=
 
-    from wagtail import hooks
-    from wagtail.admin.action_menu import ActionMenuItem
+### `construct_page_action_menu`
 
-    class GuacamoleMenuItem(ActionMenuItem):
-        name = 'action-guacamole'
-        label = "Guacamole"
+Modify the final list of action menu items on the page creation and edit views. The callable passed to this hook receives a list of `ActionMenuItem` objects, a request object and a context dictionary as per `register_page_action_menu_item`, and should modify the list of menu items in-place.
 
-        def get_url(self, context):
-            return "https://www.youtube.com/watch?v=dNJdJIwCF_Y"
+```python
+@hooks.register('construct_page_action_menu')
+def remove_submit_to_moderator_option(menu_items, request, context):
+    menu_items[:] = [item for item in menu_items if item.name != 'action-submit']
+```
 
+The `construct_page_action_menu` hook is called after the menu items have been sorted by their order attributes, and so setting a menu item's order will have no effect at this point. Instead, items can be reordered by changing their position in the list, with the first item being selected as the default action. For example, to change the default action to Publish:
 
-    @hooks.register('register_page_action_menu_item')
-    def register_guacamole_menu_item():
-        return GuacamoleMenuItem(order=10)
+```python
+@hooks.register('construct_page_action_menu')
+def make_publish_default_action(menu_items, request, context):
+    for (index, item) in enumerate(menu_items):
+        if item.name == 'action-publish':
+            # move to top of list
+            menu_items.pop(index)
+            menu_items.insert(0, item)
+            break
+```
 
+(construct_page_listing_buttons)=
 
-.. _construct_page_action_menu:
+### `construct_page_listing_buttons`
 
-``construct_page_action_menu``
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+Modify the final list of page listing buttons in the page explorer. The callable passed to this hook receives a list of `PageListingButton` objects, a page, a page perms object, and a context dictionary as per `register_page_listing_buttons`, and should modify the list of listing items in-place.
 
-  Modify the final list of action menu items on the page creation and edit views. The callable passed to this hook receives a list of ``ActionMenuItem`` objects, a request object and a context dictionary as per ``register_page_action_menu_item``, and should modify the list of menu items in-place.
+```python
+@hooks.register('construct_page_listing_buttons')
+def remove_page_listing_button_item(buttons, page, page_perms, is_parent=False, context=None):
+    if is_parent:
+        buttons.pop() # removes the last 'more' dropdown button on the parent page listing buttons
+```
 
+(construct_wagtail_userbar)=
 
-  .. code-block:: python
+### `construct_wagtail_userbar`
 
-    @hooks.register('construct_page_action_menu')
-    def remove_submit_to_moderator_option(menu_items, request, context):
-        menu_items[:] = [item for item in menu_items if item.name != 'action-submit']
+Add or remove items from the wagtail userbar. Add, edit, and moderation tools are provided by default. The callable passed into the hook must take the `request` object and a list of menu objects, `items`. The menu item objects must have a `render` method which can take a `request` object and return the HTML string representing the menu item. See the userbar templates and menu item classes for more information.
 
+```python
+from wagtail import hooks
 
-  The ``construct_page_action_menu`` hook is called after the menu items have been sorted by their order attributes, and so setting a menu item's order will have no effect at this point. Instead, items can be reordered by changing their position in the list, with the first item being selected as the default action. For example, to change the default action to Publish:
+class UserbarPuppyLinkItem:
+    def render(self, request):
+        return '<li><a href="http://cuteoverload.com/tag/puppehs/" ' \
+            + 'target="_parent" role="menuitem" class="action icon icon-wagtail">Puppies!</a></li>'
 
-  .. code-block:: python
+@hooks.register('construct_wagtail_userbar')
+def add_puppy_link_item(request, items):
+    return items.append( UserbarPuppyLinkItem() )
+```
 
-    @hooks.register('construct_page_action_menu')
-    def make_publish_default_action(menu_items, request, context):
-        for (index, item) in enumerate(menu_items):
-            if item.name == 'action-publish':
-                # move to top of list
-                menu_items.pop(index)
-                menu_items.insert(0, item)
-                break
+## Admin workflow
 
-
-.. construct_page_listing_buttons:
-
-``construct_page_listing_buttons``
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-  Modify the final list of page listing buttons in the page explorer. The
-  callable passed to this hook receives a list of ``PageListingButton`` objects, a page,
-  a page perms object, and a context dictionary as per ``register_page_listing_buttons``,
-  and should modify the list of listing items in-place.
-
-  .. code-block:: python
-
-    @hooks.register('construct_page_listing_buttons')
-    def remove_page_listing_button_item(buttons, page, page_perms, is_parent=False, context=None):
-        if is_parent:
-            buttons.pop() # removes the last 'more' dropdown button on the parent page listing buttons
-
-
-.. _construct_wagtail_userbar:
-
-``construct_wagtail_userbar``
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-  Add or remove items from the wagtail userbar. Add, edit, and moderation tools are provided by default. The callable passed into the hook must take the ``request`` object and a list of menu objects, ``items``. The menu item objects must have a ``render`` method which can take a ``request`` object and return the HTML string representing the menu item. See the userbar templates and menu item classes for more information.
-
-  .. code-block:: python
-
-    from wagtail import hooks
-
-    class UserbarPuppyLinkItem:
-        def render(self, request):
-            return '<li><a href="http://cuteoverload.com/tag/puppehs/" ' \
-                + 'target="_parent" role="menuitem" class="action icon icon-wagtail">Puppies!</a></li>'
-
-    @hooks.register('construct_wagtail_userbar')
-    def add_puppy_link_item(request, items):
-        return items.append( UserbarPuppyLinkItem() )
-
-
-Admin workflow
---------------
 Hooks for customising the way admins are directed through the process of editing users.
 
+(after_create_user)=
 
-.. _after_create_user:
-
-``after_create_user``
-~~~~~~~~~~~~~~~~~~~~~
-
-  Do something with a ``User`` object after it has been saved to the database.  The callable passed to this hook should take a ``request`` object and a ``user`` object. The function does not have to return anything, but if an object with a ``status_code`` property is returned, Wagtail will use it as a response object. By default, Wagtail will instead redirect to the User index page.
-
-  .. code-block:: python
-
-    from django.http import HttpResponse
-
-    from wagtail import hooks
-
-    @hooks.register('after_create_user')
-    def do_after_page_create(request, user):
-        return HttpResponse("Congrats on creating a new user!", content_type="text/plain")
-
-
-.. _before_create_user:
-
-``before_create_user``
-~~~~~~~~~~~~~~~~~~~~~~
-
-  Called at the beginning of the "create user" view passing in the request.
-
-  The function does not have to return anything, but if an object with a ``status_code`` property is returned, Wagtail will use it as a response object and skip the rest of the view.
-
-  Unlike, ``after_create_user``, this is run both for both ``GET`` and ``POST`` requests.
-
-  This can be used to completely override the user editor on a per-view basis:
-
-  .. code-block:: python
-
-    from django.http import HttpResponse
-
-    from wagtail import hooks
-
-    from .models import AwesomePage
-    from .admin_views import edit_awesome_page
-
-    @hooks.register('before_create_user')
-    def before_create_page(request):
-        return HttpResponse("A user creation form", content_type="text/plain")
-
+### `after_create_user`
 
+Do something with a `User` object after it has been saved to the database. The callable passed to this hook should take a `request` object and a `user` object. The function does not have to return anything, but if an object with a `status_code` property is returned, Wagtail will use it as a response object. By default, Wagtail will instead redirect to the User index page.
 
-.. _after_delete_user:
+```python
+from django.http import HttpResponse
 
-``after_delete_user``
-~~~~~~~~~~~~~~~~~~~~~
+from wagtail import hooks
 
-  Do something after a ``User`` object is deleted. Uses the same behaviour as ``after_create_user``.
+@hooks.register('after_create_user')
+def do_after_page_create(request, user):
+    return HttpResponse("Congrats on creating a new user!", content_type="text/plain")
+```
 
+(before_create_user)=
 
-.. _before_delete_user:
+### `before_create_user`
 
-``before_delete_user``
-~~~~~~~~~~~~~~~~~~~~~~
+Called at the beginning of the "create user" view passing in the request.
 
-  Called at the beginning of the "delete user" view passing in the request and the user object.
+The function does not have to return anything, but if an object with a `status_code` property is returned, Wagtail will use it as a response object and skip the rest of the view.
 
-  Uses the same behaviour as ``before_create_user``.
+Unlike, `after_create_user`, this is run both for both `GET` and `POST` requests.
 
+This can be used to completely override the user editor on a per-view basis:
 
-.. _after_edit_user:
+```python
+from django.http import HttpResponse
 
-``after_edit_user``
-~~~~~~~~~~~~~~~~~~~
+from wagtail import hooks
 
-  Do something with a ``User`` object after it has been updated. Uses the same behaviour as ``after_create_user``.
+from .models import AwesomePage
+from .admin_views import edit_awesome_page
 
+@hooks.register('before_create_user')
+def before_create_page(request):
+    return HttpResponse("A user creation form", content_type="text/plain")
+```
 
-.. _before_edit_user:
+(after_delete_user)=
 
-``before_edit_user``
-~~~~~~~~~~~~~~~~~~~~~
+### `after_delete_user`
 
-  Called at the beginning of the "edit user" view passing in the request and the user object.
+Do something after a `User` object is deleted. Uses the same behaviour as `after_create_user`.
 
-  Uses the same behaviour as ``before_create_user``.
+(before_delete_user)=
 
-Choosers
---------
+### `before_delete_user`
 
-.. _construct_page_chooser_queryset:
+Called at the beginning of the "delete user" view passing in the request and the user object.
 
-``construct_page_chooser_queryset``
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+Uses the same behaviour as `before_create_user`.
 
-  Called when rendering the page chooser view, to allow the page listing QuerySet to be customised. The callable passed into the hook will receive the current page QuerySet and the request object, and must return a Page QuerySet (either the original one, or a new one).
+(after_edit_user)=
 
-  .. code-block:: python
+### `after_edit_user`
 
-    from wagtail import hooks
+Do something with a `User` object after it has been updated. Uses the same behaviour as `after_create_user`.
 
-    @hooks.register('construct_page_chooser_queryset')
-    def show_my_pages_only(pages, request):
-        # Only show own pages
-        pages = pages.filter(owner=request.user)
-
-        return pages
-
-
-.. _construct_document_chooser_queryset:
-
-``construct_document_chooser_queryset``
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-  Called when rendering the document chooser view, to allow the document listing QuerySet to be customised. The callable passed into the hook will receive the current document QuerySet and the request object, and must return a Document QuerySet (either the original one, or a new one).
-
-  .. code-block:: python
-
-    from wagtail import hooks
-
-    @hooks.register('construct_document_chooser_queryset')
-    def show_my_uploaded_documents_only(documents, request):
-        # Only show uploaded documents
-        documents = documents.filter(uploaded_by_user=request.user)
+(before_edit_user)=
 
-        return documents
+### `before_edit_user`
 
+Called at the beginning of the "edit user" view passing in the request and the user object.
 
-.. _construct_image_chooser_queryset:
+Uses the same behaviour as `before_create_user`.
 
-``construct_image_chooser_queryset``
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+## Choosers
 
-  Called when rendering the image chooser view, to allow the image listing QuerySet to be customised. The callable passed into the hook will receive the current image QuerySet and the request object, and must return an Image QuerySet (either the original one, or a new one).
+(construct_page_chooser_queryset)=
 
-  .. code-block:: python
+### `construct_page_chooser_queryset`
 
-    from wagtail import hooks
+Called when rendering the page chooser view, to allow the page listing QuerySet to be customised. The callable passed into the hook will receive the current page QuerySet and the request object, and must return a Page QuerySet (either the original one, or a new one).
 
-    @hooks.register('construct_image_chooser_queryset')
-    def show_my_uploaded_images_only(images, request):
-        # Only show uploaded images
-        images = images.filter(uploaded_by_user=request.user)
+```python
+from wagtail import hooks
 
-        return images
+@hooks.register('construct_page_chooser_queryset')
+def show_my_pages_only(pages, request):
+    # Only show own pages
+    pages = pages.filter(owner=request.user)
 
+    return pages
+```
 
-Page explorer
--------------
+(construct_document_chooser_queryset)=
 
-.. _construct_explorer_page_queryset:
+### `construct_document_chooser_queryset`
 
-``construct_explorer_page_queryset``
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+Called when rendering the document chooser view, to allow the document listing QuerySet to be customised. The callable passed into the hook will receive the current document QuerySet and the request object, and must return a Document QuerySet (either the original one, or a new one).
 
-  Called when rendering the page explorer view, to allow the page listing QuerySet to be customised. The callable passed into the hook will receive the parent page object, the current page QuerySet, and the request object, and must return a Page QuerySet (either the original one, or a new one).
+```python
+from wagtail import hooks
 
-  .. code-block:: python
+@hooks.register('construct_document_chooser_queryset')
+def show_my_uploaded_documents_only(documents, request):
+    # Only show uploaded documents
+    documents = documents.filter(uploaded_by_user=request.user)
 
-    from wagtail import hooks
+    return documents
+```
 
-    @hooks.register('construct_explorer_page_queryset')
-    def show_my_profile_only(parent_page, pages, request):
-        # If we're in the 'user-profiles' section, only show the user's own profile
-        if parent_page.slug == 'user-profiles':
-            pages = pages.filter(owner=request.user)
+(construct_image_chooser_queryset)=
 
-        return pages
+### `construct_image_chooser_queryset`
 
+Called when rendering the image chooser view, to allow the image listing QuerySet to be customised. The callable passed into the hook will receive the current image QuerySet and the request object, and must return an Image QuerySet (either the original one, or a new one).
 
-.. _register_page_listing_buttons:
+```python
+from wagtail import hooks
 
-``register_page_listing_buttons``
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+@hooks.register('construct_image_chooser_queryset')
+def show_my_uploaded_images_only(images, request):
+    # Only show uploaded images
+    images = images.filter(uploaded_by_user=request.user)
 
-  Add buttons to the actions list for a page in the page explorer. This is useful when adding custom actions to the listing, such as translations or a complex workflow.
+    return images
+```
 
-  This example will add a simple button to the listing:
+## Page explorer
 
-  .. code-block:: python
+(construct_explorer_page_queryset)=
 
-    from wagtail.admin import widgets as wagtailadmin_widgets
+### `construct_explorer_page_queryset`
 
-    @hooks.register('register_page_listing_buttons')
-    def page_listing_buttons(page, page_perms, is_parent=False, next_url=None):
-        yield wagtailadmin_widgets.PageListingButton(
-            'A page listing button',
-            '/goes/to/a/url/',
-            priority=10
-        )
+Called when rendering the page explorer view, to allow the page listing QuerySet to be customised. The callable passed into the hook will receive the parent page object, the current page QuerySet, and the request object, and must return a Page QuerySet (either the original one, or a new one).
 
-  The arguments passed to the hook are as follows:
+```python
+from wagtail import hooks
 
-  * ``page`` - the page object to generate the button for
-  * ``page_perms`` - a ``PagePermissionTester`` object that can be queried to determine the current user's permissions on the given page
-  * ``is_parent`` - if true, this button is being rendered for the parent page being displayed at the top of the listing
-  * ``next_url`` - the URL that the linked action should redirect back to on completion of the action, if the view supports it
-
-  The ``priority`` argument controls the order the buttons are displayed in. Buttons are ordered from low to high priority, so a button with ``priority=10`` will be displayed before a button with ``priority=20``.
+@hooks.register('construct_explorer_page_queryset')
+def show_my_profile_only(parent_page, pages, request):
+    # If we're in the 'user-profiles' section, only show the user's own profile
+    if parent_page.slug == 'user-profiles':
+        pages = pages.filter(owner=request.user)
 
+    return pages
+```
 
-.. register_page_listing_more_buttons:
+(register_page_listing_buttons)=
 
-``register_page_listing_more_buttons``
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+### `register_page_listing_buttons`
 
-  Add buttons to the "More" dropdown menu for a page in the page explorer. This works similarly to the ``register_page_listing_buttons`` hook but is useful for lesser-used custom actions that are better suited for the dropdown.
+Add buttons to the actions list for a page in the page explorer. This is useful when adding custom actions to the listing, such as translations or a complex workflow.
 
-  This example will add a simple button to the dropdown menu:
+This example will add a simple button to the listing:
 
-  .. code-block:: python
+```python
+from wagtail.admin import widgets as wagtailadmin_widgets
 
-    from wagtail.admin import widgets as wagtailadmin_widgets
+@hooks.register('register_page_listing_buttons')
+def page_listing_buttons(page, page_perms, is_parent=False, next_url=None):>
+    yield wagtailadmin_widgets.PageListingButton(
+        'A page listing button',
+        '/goes/to/a/url/',
+        priority=10
+    )
+```
 
-    @hooks.register('register_page_listing_more_buttons')
-    def page_listing_more_buttons(page, page_perms, is_parent=False, next_url=None):
-        yield wagtailadmin_widgets.Button(
-            'A dropdown button',
-            '/goes/to/a/url/',
-            priority=60
-        )
+The arguments passed to the hook are as follows:
 
-  The arguments passed to the hook are as follows:
+-   `page` - the page object to generate the button for
+-   `page_perms` - a `PagePermissionTester` object that can be queried to determine the current user's permissions on the given page
+-   `is_parent` - if true, this button is being rendered for the parent page being displayed at the top of the listing
+-   `next_url` - the URL that the linked action should redirect back to on completion of the action, if the view supports it
 
-  * ``page`` - the page object to generate the button for
-  * ``page_perms`` - a ``PagePermissionTester`` object that can be queried to determine the current user's permissions on the given page
-  * ``is_parent`` - if true, this button is being rendered for the parent page being displayed at the top of the listing
-  * ``next_url`` - the URL that the linked action should redirect back to on completion of the action, if the view supports it
+The `priority` argument controls the order the buttons are displayed in. Buttons are ordered from low to high priority, so a button with `priority=10` will be displayed before a button with `priority=20`.
 
-  The ``priority`` argument controls the order the buttons are displayed in the dropdown. Buttons are ordered from low to high priority, so a button with ``priority=10`` will be displayed before a button with ``priority=60``.
+(register_page_listing_more_buttons)=
 
+### `register_page_listing_more_buttons`
 
-Buttons with dropdown lists
-^^^^^^^^^^^^^^^^^^^^^^^^^^^
+Add buttons to the "More" dropdown menu for a page in the page explorer. This works similarly to the `register_page_listing_buttons` hook but is useful for lesser-used custom actions that are better suited for the dropdown.
 
-  The admin widgets also provide ``ButtonWithDropdownFromHook``, which allows you to define a custom hook for generating a dropdown menu that gets attached to your button.
+This example will add a simple button to the dropdown menu:
 
-  Creating a button with a dropdown menu involves two steps. Firstly, you add your button to the ``register_page_listing_buttons`` hook, just like the example above.
-  Secondly, you register a new hook that yields the contents of the dropdown menu.
+```python
+from wagtail.admin import widgets as wagtailadmin_widgets
 
-  This example shows how Wagtail's default admin dropdown is implemented. You can also see how to register buttons conditionally, in this case by evaluating the ``page_perms``:
+@hooks.register('register_page_listing_more_buttons')
+def page_listing_more_buttons(page, page_perms, is_parent=False, next_url=None):
+    yield wagtailadmin_widgets.Button(
+        'A dropdown button',
+        '/goes/to/a/url/',
+        priority=60
+    )
+```
 
-  .. code-block:: python
+The arguments passed to the hook are as follows:
 
-    from wagtail.admin import widgets as wagtailadmin_widgets
+-   `page` - the page object to generate the button for
+-   `page_perms` - a `PagePermissionTester` object that can be queried to determine the current user's permissions on the given page
+-   `is_parent` - if true, this button is being rendered for the parent page being displayed at the top of the listing
+-   `next_url` - the URL that the linked action should redirect back to on completion of the action, if the view supports it
 
-    @hooks.register('register_page_listing_buttons')
-    def page_custom_listing_buttons(page, page_perms, is_parent=False, next_url=None):
-        yield wagtailadmin_widgets.ButtonWithDropdownFromHook(
-            'More actions',
-            hook_name='my_button_dropdown_hook',
-            page=page,
-            page_perms=page_perms,
-            is_parent=is_parent,
-            next_url=next_url,
-            priority=50
-        )
+The `priority` argument controls the order the buttons are displayed in the dropdown. Buttons are ordered from low to high priority, so a button with `priority=10` will be displayed before a button with `priority=60`.
 
-    @hooks.register('my_button_dropdown_hook')
-    def page_custom_listing_more_buttons(page, page_perms, is_parent=False, next_url=None):
-        if page_perms.can_move():
-            yield wagtailadmin_widgets.Button('Move', reverse('wagtailadmin_pages:move', args=[page.id]), priority=10)
-        if page_perms.can_delete():
-            yield wagtailadmin_widgets.Button('Delete', reverse('wagtailadmin_pages:delete', args=[page.id]), priority=30)
-        if page_perms.can_unpublish():
-            yield wagtailadmin_widgets.Button('Unpublish', reverse('wagtailadmin_pages:unpublish', args=[page.id]), priority=40)
+#### Buttons with dropdown lists
 
+The admin widgets also provide `ButtonWithDropdownFromHook`, which allows you to define a custom hook for generating a dropdown menu that gets attached to your button.
 
+Creating a button with a dropdown menu involves two steps. Firstly, you add your button to the `register_page_listing_buttons` hook, just like the example above.
+Secondly, you register a new hook that yields the contents of the dropdown menu.
 
-  The template for the dropdown button can be customised by overriding ``wagtailadmin/pages/listing/_button_with_dropdown.html``. The JavaScript that runs the dropdowns makes use of custom data attributes, so you should leave ``data-dropdown`` and ``data-dropdown-toggle`` in the markup if you customise it.
+This example shows how Wagtail's default admin dropdown is implemented. You can also see how to register buttons conditionally, in this case by evaluating the `page_perms`:
 
+```python
+from wagtail.admin import widgets as wagtailadmin_widgets
 
-Page serving
-------------
+@hooks.register('register_page_listing_buttons')
+def page_custom_listing_buttons(page, page_perms, is_parent=False, next_url=None):
+    yield wagtailadmin_widgets.ButtonWithDropdownFromHook(
+        'More actions',
+        hook_name='my_button_dropdown_hook',
+        page=page,
+        page_perms=page_perms,
+        is_parent=is_parent,
+        next_url=next_url,
+        priority=50
+    )
 
-.. _before_serve_page:
+@hooks.register('my_button_dropdown_hook')
+def page_custom_listing_more_buttons(page, page_perms, is_parent=False, next_url=None):
+    if page_perms.can_move():
+        yield wagtailadmin_widgets.Button('Move', reverse('wagtailadmin_pages:move', args=[page.id]), priority=10)
+    if page_perms.can_delete():
+        yield wagtailadmin_widgets.Button('Delete', reverse('wagtailadmin_pages:delete', args=[page.id]), priority=30)
+    if page_perms.can_unpublish():
+        yield wagtailadmin_widgets.Button('Unpublish', reverse('wagtailadmin_pages:unpublish', args=[page.id]), priority=40)
+```
 
-``before_serve_page``
-~~~~~~~~~~~~~~~~~~~~~
+The template for the dropdown button can be customised by overriding `wagtailadmin/pages/listing/_button_with_dropdown.html`. The JavaScript that runs the dropdowns makes use of custom data attributes, so you should leave `data-dropdown` and `data-dropdown-toggle` in the markup if you customise it.
 
-  Called when Wagtail is about to serve a page. The callable passed into the hook will receive the page object, the request object, and the ``args`` and ``kwargs`` that will be passed to the page's ``serve()`` method. If the callable returns an ``HttpResponse``, that response will be returned immediately to the user, and Wagtail will not proceed to call ``serve()`` on the page.
+## Page serving
 
-  .. code-block:: python
+(before_serve_page)=
 
-    from django.http import HttpResponse
+### `before_serve_page`
 
-    from wagtail import hooks
+Called when Wagtail is about to serve a page. The callable passed into the hook will receive the page object, the request object, and the `args` and `kwargs` that will be passed to the page's `serve()` method. If the callable returns an `HttpResponse`, that response will be returned immediately to the user, and Wagtail will not proceed to call `serve()` on the page.
 
-    @hooks.register('before_serve_page')
-    def block_googlebot(page, request, serve_args, serve_kwargs):
-        if request.META.get('HTTP_USER_AGENT') == 'GoogleBot':
-            return HttpResponse("<h1>bad googlebot no cookie</h1>")
+```python
+from django.http import HttpResponse
 
+from wagtail import hooks
 
-Document serving
-----------------
+@hooks.register('before_serve_page')
+def block_googlebot(page, request, serve_args, serve_kwargs):
+    if request.META.get('HTTP_USER_AGENT') == 'GoogleBot':
+        return HttpResponse("<h1>bad googlebot no cookie</h1>")
+```
 
-.. _before_serve_document:
+## Document serving
 
-``before_serve_document``
-~~~~~~~~~~~~~~~~~~~~~~~~~
+(before_serve_document)=
 
-  Called when Wagtail is about to serve a document. The callable passed into the hook will receive the document object and the request object. If the callable returns an ``HttpResponse``, that response will be returned immediately to the user, instead of serving the document. Note that this hook will be skipped if the :ref:`WAGTAILDOCS_SERVE_METHOD <wagtaildocs_serve_method>` setting is set to ``direct``.
+### `before_serve_document`
 
+Called when Wagtail is about to serve a document. The callable passed into the hook will receive the document object and the request object. If the callable returns an `HttpResponse`, that response will be returned immediately to the user, instead of serving the document. Note that this hook will be skipped if the [`WAGTAILDOCS_SERVE_METHOD`](wagtaildocs_serve_method) setting is set to `direct`.
 
-Snippets
---------
+## Snippets
 
 Hooks for working with registered Snippets.
 
-.. _after_edit_snippet:
-
-``after_edit_snippet``
-~~~~~~~~~~~~~~~~~~~~~~
-
-  Called when a Snippet is edited. The callable passed into the hook will receive the model instance, the request object. If the callable returns an ``HttpResponse``, that response will be returned immediately to the user, and Wagtail will not proceed to call ``redirect()`` to the listing view.
-
-  .. code-block:: python
-
-    from django.http import HttpResponse
-
-    from wagtail import hooks
-
-    @hooks.register('after_edit_snippet')
-    def after_snippet_update(request, instance):
-        return HttpResponse(f"Congrats on editing a snippet with id {instance.pk}", content_type="text/plain")
-
-.. _before_edit_snippet:
-
-``before_edit_snippet``
-~~~~~~~~~~~~~~~~~~~~~~~
-
-  Called at the beginning of the edit snippet view. The callable passed into the hook will receive the model instance, the request object. If the callable returns an ``HttpResponse``, that response will be returned immediately to the user, and Wagtail will not proceed to call ``redirect()`` to the listing view.
-
-  .. code-block:: python
-
-    from django.http import HttpResponse
-
-    from wagtail import hooks
-
-    @hooks.register('before_edit_snippet')
-    def block_snippet_edit(request, instance):
-        if isinstance(instance, RestrictedSnippet) and instance.prevent_edit:
-            return HttpResponse("Sorry, you can't edit this snippet", content_type="text/plain")
-
-.. _after_create_snippet:
-
-``after_create_snippet``
-~~~~~~~~~~~~~~~~~~~~~~~~
-
-  Called when a Snippet is created. ``after_create_snippet`` and
-  ``after_edit_snippet`` work in identical ways. The only difference is where
-  the hook is called.
-
-.. _before_create_snippet:
-
-``before_create_snippet``
-~~~~~~~~~~~~~~~~~~~~~~~~~
-
-  Called at the beginning of the create snippet view. Works in a similar way to `before_edit_snippet` except the model is passed as an argument instead of an instance.
-
-.. _after_delete_snippet:
-
-``after_delete_snippet``
-~~~~~~~~~~~~~~~~~~~~~~~~
-
-  Called when a Snippet is deleted. The callable passed into the hook will receive the model instance(s) as a queryset along with the request object. If the callable returns an ``HttpResponse``, that response will be returned immediately to the user, and Wagtail will not proceed to call ``redirect()`` to the listing view.
-
-  .. code-block:: python
+(after_edit_snippet)=
 
-    from django.http import HttpResponse
+### `after_edit_snippet`
 
-    from wagtail import hooks
+Called when a Snippet is edited. The callable passed into the hook will receive the model instance, the request object. If the callable returns an `HttpResponse`, that response will be returned immediately to the user, and Wagtail will not proceed to call `redirect()` to the listing view.
 
-    @hooks.register('after_delete_snippet')
-    def after_snippet_delete(request, instances):
-        # "instances" is a QuerySet
-        total = len(instances)
-        return HttpResponse(f"{total} snippets have been deleted", content_type="text/plain")
+```python
+from django.http import HttpResponse
 
-.. _before_delete_snippet:
+from wagtail import hooks
 
-``before_delete_snippet``
-~~~~~~~~~~~~~~~~~~~~~~~~~
+@hooks.register('after_edit_snippet')
+def after_snippet_update(request, instance):
+    return HttpResponse(f"Congrats on editing a snippet with id {instance.pk}", content_type="text/plain")
+```
 
-  Called at the beginning of the delete snippet view. The callable passed into the hook will receive the model instance(s) as a queryset along with the request object. If the callable returns an ``HttpResponse``, that response will be returned immediately to the user, and Wagtail will not proceed to call ``redirect()`` to the listing view.
+(before_edit_snippet)=
 
-  .. code-block:: python
+### `before_edit_snippet`
 
-    from django.http import HttpResponse
+Called at the beginning of the edit snippet view. The callable passed into the hook will receive the model instance, the request object. If the callable returns an `HttpResponse`, that response will be returned immediately to the user, and Wagtail will not proceed to call `redirect()` to the listing view.
 
-    from wagtail import hooks
+```python
+from django.http import HttpResponse
 
-    @hooks.register('before_delete_snippet')
-    def before_snippet_delete(request, instances):
-        # "instances" is a QuerySet
-        total = len(instances)
+from wagtail import hooks
 
-        if request.method == 'POST':
-          # Override the deletion behaviour
-          instances.delete()
+@hooks.register('before_edit_snippet')
+def block_snippet_edit(request, instance):
+    if isinstance(instance, RestrictedSnippet) and instance.prevent_edit:
+        return HttpResponse("Sorry, you can't edit this snippet", content_type="text/plain")
+```
 
-          return HttpResponse(f"{total} snippets have been deleted", content_type="text/plain")
+(after_create_snippet)=
 
-.. _register_snippet_action_menu_item:
+### `after_create_snippet`
 
-``register_snippet_action_menu_item``
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+Called when a Snippet is created. `after_create_snippet` and `after_edit_snippet` work in identical ways. The only difference is where the hook is called.
 
-  Add an item to the popup menu of actions on the snippet creation and edit views.
-  The callable passed to this hook must return an instance of
-  ``wagtail.snippets.action_menu.ActionMenuItem``. ``ActionMenuItem`` is a subclass of :ref:`Component <creating_template_components>` and so the rendering of the menu item can be customised through ``template_name``, ``get_context_data``, ``render_html`` and ``Media``. In addition, the following attributes and methods are available to be overridden:
+(before_create_snippet)=
 
-  :order: an integer (default 100) which determines the item's position in the menu. Can also be passed as a keyword argument to the object constructor. The lowest-numbered item in this sequence will be selected as the default menu item; as standard, this is "Save draft" (which has an ``order`` of 0).
-  :label: the displayed text of the menu item
-  :get_url: a method which returns a URL for the menu item to link to; by default, returns ``None`` which causes the menu item to behave as a form submit button instead
-  :name: value of the ``name`` attribute of the submit button if no URL is specified
-  :icon_name: icon to display against the menu item
-  :classname: a ``class`` attribute value to add to the button element
-  :is_shown: a method which returns a boolean indicating whether the menu item should be shown; by default, true except when editing a locked page
+### `before_create_snippet`
 
-  The ``get_url``, ``is_shown``, ``get_context_data`` and ``render_html`` methods all accept a context dictionary containing the following fields:
+Called at the beginning of the create snippet view. Works in a similar way to `before_edit_snippet` except the model is passed as an argument instead of an instance.
 
-  :view: name of the current view: ``'create'`` or ``'edit'``
-  :model: The snippet's model class
-  :instance: For ``view`` = ``'edit'``, the instance being edited
-  :request: The current request object
+(after_delete_snippet)=
 
-  .. code-block:: python
+### `after_delete_snippet`
 
-    from wagtail import hooks
-    from wagtail.snippets.action_menu import ActionMenuItem
+Called when a Snippet is deleted. The callable passed into the hook will receive the model instance(s) as a queryset along with the request object. If the callable returns an `HttpResponse`, that response will be returned immediately to the user, and Wagtail will not proceed to call `redirect()` to the listing view.
 
-    class GuacamoleMenuItem(ActionMenuItem):
-        name = 'action-guacamole'
-        label = "Guacamole"
+```python
+from django.http import HttpResponse
 
-        def get_url(self, context):
-            return "https://www.youtube.com/watch?v=dNJdJIwCF_Y"
+from wagtail import hooks
 
+@hooks.register('after_delete_snippet')
+def after_snippet_delete(request, instances):
+    # "instances" is a QuerySet
+    total = len(instances)
+    return HttpResponse(f"{total} snippets have been deleted", content_type="text/plain")
+```
 
-    @hooks.register('register_snippet_action_menu_item')
-    def register_guacamole_menu_item():
-        return GuacamoleMenuItem(order=10)
+(before_delete_snippet)=
 
-.. _construct_snippet_action_menu:
+### `before_delete_snippet`
 
-``construct_snippet_action_menu``
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+Called at the beginning of the delete snippet view. The callable passed into the hook will receive the model instance(s) as a queryset along with the request object. If the callable returns an `HttpResponse`, that response will be returned immediately to the user, and Wagtail will not proceed to call `redirect()` to the listing view.
 
-  Modify the final list of action menu items on the snippet creation and edit views.
-  The callable passed to this hook receives a list of ``ActionMenuItem`` objects, a
-  request object and a context dictionary as per ``register_snippet_action_menu_item``,
-  and should modify the list of menu items in-place.
+```python
+from django.http import HttpResponse
 
-  .. code-block:: python
+from wagtail import hooks
 
-    @hooks.register('construct_snippet_action_menu')
-    def remove_delete_option(menu_items, request, context):
-        menu_items[:] = [item for item in menu_items if item.name != 'delete']
+@hooks.register('before_delete_snippet')
+def before_snippet_delete(request, instances):
+    # "instances" is a QuerySet
+    total = len(instances)
 
+    if request.method == 'POST':
+      # Override the deletion behaviour
+      instances.delete()
 
-  The ``construct_snippet_action_menu`` hook is called after the menu items have been
-  sorted by their order attributes, and so setting a menu item's order will have no
-  effect at this point. Instead, items can be reordered by changing their position in
-  the list, with the first item being selected as the default action. For example, to
-  change the default action to Delete:
+      return HttpResponse(f"{total} snippets have been deleted", content_type="text/plain")
+```
 
-  .. code-block:: python
+(register_snippet_action_menu_item)=
 
-    @hooks.register('construct_snippet_action_menu')
-    def make_delete_default_action(menu_items, request, context):
-        for (index, item) in enumerate(menu_items):
-            if item.name == 'delete':
-                # move to top of list
-                menu_items.pop(index)
-                menu_items.insert(0, item)
-                break
+### `register_snippet_action_menu_item`
 
-.. _register_snippet_listing_buttons:
+Add an item to the popup menu of actions on the snippet creation and edit views.
+The callable passed to this hook must return an instance of `wagtail.snippets.action_menu.ActionMenuItem`. `ActionMenuItem` is a subclass of [Component](creating_template_components) and so the rendering of the menu item can be customised through `template_name`, `get_context_data`, `render_html` and `Media`. In addition, the following attributes and methods are available to be overridden:
 
-``register_snippet_listing_buttons``
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+-   `order`- an integer (default 100) which determines the item's position in the menu. Can also be passed as a keyword argument to the object constructor. The lowest-numbered item in this sequence will be selected as the default menu item; as standard, this is "Save draft" (which has an`order` of 0).
+-   `label` - the displayed text of the menu item
+-   `get_url` - a method which returns a URL for the menu item to link to; by default, returns `None` which causes the menu item to behave as a form submit button instead
+-   `name` - value of the `name` attribute of the submit button if no URL is specified
+-   `icon_name` - icon to display against the menu item
+-   `classname` - a `class` attribute value to add to the button element
+-   `is_shown` - a method which returns a boolean indicating whether the menu item should be shown; by default, true except when editing a locked page
 
-  Add buttons to the actions list for a snippet in the snippets listing. This is useful when adding custom actions to the listing, such as translations or a complex workflow.
+The `get_url`, `is_shown`, `get_context_data` and `render_html` methods all accept a context dictionary containing the following fields:
 
-  This example will add a simple button to the listing:
+-   `view` - name of the current view: `'create'` or `'edit'`
+-   `model` - the snippet's model class
+-   `instance` - for `view` = `'edit'`, the instance being edited
+-   `request` - the current request object
 
-  .. code-block:: python
+```python
+from wagtail import hooks
+from wagtail.snippets.action_menu import ActionMenuItem
 
-    from wagtail.snippets import widgets as wagtailsnippets_widgets
+class GuacamoleMenuItem(ActionMenuItem):
+    name = 'action-guacamole'
+    label = "Guacamole"
 
-    @hooks.register('register_snippet_listing_buttons')
-    def snippet_listing_buttons(snippet, user, next_url=None):
-        yield wagtailsnippets_widgets.SnippetListingButton(
-            'A page listing button',
-            '/goes/to/a/url/',
-            priority=10
-        )
+    def get_url(self, context):
+        return "https://www.youtube.com/watch?v=dNJdJIwCF_Y"
 
-  The arguments passed to the hook are as follows:
 
-  * ``snippet`` - the snippet object to generate the button for
-  * ``user`` - the user who is viewing the snippets listing
-  * ``next_url`` - the URL that the linked action should redirect back to on completion of the action, if the view supports it
+@hooks.register('register_snippet_action_menu_item')
+def register_guacamole_menu_item():
+    return GuacamoleMenuItem(order=10)
+```
 
-  The ``priority`` argument controls the order the buttons are displayed in. Buttons are ordered from low to high priority, so a button with ``priority=10`` will be displayed before a button with ``priority=20``.
+(construct_snippet_action_menu)=
 
-.. construct_snippet_listing_buttons:
+### `construct_snippet_action_menu`
 
-``construct_snippet_listing_buttons``
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+Modify the final list of action menu items on the snippet creation and edit views.
+The callable passed to this hook receives a list of `ActionMenuItem` objects, a request object and a context dictionary as per `register_snippet_action_menu_item`, and should modify the list of menu items in-place.
 
-  Modify the final list of snippet listing buttons. The
-  callable passed to this hook receives a list of ``SnippetListingButton`` objects, a user,
-  and a context dictionary as per ``register_snippet_listing_buttons``,
-  and should modify the list of menu items in-place.
+```python
+@hooks.register('construct_snippet_action_menu')
+def remove_delete_option(menu_items, request, context):
+    menu_items[:] = [item for item in menu_items if item.name != 'delete']
+```
 
-  .. code-block:: python
+The `construct_snippet_action_menu` hook is called after the menu items have been sorted by their order attributes, and so setting a menu item's order will have no effect at this point. Instead, items can be reordered by changing their position in the list, with the first item being selected as the default action. For example, to change the default action to Delete:
 
-    @hooks.register('construct_snippet_listing_buttons')
-    def remove_snippet_listing_button_item(buttons, snippet, user, context=None):
-        buttons.pop()  # Removes the 'delete' button
+```python
+@hooks.register('construct_snippet_action_menu')
+def make_delete_default_action(menu_items, request, context):
+    for (index, item) in enumerate(menu_items):
+        if item.name == 'delete':
+            # move to top of list
+            menu_items.pop(index)
+            menu_items.insert(0, item)
+            break
+```
 
+(register_snippet_listing_buttons)=
 
-Bulk actions
-------------
+### `register_snippet_listing_buttons`
 
-Hooks for registering and customising bulk actions. See :ref:`here <custom_bulk_actions>` on how to write custom bulk actions.
+Add buttons to the actions list for a snippet in the snippets listing. This is useful when adding custom actions to the listing, such as translations or a complex workflow.
 
+This example will add a simple button to the listing:
 
-.. _register_bulk_action:
+```python
+from wagtail.snippets import widgets as wagtailsnippets_widgets
 
-``register_bulk_action``
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+@hooks.register('register_snippet_listing_buttons')
+def snippet_listing_buttons(snippet, user, next_url=None):
+    yield wagtailsnippets_widgets.SnippetListingButton(
+        'A page listing button',
+        '/goes/to/a/url/',
+        priority=10
+    )
+```
 
-  Registers a new bulk action to add to the list of bulk actions in the explorer
+The arguments passed to the hook are as follows:
 
-  This hook must be registered with a sub-class of ``BulkAction`` . For example:
+-   `snippet` - the snippet object to generate the button for
+-   `user` - the user who is viewing the snippets listing
+-   `next_url` - the URL that the linked action should redirect back to on completion of the action, if the view supports it
 
-  .. code-block:: python
+The `priority` argument controls the order the buttons are displayed in. Buttons are ordered from low to high priority, so a button with `priority=10` will be displayed before a button with `priority=20`.
 
-    from wagtail.admin.views.bulk_action import BulkAction
-    from wagtail import hooks
+(construct_snippet_listing_buttons)=
 
+### `construct_snippet_listing_buttons`
 
-    @hooks.register("register_bulk_action")
-    class CustomBulkAction(BulkAction):
-        display_name = _("Custom Action")
-        action_type = "action"
-        aria_label = _("Do custom action")
-        template_name = "/path/to/template"
-        models = [...]  # list of models the action should execute upon
+Modify the final list of snippet listing buttons. The callable passed to this hook receives a list of `SnippetListingButton` objects, a user, and a context dictionary as per `register_snippet_listing_buttons`, and should modify the list of menu items in-place.
 
+```python
+@hooks.register('construct_snippet_listing_buttons')
+def remove_snippet_listing_button_item(buttons, snippet, user, context=None):
+    buttons.pop()  # Removes the 'delete' button
+```
 
-        @classmethod
-        def execute_action(cls, objects, **kwargs):
-            for object in objects:
-                do_something(object)
-            return num_parent_objects, num_child_objects  # return the count of updated objects
+## Bulk actions
 
+Hooks for registering and customising bulk actions. See [](custom_bulk_actions) on how to write custom bulk actions.
 
-.. _before_bulk_action:
+(register_bulk_action)=
 
-``before_bulk_action``
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+### `register_bulk_action`
 
-  Do something right before a bulk action is executed (before the ``execute_action`` method is called)
+Registers a new bulk action to add to the list of bulk actions in the explorer
 
-  This hook can be used to return an HTTP response. For example:
+This hook must be registered with a sub-class of `BulkAction` . For example:
 
-  .. code-block:: python
+```python
+from wagtail.admin.views.bulk_action import BulkAction
+from wagtail import hooks
 
-    from wagtail import hooks
 
-    @hooks.register("before_bulk_action")
-    def hook_func(request, action_type, objects, action_class_instance):
-      if action_type == 'delete':
-        return HttpResponse(f"{len(objects)} objects would be deleted", content_type="text/plain")
+@hooks.register("register_bulk_action")
+class CustomBulkAction(BulkAction):
+    display_name = _("Custom Action")
+    action_type = "action"
+    aria_label = _("Do custom action")
+    template_name = "/path/to/template"
+    models = [...]  # list of models the action should execute upon
 
 
-.. _after_bulk_action:
+    @classmethod
+    def execute_action(cls, objects, **kwargs):
+        for object in objects:
+            do_something(object)
+        return num_parent_objects, num_child_objects  # return the count of updated objects
+```
 
-``after_bulk_action``
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+(before_bulk_action)=
 
-  Do something right after a bulk action is executed (after the ``execute_action`` method is called)
+### `before_bulk_action`
 
-  This hook can be used to return an HTTP response. For example:
+Do something right before a bulk action is executed (before the `execute_action` method is called)
 
-  .. code-block:: python
+This hook can be used to return an HTTP response. For example:
 
-    from wagtail import hooks
+```python
+from wagtail import hooks
 
-    @hooks.register("after_bulk_action")
-    def hook_func(request, action_type, objects, action_class_instance):
-      if action_type == 'delete':
-        return HttpResponse(f"{len(objects)} objects have been deleted", content_type="text/plain")
+@hooks.register("before_bulk_action")
+def hook_func(request, action_type, objects, action_class_instance):
+  if action_type == 'delete':
+    return HttpResponse(f"{len(objects)} objects would be deleted", content_type="text/plain")
+```
 
+(after_bulk_action)=
 
+### `after_bulk_action`
 
-Audit log
----------
+Do something right after a bulk action is executed (after the `execute_action` method is called)
 
-.. _register_log_actions:
+This hook can be used to return an HTTP response. For example:
 
-``register_log_actions``
-~~~~~~~~~~~~~~~~~~~~~~~~
+```python
+from wagtail import hooks
 
-    See :ref:`audit_log`
+@hooks.register("after_bulk_action")
+def hook_func(request, action_type, objects, action_class_instance):
+  if action_type == 'delete':
+    return HttpResponse(f"{len(objects)} objects have been deleted", content_type="text/plain")
+```
 
-    To add new actions to the registry, call the ``register_action`` method with the action type, its label and the message to be displayed in administrative listings.
+## Audit log
 
-    .. code-block:: python
+(register_log_actions)=
 
-        from django.utils.translation import gettext_lazy as _
+### `register_log_actions`
 
-        from wagtail import hooks
+See [](audit_log)
 
-        @hooks.register('register_log_actions')
-        def additional_log_actions(actions):
-            actions.register_action('wagtail_package.echo', _('Echo'), _('Sent an echo'))
+To add new actions to the registry, call the `register_action` method with the action type, its label and the message to be displayed in administrative listings.
 
+```python
+from django.utils.translation import gettext_lazy as _
 
-    Alternatively, for a log message that varies according to the log entry's data, create a subclass of ``wagtail.log_actions.LogFormatter`` that overrides the ``format_message`` method, and use ``register_action`` as a decorator on that class:
+from wagtail import hooks
 
-    .. code-block:: python
+@hooks.register('register_log_actions')
+def additional_log_actions(actions):
+    actions.register_action('wagtail_package.echo', _('Echo'), _('Sent an echo'))
+```
 
-        from django.utils.translation import gettext_lazy as _
+Alternatively, for a log message that varies according to the log entry's data, create a subclass of `wagtail.log_actions.LogFormatter` that overrides the `format_message` method, and use `register_action` as a decorator on that class:
 
-        from wagtail import hooks
-        from wagtail.log_actions import LogFormatter
+```python
+from django.utils.translation import gettext_lazy as _
 
-        @hooks.register('register_log_actions')
-        def additional_log_actions(actions):
-            @actions.register_action('wagtail_package.greet_audience')
-            class GreetingActionFormatter(LogFormatter):
-                label = _('Greet audience')
+from wagtail import hooks
+from wagtail.log_actions import LogFormatter
 
-                def format_message(self, log_entry):
-                    return _('Hello %(audience)s') % {
-                        'audience': log_entry.data['audience'],
-                    }
+@hooks.register('register_log_actions')
+def additional_log_actions(actions):
+    @actions.register_action('wagtail_package.greet_audience')
+    class GreetingActionFormatter(LogFormatter):
+        label = _('Greet audience')
 
-    .. versionchanged:: 2.15
+        def format_message(self, log_entry):
+            return _('Hello %(audience)s') % {
+                'audience': log_entry.data['audience'],
+            }
+```
 
-      The ``LogFormatter`` class was introduced. Previously, dynamic messages were achieved by passing a callable as the ``message`` argument to ``register_action``.
+```{versionchanged} 2.15
+The ``LogFormatter`` class was introduced. Previously, dynamic messages were achieved by passing a callable as the ``message`` argument to ``register_action``.
+```

+ 18 - 17
docs/reference/index.md

@@ -1,19 +1,20 @@
-=========
-Reference
-=========
+# Reference
 
-.. toctree::
-    :maxdepth: 2
-    :titlesonly:
+```{toctree}
+---
+maxdepth: 2
+titlesonly:
+---
 
-    pages/index
-    streamfield/index
-    contrib/index
-    management_commands
-    hooks
-    signals
-    settings
-    project_template
-    jinja2
-    panel_api
-    viewsets
+pages/index
+streamfield/index
+contrib/index
+management_commands
+hooks
+signals
+settings
+project_template
+jinja2
+panel_api
+viewsets
+```

+ 80 - 92
docs/reference/jinja2.md

@@ -1,136 +1,124 @@
-.. _jinja2:
-
-=======================
-Jinja2 template support
-=======================
-
-Wagtail supports Jinja2 templating for all front end features. More information on each of the template tags below can be found in the :ref:`writing_templates` documentation.
-
-Configuring Django
-==================
-
-Django needs to be configured to support Jinja2 templates. As the Wagtail admin is written using standard Django templates, Django has to be configured to use **both** templating engines. Add the Jinja2 template backend configuration to the ``TEMPLATES`` setting for your app as shown here:
-
-.. code-block:: python
-
-    TEMPLATES = [
-        {
-            "BACKEND": "django.template.backends.django.DjangoTemplates",
-            # ... the rest of the existing Django template configuration ...
+(jinja2)=
+
+# Jinja2 template support
+
+Wagtail supports Jinja2 templating for all front end features. More information on each of the template tags below can be found in the [](writing_templates) documentation.
+
+## Configuring Django
+
+Django needs to be configured to support Jinja2 templates. As the Wagtail admin is written using standard Django templates, Django has to be configured to use **both** templating engines. Add the Jinja2 template backend configuration to the `TEMPLATES` setting for your app as shown here:
+
+```python
+TEMPLATES = [
+    {
+        "BACKEND": "django.template.backends.django.DjangoTemplates",
+        # ... the rest of the existing Django template configuration ...
+    },
+    {
+        'BACKEND': 'django.template.backends.jinja2.Jinja2',
+        'APP_DIRS': True,
+        'OPTIONS': {
+            'extensions': [
+                'wagtail.jinja2tags.core',
+                'wagtail.admin.jinja2tags.userbar',
+                'wagtail.images.jinja2tags.images',
+            ],
         },
-        {
-            'BACKEND': 'django.template.backends.jinja2.Jinja2',
-            'APP_DIRS': True,
-            'OPTIONS': {
-                'extensions': [
-                    'wagtail.jinja2tags.core',
-                    'wagtail.admin.jinja2tags.userbar',
-                    'wagtail.images.jinja2tags.images',
-                ],
-            },
-        }
-    ]
+    }
+]
+```
 
-Jinja templates must be placed in a ``jinja2/`` directory in your app. For example, the standard template location for an ``EventPage`` model in an ``events`` app would be ``events/jinja2/events/event_page.html``.
+Jinja templates must be placed in a `jinja2/` directory in your app. For example, the standard template location for an `EventPage` model in an `events` app would be `events/jinja2/events/event_page.html`.
 
-By default, the Jinja environment does not have any Django functions or filters. The Django documentation has more information on :class:`configuring Jinja for Django <django.template.backends.jinja2.Jinja2>`.
+By default, the Jinja environment does not have any Django functions or filters. The Django documentation has more information on {class}`django.template.backends.jinja2.Jinja2` (configuring Jinja for Django).
 
-``self`` in templates
-=====================
+## `self` in templates
 
-In Django templates, ``self`` can be used to refer to the current page, stream block, or field panel. In Jinja, ``self`` is reserved for internal use. When writing Jinja templates, use ``page`` to refer to pages, ``value`` for stream blocks, and ``field_panel`` for field panels.
+In Django templates, `self` can be used to refer to the current page, stream block, or field panel. In Jinja, `self` is reserved for internal use. When writing Jinja templates, use `page` to refer to pages, `value` for stream blocks, and `field_panel` for field panels.
 
-Template tags, functions & filters
-==================================
+## Template tags, functions & filters
 
-``pageurl()``
-~~~~~~~~~~~~~
+### `pageurl()`
 
 Generate a URL for a Page instance:
 
-.. code-block:: html+jinja
-
-    <a href="{{ pageurl(page.more_information) }}">More information</a>
+```html+jinja
+<a href="{{ pageurl(page.more_information) }}">More information</a>
+```
 
-See :ref:`pageurl_tag` for more information
+See [](pageurl_tag) for more information
 
-``slugurl()``
-~~~~~~~~~~~~~
+### `slugurl()`
 
 Generate a URL for a Page with a slug:
 
-.. code-block:: html+jinja
+```html+jinja
+<a href="{{ slugurl("about") }}">About us</a>
+```
 
-    <a href="{{ slugurl("about") }}">About us</a>
+See [](slugurl_tag) for more information
 
-See :ref:`slugurl_tag` for more information
+### `image()`
 
-``image()``
-~~~~~~~~~~~
+Resize an image, and print an `<img>` tag:
 
-Resize an image, and print an ``<img>`` tag:
+```html+jinja
+{# Print an image tag #}
+{{ image(page.header_image, "fill-1024x200", class="header-image") }}
 
-.. code-block:: html+jinja
+{# Resize an image #}
+{% set background=image(page.background_image, "max-1024x1024") %}
+<div class="wrapper" style="background-image: url({{ background.url }});">
+```
 
-    {# Print an image tag #}
-    {{ image(page.header_image, "fill-1024x200", class="header-image") }}
+See [](image_tag) for more information
 
-    {# Resize an image #}
-    {% set background=image(page.background_image, "max-1024x1024") %}
-    <div class="wrapper" style="background-image: url({{ background.url }});">
-
-See :ref:`image_tag` for more information
-
-``|richtext``
-~~~~~~~~~~~~~
+### `|richtext`
 
 Transform Wagtail's internal HTML representation, expanding internal references to pages and images.
 
-.. code-block:: html+jinja
+```html+jinja
+{{ page.body|richtext }}
+```
 
-    {{ page.body|richtext }}
+See [](rich_text_filter) for more information
 
-See :ref:`rich-text-filter` for more information
-
-``wagtail_site``
-~~~~~~~~~~~~~~~~
+### `wagtail_site`
 
 Returns the Site object corresponding to the current request.
 
-.. code-block:: html+jinja
-
-    {{ wagtail_site().site_name }}
+```html+jinja
+{{ wagtail_site().site_name }}
+```
 
-See :ref:`wagtail_site_tag` for more information
+See [](wagtail_site_tag) for more information
 
-``wagtailuserbar()``
-~~~~~~~~~~~~~~~~~~~~
+### `wagtailuserbar()`
 
 Output the Wagtail contextual flyout menu for editing pages from the front end
 
-.. code-block:: html+jinja
+```html+jinja
+{{ wagtailuserbar() }}
+```
 
-    {{ wagtailuserbar() }}
+See [](wagtailuserbar_tag) for more information
 
-See :ref:`wagtailuserbar_tag` for more information
-
-``{% include_block %}``
-~~~~~~~~~~~~~~~~~~~~~~~
+### `{% include_block %}`
 
 Output the HTML representation for the stream content as a whole, as well as for each individual block.
 
 Allows to pass template context (by default) to the StreamField template.
 
-.. code-block:: html+jinja
-
-    {% include_block page.body %}
-    {% include_block page.body with context %} {# The same as the previous #}
-    {% include_block page.body without context %}
-
-See :ref:`StreamField template rendering<streamfield_template_rendering>` for more information.
+```html+jinja
+{% include_block page.body %}
+{% include_block page.body with context %} {# The same as the previous #}
+{% include_block page.body without context %}
+```
 
-.. note::
+See [StreamField template rendering](streamfield_template_rendering) for more information.
 
-        The ``{% include_block %}`` tag is designed to closely follow the syntax and behaviour
-        of Jinja's ``{% include %}``, so it does not implement the Django version's feature of
-        only passing specified variables into the context.
+```{note}
+The ``{% include_block %}`` tag is designed to closely follow the syntax and behaviour
+of Jinja's ``{% include %}``, so it does not implement the Django version's feature of
+only passing specified variables into the context.
+```

+ 63 - 82
docs/reference/management_commands.md

@@ -1,146 +1,127 @@
-.. _management_commands:
+(management_commands)=
 
-Management commands
-===================
+# Management commands
 
+(publish_scheduled_pages)=
 
-.. _publish_scheduled_pages:
+## publish_scheduled_pages
 
-publish_scheduled_pages
------------------------
-
-.. code-block:: console
-
-    $ ./manage.py publish_scheduled_pages
+```console
+$ ./manage.py publish_scheduled_pages
+```
 
 This command publishes, updates or unpublishes pages that have had these actions scheduled by an editor. We recommend running this command once an hour.
 
+(fixtree)=
 
-.. _fixtree:
-
-fixtree
--------
+## fixtree
 
-.. code-block:: console
-
-    $ ./manage.py fixtree
+```console
+$ ./manage.py fixtree
+```
 
 This command scans for errors in your database and attempts to fix any issues it finds.
 
+(move_pages)=
 
-.. _move_pages:
-
-move_pages
-----------
+## move_pages
 
-.. code-block:: console
-
-    $ manage.py move_pages from to
+```console
+$ manage.py move_pages from to
+```
 
 This command moves a selection of pages from one section of the tree to another.
 
 Options:
 
-- **from**
-  This is the **id** of the page to move pages from. All descendants of this page will be moved to the destination. After the operation is complete, this page will have no children.
-
-- **to**
-  This is the **id** of the page to move pages to.
+-   **from**
+    This is the **id** of the page to move pages from. All descendants of this page will be moved to the destination. After the operation is complete, this page will have no children.
 
+-   **to**
+    This is the **id** of the page to move pages to.
 
-.. _purge_revisions:
+(purge_revisions)=
 
-purge_revisions
----------------
+## purge_revisions
 
-.. code-block:: console
-
-    $ manage.py purge_revisions [--days=<number of days>]
+```console
+$ manage.py purge_revisions [--days=<number of days>]
+```
 
 This command deletes old page revisions which are not in moderation, live, approved to go live, or the latest
-revision for a page. If the ``days`` argument is supplied, only revisions older than the specified number of
+revision for a page. If the `days` argument is supplied, only revisions older than the specified number of
 days will be deleted.
 
+(update_index)=
 
-.. _update_index:
-
-update_index
-------------
+## update_index
 
-.. code-block:: console
-
-    $ ./manage.py update_index [--backend <backend name>]
+```console
+$ ./manage.py update_index [--backend <backend name>]
+```
 
 This command rebuilds the search index from scratch.
 
 It is recommended to run this command once a week and at the following times:
 
-- whenever any pages have been created through a script (after an import, for example)
-- whenever any changes have been made to models or search configuration
+-   whenever any pages have been created through a script (after an import, for example)
+-   whenever any changes have been made to models or search configuration
 
 The search may not return any results while this command is running, so avoid running it at peak times.
 
+### Specifying which backend to update
 
-Specifying which backend to update
-``````````````````````````````````
-
-By default, ``update_index`` will rebuild all the search indexes listed in ``WAGTAILSEARCH_BACKENDS``.
+By default, `update_index` will rebuild all the search indexes listed in `WAGTAILSEARCH_BACKENDS`.
 
-If you have multiple backends and would only like to update one of them, you can use the ``--backend`` option.
+If you have multiple backends and would only like to update one of them, you can use the `--backend` option.
 
 For example, to update just the default backend:
 
-.. code-block:: console
+```console
+$ python manage.py update_index --backend default
+```
 
-    $ python manage.py update_index --backend default
-
-The ``--chunk_size`` option can be used to set the size of chunks that are indexed at a time. This defaults to
+The `--chunk_size` option can be used to set the size of chunks that are indexed at a time. This defaults to
 1000 but may need to be reduced for larger document sizes.
 
-Indexing the schema only
-````````````````````````
-
-You can prevent the ``update_index`` command from indexing any data by using the ``--schema-only`` option:
-
-.. code-block:: console
-
-    $ python manage.py update_index --schema-only
-
-
-.. _wagtail_update_index:
+### Indexing the schema only
 
-wagtail_update_index
---------------------
+You can prevent the `update_index` command from indexing any data by using the `--schema-only` option:
 
-An alias for the ``update_index`` command that can be used when another installed package (such as `Haystack <https://haystacksearch.org/>`_) provides a command named ``update_index``. In this case, the other package's entry in ``INSTALLED_APPS`` should appear above ``wagtail.search`` so that its ``update_index`` command takes precedence over Wagtail's.
+```console
+$ python manage.py update_index --schema-only
+```
 
+(wagtail_update_index)=
 
-.. _search_garbage_collect:
+## wagtail_update_index
 
-search_garbage_collect
-----------------------
+An alias for the `update_index` command that can be used when another installed package (such as [Haystack](https://haystacksearch.org/)) provides a command named `update_index`. In this case, the other package's entry in `INSTALLED_APPS` should appear above `wagtail.search` so that its `update_index` command takes precedence over Wagtail's.
 
-.. code-block:: console
+(search_garbage_collect)=
 
-    $ ./manage.py search_garbage_collect
+## search_garbage_collect
 
-Wagtail keeps a log of search queries that are popular on your website. On high traffic websites, this log may get big and you may want to clean out old search queries. This command cleans out all search query logs that are more than one week old (or a number of days configurable through the :ref:`WAGTAILSEARCH_HITS_MAX_AGE <wagtailsearch_hits_max_age>` setting).
+```console
+$ ./manage.py search_garbage_collect
+```
 
-.. _wagtail_update_image_renditions:
+Wagtail keeps a log of search queries that are popular on your website. On high traffic websites, this log may get big and you may want to clean out old search queries. This command cleans out all search query logs that are more than one week old (or a number of days configurable through the [`WAGTAILSEARCH_HITS_MAX_AGE`](wagtailsearch_hits_max_age) setting).
 
-wagtail_update_image_renditions
--------------------------------
+(wagtail_update_image_renditions)=
 
-.. code-block:: console
+## wagtail_update_image_renditions
 
-    $ ./manage.py wagtail_update_image_renditions
+```console
+$ ./manage.py wagtail_update_image_renditions
+```
 
 This command provides the ability to regenerate image renditions.
 This is useful if you have deployed to a server where the image renditions have not yet been generated or you have changed the underlying image rendition behaviour and need to ensure all renditions are created again.
 
-This does not remove rendition images that are unused, this can be done by clearing the folder using ``rm -rf`` or similar, once this is done you can then use the management command to generate the renditions.
+This does not remove rendition images that are unused, this can be done by clearing the folder using `rm -rf` or similar, once this is done you can then use the management command to generate the renditions.
 
 Options:
 
-- **--purge-only** :
-  This argument will purge all image renditions without regenerating them. They will be regenerated when next requested.
+-   **--purge-only** :
+    This argument will purge all image renditions without regenerating them. They will be regenerated when next requested.

+ 2 - 2
docs/reference/pages/model_reference.rst

@@ -6,7 +6,7 @@ Model Reference
 
 This document contains reference information for the model classes inside the ``wagtailcore`` module.
 
-.. _page-model-ref:
+.. _page_model_ref:
 
 ``Page``
 ========
@@ -486,7 +486,7 @@ The ``locale`` and ``translation_key`` fields have a unique key constraint to pr
     .. autoattribute:: localized
 
 
-.. _revision-model-ref:
+.. _revision_model_ref:
 
 ``Revision``
 ============

+ 2 - 2
docs/reference/pages/panels.md

@@ -240,7 +240,7 @@ See {ref}`collapsible` for more details on `collapsible` usage.
 ## Field Customisation
 
 By adding CSS classes to your panel definitions or adding extra parameters to your field definitions, you can control much of how your fields will display in the Wagtail page editing interface. Wagtail's page editing interface takes much of its behaviour from Django's admin, so you may find many options for customisation covered there.
-(See {doc}`Django model field reference <django:ref/models/fields>`).
+(See [Django model field reference](django:ref/models/fields)).
 
 ### Full-Width Input
 
@@ -278,7 +278,7 @@ You must define a `heading` or `label` when using `collapsible` with `InlinePane
 
 ### Placeholder Text
 
-By default, Wagtail uses the field's label as placeholder text. To change it, pass to the FieldPanel a widget with a placeholder attribute set to your desired text. You can select widgets from {doc}`Django's form widgets <django:ref/forms/widgets>`, or any of the Wagtail's widgets found in `wagtail.admin.widgets`.
+By default, Wagtail uses the field's label as placeholder text. To change it, pass to the FieldPanel a widget with a placeholder attribute set to your desired text. You can select widgets from [Django's form widgets](django:ref/forms/widgets), or any of the Wagtail's widgets found in `wagtail.admin.widgets`.
 
 For example, to customise placeholders for a Book model exposed via ModelAdmin:
 

+ 72 - 86
docs/reference/project_template.md

@@ -1,103 +1,89 @@
-The project template
-====================
+# The project template
 
-.. code-block:: text
-
-    mysite/
-        home/
-            migrations/
-                __init__.py
-                0001_initial.py
-                0002_create_homepage.py
-            templates/
-                home/
-                    home_page.html
-            __init__.py
-            models.py
-        search/
-            templates/
-                search/
-                    search.html
+```text
+mysite/
+    home/
+        migrations/
             __init__.py
-            views.py
-        mysite/
-            settings/
-                __init__.py
-                base.py
-                dev.py
-                production.py
-            static/
-                css/
-                    mysite.css
-                js/
-                    mysite.js
-            templates/
-                404.html
-                500.html
-                base.html
+            0001_initial.py
+            0002_create_homepage.py
+        templates/
+            home/
+                home_page.html
+        __init__.py
+        models.py
+    search/
+        templates/
+            search/
+                search.html
+        __init__.py
+        views.py
+    mysite/
+        settings/
             __init__.py
-            urls.py
-            wsgi.py
-        Dockerfile
-        manage.py
-        requirements.txt
-
-The "home" app
-----------------
-
-Location: ``/mysite/home/``
-
-This app is here to help get you started quicker by providing a ``HomePage`` model with migrations to create one when you first set up your app.
-
-
-Default templates and static files
-----------------------------------
-
-Location: ``/mysite/mysite/templates/`` and ``/mysite/mysite/static/``
-
-The templates directory contains ``base.html``, ``404.html`` and ``500.html``. These files are very commonly needed on Wagtail sites to they have been added into the template.
+            base.py
+            dev.py
+            production.py
+        static/
+            css/
+                mysite.css
+            js/
+                mysite.js
+        templates/
+            404.html
+            500.html
+            base.html
+        __init__.py
+        urls.py
+        wsgi.py
+    Dockerfile
+    manage.py
+    requirements.txt
+```
+
+## The "home" app
+
+Location: `/mysite/home/`
+
+This app is here to help get you started quicker by providing a `HomePage` model with migrations to create one when you first set up your app.
+
+## Default templates and static files
+
+Location: `/mysite/mysite/templates/` and `/mysite/mysite/static/`
+
+The templates directory contains `base.html`, `404.html` and `500.html`. These files are very commonly needed on Wagtail sites to they have been added into the template.
 
 The static directory contains an empty JavaScript and CSS file.
 
+## Django settings
 
-Django settings
----------------
-
-Location: ``/mysite/mysite/settings/``
-
-The Django settings files are split up into ``base.py``, ``dev.py``, ``production.py`` and ``local.py``.
-
-.. glossary::
-
-    ``base.py``
-
-        This file is for global settings that will be used in both development and production. Aim to keep most of your configuration in this file.
-
-    ``dev.py``
-
-        This file is for settings that will only be used by developers. For example: ``DEBUG = True``
-
-    ``production.py``
-
-        This file is for settings that will only run on a production server. For example: ``DEBUG = False``
+Location: `/mysite/mysite/settings/`
 
-    ``local.py``
+The Django settings files are split up into `base.py`, `dev.py`, `production.py` and `local.py`.
 
-        This file is used for settings local to a particular machine. This file should never be tracked by a version control system.
+-   `base.py`
+    This file is for global settings that will be used in both development and production. Aim to keep most of your configuration in this file.
 
-        .. tip::
+-   `dev.py`
+    This file is for settings that will only be used by developers. For example: `DEBUG = True`
 
-            On production servers, we recommend that you only store secrets in ``local.py`` (such as API keys and passwords). This can save you headaches in the future if you are ever trying to debug why a server is behaving badly. If you are using multiple servers which need different settings then we recommend that you create a different ``production.py`` file for each one.
+-   `production.py`
+    This file is for settings that will only run on a production server. For example: `DEBUG = False`
 
+-   `local.py`
+    This file is used for settings local to a particular machine. This file should never be tracked by a version control system.
 
-Dockerfile
-----------
+```{note}
+On production servers, we recommend that you only store secrets in ``local.py`` (such as API keys and passwords). This can save you headaches in the future if you are ever trying to debug why a server is behaving badly. If you are using multiple servers which need different settings then we recommend that you create a different ``production.py`` file for each one.
+```
 
-Location: ``/mysite/Dockerfile``
+## Dockerfile
 
-Contains configuration for building and deploying the site as a `Docker <https://docs.docker.com/>`_ container. To build and use the Docker image for your project, run:
+Location: `/mysite/Dockerfile`
 
-.. code-block:: console
+Contains configuration for building and deploying the site as a [Docker](https://docs.docker.com/) container. To build and use the Docker image for your project, run:
 
-    docker build -t mysite .
-    docker run -p 8000:8000 mysite
+```console
+docker build -t mysite .
+docker run -p 8000:8000 mysite
+```

File diff suppressed because it is too large
+ 336 - 438
docs/reference/settings.md


+ 125 - 155
docs/reference/signals.md

@@ -1,226 +1,196 @@
-.. _signals:
+# Signals
 
-Signals
-=======
-
-Wagtail's :ref:`revision-model-ref` and :ref:`page-model-ref` implement
-:doc:`Signals <topics/signals>` from ``django.dispatch``.
+Wagtail's [](revision_model_ref) and [](page_model_ref) implement [Signals](django:topics/signals) from `django.dispatch`.
 Signals are useful for creating side-effects from page publish/unpublish events.
 
-For example, you could use signals to send publish notifications to a messaging service, or ``POST`` messages to another app that's consuming the API, such as a static site generator.
-
+For example, you could use signals to send publish notifications to a messaging service, or `POST` messages to another app that's consuming the API, such as a static site generator.
 
-``page_published``
-------------------
+## `page_published`
 
-This signal is emitted from a ``Revision`` when a page revision is set to `published`.
+This signal is emitted from a `Revision` when a page revision is set to `published`.
 
-:sender: The page ``class``.
-:instance: The specific ``Page`` instance.
-:revision: The ``Revision`` that was published.
-:kwargs: Any other arguments passed to ``page_published.send()``.
+-   `sender` - The page `class`.
+-   `instance` - The specific `Page` instance.
+-   `revision` - The `Revision` that was published.
+-   `kwargs` - Any other arguments passed to `page_published.send()`.
 
-To listen to a signal, implement ``page_published.connect(receiver, sender, **kwargs)``. Here's a simple
+To listen to a signal, implement `page_published.connect(receiver, sender, **kwargs)`. Here's a simple
 example showing how you might notify your team when something is published:
 
-.. code-block:: python
-
-    from wagtail.signals import page_published
-    import requests
-
+```python
+from wagtail.signals import page_published
+import requests
 
-    # Let everyone know when a new page is published
-    def send_to_slack(sender, **kwargs):
-        instance = kwargs['instance']
-        url = 'https://hooks.slack.com/services/T00000000/B00000000/XXXXXXXXXXXXXXXXXXXXXXXX'
-        values = {
-            "text" : "%s was published by %s " % (instance.title, instance.owner.username),
-            "channel": "#publish-notifications",
-            "username": "the squid of content",
-            "icon_emoji": ":octopus:"
-        }
 
-        response = requests.post(url, values)
+# Let everyone know when a new page is published
+def send_to_slack(sender, **kwargs):
+    instance = kwargs['instance']
+    url = 'https://hooks.slack.com/services/T00000000/B00000000/XXXXXXXXXXXXXXXXXXXXXXXX'
+    values = {
+        "text" : "%s was published by %s " % (instance.title, instance.owner.username),
+        "channel": "#publish-notifications",
+        "username": "the squid of content",
+        "icon_emoji": ":octopus:"
+    }
 
-    # Register a receiver
-    page_published.connect(send_to_slack)
+    response = requests.post(url, values)
 
+# Register a receiver
+page_published.connect(send_to_slack)
+```
 
-Receiving specific model events
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+### Receiving specific model events
 
 Sometimes you're not interested in receiving signals for every model, or you want
 to handle signals for specific models in different ways. For instance, you may
 wish to do something when a new blog post is published:
 
-.. code-block:: python
+```python
+from wagtail.signals import page_published
+from mysite.models import BlogPostPage
 
-    from wagtail.signals import page_published
-    from mysite.models import BlogPostPage
+# Do something clever for each model type
+def receiver(sender, **kwargs):
+    # Do something with blog posts
+    pass
 
-    # Do something clever for each model type
-    def receiver(sender, **kwargs):
-        # Do something with blog posts
-        pass
+# Register listeners for each page model class
+page_published.connect(receiver, sender=BlogPostPage)
+```
 
-    # Register listeners for each page model class
-    page_published.connect(receiver, sender=BlogPostPage)
+Wagtail provides access to a list of registered page types through the `get_page_models()` function in `wagtail.models`.
 
-Wagtail provides access to a list of registered page types through the ``get_page_models()`` function in ``wagtail.models``.
+Read the [Django documentation](django:topics/signals) for more information about specifying senders.
 
-Read the :ref:`Django documentation <connecting-to-specific-signals>` for more information about specifying senders.
+## `page_unpublished`
 
+This signal is emitted from a `Page` when the page is unpublished.
 
-``page_unpublished``
---------------------
+-   `sender` - The page `class`.
+-   `instance` - The specific `Page` instance.
+-   `kwargs` - Any other arguments passed to `page_unpublished.send()`
 
-This signal is emitted from a ``Page`` when the page is unpublished.
+## `pre_page_move` and `post_page_move`
 
-:sender: The page ``class``.
-:instance: The specific ``Page`` instance.
-:kwargs: Any other arguments passed to ``page_unpublished.send()``
+These signals are emitted from a `Page` immediately before and after it is moved.
 
+Subscribe to `pre_page_move` if you need to know values BEFORE any database changes are applied. For example: Getting the page's previous URL, or that of its descendants.
 
-``pre_page_move`` and ``post_page_move``
-------------------------------------------
-
-These signals are emitted from a ``Page`` immediately before and after it is moved.
-
-Subscribe to ``pre_page_move`` if you need to know values BEFORE any database changes are applied. For example: Getting the page's previous URL, or that of its descendants.
-
-Subscribe to ``post_page_move`` if you need to know values AFTER database changes have been applied. For example: Getting the page's new URL, or that of its descendants.
+Subscribe to `post_page_move` if you need to know values AFTER database changes have been applied. For example: Getting the page's new URL, or that of its descendants.
 
 The following arguments are emitted for both signals:
 
-:sender: The page ``class``.
-:instance: The specific ``Page`` instance.
-:parent_page_before: The parent page of ``instance`` **before** moving.
-:parent_page_after: The parent page of ``instance`` **after** moving.
-:url_path_before: The value of ``instance.url_path`` **before** moving.
-:url_path_after: The value of ``instance.url_path`` **after** moving.
-:kwargs: Any other arguments passed to ``pre_page_move.send()`` or ``post_page_move.send()``.
-
+-   `sender` - The page `class`.
+-   `instance` - The specific `Page` instance.
+-   `parent_page_before` - The parent page of `instance` **before** moving.
+-   `parent_page_after` - The parent page of `instance` **after** moving.
+-   `url_path_before` - The value of `instance.url_path` **before** moving.
+-   `url_path_after` - The value of `instance.url_path` **after** moving.
+-   `kwargs` - Any other arguments passed to `pre_page_move.send()` or `post_page_move.send()`.
 
-Distinguishing between a 'move' and a 'reorder'
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+### Distinguishing between a 'move' and a 'reorder'
 
 The signal can be emitted as a result of a page being moved to a different section (a 'move'), or as a result of a page being moved to a different position within the same section (a 'reorder'). Knowing the difference between the two can be particularly useful, because only a 'move' affects a page's URL (and that of its descendants), whereas a 'reorder' only affects the natural page order; which is probably less impactful.
 
-The best way to distinguish between a 'move' and 'reorder' is to compare the ``url_path_before`` and ``url_path_after`` values. For example:
+The best way to distinguish between a 'move' and 'reorder' is to compare the `url_path_before` and `url_path_after` values. For example:
 
-.. code-block:: python
+```python
+from wagtail.signals import pre_page_move
+from wagtail.contrib.frontend_cache.utils import purge_page_from_cache
 
-    from wagtail.signals import pre_page_move
-    from wagtail.contrib.frontend_cache.utils import purge_page_from_cache
+# Clear a page's old URLs from the cache when it moves to a different section
+def clear_page_url_from_cache_on_move(sender, **kwargs):
 
-    # Clear a page's old URLs from the cache when it moves to a different section
-    def clear_page_url_from_cache_on_move(sender, **kwargs):
+    if kwargs['url_path_before'] == kwargs['url_path_after']:
+        # No URLs are changing :) nothing to do here!
+        return
 
-        if kwargs['url_path_before'] == kwargs['url_path_after']:
-            # No URLs are changing :) nothing to do here!
-            return
+    # The page is moving to a new section (possibly even a new site)
+    # so clear old URL(s) from the cache
+    purge_page_from_cache(kwargs['instance'])
 
-        # The page is moving to a new section (possibly even a new site)
-        # so clear old URL(s) from the cache
-        purge_page_from_cache(kwargs['instance'])
+# Register a receiver
+pre_page_move.connect(clear_old_page_urls_from_cache)
+```
 
-    # Register a receiver
-    pre_page_move.connect(clear_old_page_urls_from_cache)
+## `page_slug_changed`
 
-``page_slug_changed``
----------------------
-
-This signal is emitted from a ``Page`` when a change to its slug is published.
+This signal is emitted from a `Page` when a change to its slug is published.
 
 The following arguments are emitted by this signal:
 
-:sender: The page ``class``.
-:instance: The updated (and saved), specific ``Page`` instance.
-:instance_before: A copy of the specific ``Page`` instance from **before** the changes were saved.
-
-workflow_submitted
-------------------
-
-This signal is emitted from a ``WorkflowState`` when a page is submitted to a workflow.
-
-:sender: ``WorkflowState``
-:instance: The specific ``WorkflowState`` instance.
-:user: The user who submitted the workflow
-:kwargs: Any other arguments passed to ``workflow_submitted.send()``
-
-
-workflow_rejected
------------------
-
-This signal is emitted from a ``WorkflowState`` when a page is rejected from a workflow.
-
-:sender: ``WorkflowState``
-:instance: The specific ``WorkflowState`` instance.
-:user: The user who rejected the workflow
-:kwargs: Any other arguments passed to ``workflow_rejected.send()``
+-   `sender` - The page `class`.
+-   `instance` - The updated (and saved), specific `Page` instance.
+-   `instance_before` - A copy of the specific `Page` instance from **before** the changes were saved.
 
+## workflow_submitted
 
-workflow_approved
------------------
+This signal is emitted from a `WorkflowState` when a page is submitted to a workflow.
 
-This signal is emitted from a ``WorkflowState`` when a page's workflow completes successfully
+-   `sender` - `WorkflowState`
+-   `instance` - The specific `WorkflowState` instance.
+-   `user` - The user who submitted the workflow
+-   `kwargs` - Any other arguments passed to `workflow_submitted.send()`
 
-:sender: ``WorkflowState``
-:instance: The specific ``WorkflowState`` instance.
-:user: The user who last approved the workflow
-:kwargs: Any other arguments passed to ``workflow_approved.send()``
+## workflow_rejected
 
+This signal is emitted from a `WorkflowState` when a page is rejected from a workflow.
 
-workflow_cancelled
-------------------
+-   `sender` - `WorkflowState`
+-   `instance` - The specific `WorkflowState` instance.
+-   `user` - The user who rejected the workflow
+-   `kwargs` - Any other arguments passed to `workflow_rejected.send()`
 
-This signal is emitted from a ``WorkflowState`` when a page's workflow is cancelled
+## workflow_approved
 
-:sender: ``WorkflowState``
-:instance: The specific ``WorkflowState`` instance.
-:user: The user who cancelled the workflow
-:kwargs: Any other arguments passed to ``workflow_cancelled.send()``
+This signal is emitted from a `WorkflowState` when a page's workflow completes successfully
 
+-   `sender` - `WorkflowState`
+-   `instance` - The specific `WorkflowState` instance.
+-   `user` - The user who last approved the workflow
+-   `kwargs` - Any other arguments passed to `workflow_approved.send()`
 
-task_submitted
---------------
+## workflow_cancelled
 
-This signal is emitted from a ``TaskState`` when a page is submitted to a task.
+This signal is emitted from a `WorkflowState` when a page's workflow is cancelled
 
-:sender: ``TaskState``
-:instance: The specific ``TaskState`` instance.
-:user: The user who submitted the page to the task
-:kwargs: Any other arguments passed to ``task_submitted.send()``
+-   `sender` - `WorkflowState`
+-   `instance` - The specific `WorkflowState` instance.
+-   `user` - The user who cancelled the workflow
+-   `kwargs` - Any other arguments passed to `workflow_cancelled.send()`
 
+## task_submitted
 
-task_rejected
--------------
+This signal is emitted from a `TaskState` when a page is submitted to a task.
 
-This signal is emitted from a ``TaskState`` when a page is rejected from a task.
+-   `sender` - `TaskState`
+-   `instance` - The specific `TaskState` instance.
+-   `user` - The user who submitted the page to the task
+-   `kwargs` - Any other arguments passed to `task_submitted.send()`
 
-:sender: ``TaskState``
-:instance: The specific ``TaskState`` instance.
-:user: The user who rejected the task
-:kwargs: Any other arguments passed to ``task_rejected.send()``
+## task_rejected
 
+This signal is emitted from a `TaskState` when a page is rejected from a task.
 
-task_approved
--------------
+-   `sender` - `TaskState`
+-   `instance` - The specific `TaskState` instance.
+-   `user` - The user who rejected the task
+-   `kwargs` - Any other arguments passed to `task_rejected.send()`
 
-This signal is emitted from a ``TaskState`` when a page's task is approved
+## task_approved
 
-:sender: ``TaskState``
-:instance: The specific ``TaskState`` instance.
-:user: The user who approved the task
-:kwargs: Any other arguments passed to ``task_approved.send()``
+This signal is emitted from a `TaskState` when a page's task is approved
 
+-   `sender` - `TaskState`
+-   `instance` - The specific `TaskState` instance.
+-   `user` - The user who approved the task
+-   `kwargs` - Any other arguments passed to `task_approved.send()`
 
-task_cancelled
---------------
+## task_cancelled
 
-This signal is emitted from a ``TaskState`` when a page's task is cancelled.
+This signal is emitted from a `TaskState` when a page's task is cancelled.
 
-:sender: ``TaskState``
-:instance: The specific ``TaskState`` instance.
-:user: The user who cancelled the task
-:kwargs: Any other arguments passed to ``task_cancelled.send()``
+-   `sender` - `TaskState`
+-   `instance` - The specific `TaskState` instance.
+-   `user` - The user who cancelled the task
+-   `kwargs` - Any other arguments passed to `task_cancelled.send()`

+ 1 - 1
docs/releases/4.0.md

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

+ 2 - 2
docs/topics/writing_templates.md

@@ -98,7 +98,7 @@ For example:
 
 See [](image_tag) for full documentation.
 
-(rich-text-filter)=
+(rich_text_filter)=
 
 ## Rich text (filter)
 
@@ -112,7 +112,7 @@ Only fields using `RichTextField` need this applied in the template.
 {{ page.body|richtext }}
 ```
 
-(responsive-embeds)=
+(responsive_embeds)=
 
 ### Responsive Embeds
 

Some files were not shown because too many files changed in this diff