From 5845cfbfe9b2b1c2dd25f43b32d011426df62fc0 Mon Sep 17 00:00:00 2001 From: Karl Hobley Date: Tue, 5 Nov 2019 15:28:45 +0000 Subject: [PATCH] Add Recipe doc for implementing AMP on a site (#5626) * Add Recipe doc for implementing AMP on a site * Fix typos * Add section on using a different page template for AMP * Move AMP recipe out of images Not very image specific anymore * Wording tweaks and grammar fixes --- docs/advanced_topics/amp.rst | 340 +++++++++++++++++++++++++++++++++ docs/advanced_topics/index.rst | 1 + 2 files changed, 341 insertions(+) create mode 100644 docs/advanced_topics/amp.rst diff --git a/docs/advanced_topics/amp.rst b/docs/advanced_topics/amp.rst new file mode 100644 index 0000000000..ee7c4dae91 --- /dev/null +++ b/docs/advanced_topics/amp.rst @@ -0,0 +1,340 @@ +Building a site with AMP support +================================ + +This recipe document describes a method for creating an +`AMP `_ 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 ```` 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 ```` 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 + + # /urls.py + + urlpatterns += [ + # Add this line just before the default ``include(wagtail_urls)`` line + url(r'amp/', include(wagtail_urls)), + + url(r'', 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. + + Please be aware though: In Django 3.x and above, you will need to use an + ``asgiref.Local`` instead. + This is because Django 3.x handles multiple requests in a single thread + so thread-locals will no longer be unique to a single request. + +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 + + # /amp_utils.py + + from contextlib import contextmanager + from threading import local + + # FIXME: For Django 3.0 support, replace this with asgiref.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 + + # /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 + + # /amp_urls.py + + from django.conf.urls import url + from wagtail.core.urls import serve_pattern + + from . import amp_views + + urlpatterns = [ + url(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 + + # /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 + url(r'amp/', include(wagtail_amp_urls)), + + url(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 + + # /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 /settings.py or /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 regular 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 + + # /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 + + # /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 + + # /models.py + + from .amp_utils import PageAMPTemplateMixin + + class MyPageModel(PageAMPTemplateMixin, Page): + amp_template = 'my_custom_amp_template.html' + +Overriding the ``{% image %}`` tag to output ```` tags +--------------------------------------------------------------- + +Finally, let's change Wagtail's ``{% image %}`` tag, so it renders an ```` +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 ` 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(''.format(flatatt(attrs))) + else: + return mark_safe(''.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 tag instead of an tag + """ + attrs = rendition.attrs_dict.copy() + attrs.update(extra_attributes) + + if amp_mode_active(): + return mark_safe(''.format(flatatt(attrs))) + else: + return mark_safe(''.format(flatatt(attrs))) + + Rendition.img_tag = img_tag diff --git a/docs/advanced_topics/index.rst b/docs/advanced_topics/index.rst index e9b581b5af..27dbb00cf7 100644 --- a/docs/advanced_topics/index.rst +++ b/docs/advanced_topics/index.rst @@ -18,3 +18,4 @@ Advanced topics jinja2 testing api/index + amp