2021-02-04 18:28:31 +01:00
|
|
|
How to build a site with AMP support
|
|
|
|
====================================
|
2019-11-05 16:28:45 +01:00
|
|
|
|
|
|
|
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
|
2020-02-17 18:26:32 +01:00
|
|
|
path('amp/', include(wagtail_urls)),
|
2019-11-05 16:28:45 +01:00
|
|
|
|
2020-02-17 18:26:32 +01:00
|
|
|
path('', include(wagtail_urls)),
|
2019-11-05 16:28:45 +01:00
|
|
|
]
|
|
|
|
|
|
|
|
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.
|
|
|
|
|
2022-01-06 12:38:52 +01:00
|
|
|
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.
|
|
|
|
|
2019-11-05 16:28:45 +01:00
|
|
|
|
|
|
|
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
|
2022-01-06 12:38:52 +01:00
|
|
|
from asgiref.local import Local
|
2019-11-05 16:28:45 +01:00
|
|
|
|
2022-01-06 12:38:52 +01:00
|
|
|
_amp_mode_active = Local()
|
2019-11-05 16:28:45 +01:00
|
|
|
|
|
|
|
@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
|
|
|
|
|
2020-02-17 18:26:32 +01:00
|
|
|
from django.urls import re_path
|
2019-11-05 16:28:45 +01:00
|
|
|
from wagtail.core.urls import serve_pattern
|
|
|
|
|
|
|
|
from . import amp_views
|
|
|
|
|
|
|
|
urlpatterns = [
|
2020-02-17 18:26:32 +01:00
|
|
|
re_path(serve_pattern, amp_views.serve, name='wagtail_amp_serve')
|
2019-11-05 16:28:45 +01:00
|
|
|
]
|
|
|
|
|
|
|
|
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
|
2020-02-17 18:26:32 +01:00
|
|
|
path('amp/', include(wagtail_amp_urls)),
|
2019-11-05 16:28:45 +01:00
|
|
|
|
2020-02-21 14:35:53 +01:00
|
|
|
re_path(r'', include(wagtail_urls)),
|
2019-11-05 16:28:45 +01:00
|
|
|
]
|
|
|
|
|
|
|
|
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
|
2021-07-12 17:25:53 +02:00
|
|
|
you do on the normal web site. Let's add some logic in to make Wagtail use a
|
2019-11-05 16:28:45 +01:00
|
|
|
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
|