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