amp.rst 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340
  1. How to build a site with AMP support
  2. ====================================
  3. This recipe document describes a method for creating an
  4. `AMP <https://amp.dev/>`_ version of a Wagtail site and hosting it separately
  5. to the rest of the site on a URL prefix. It also describes how to make Wagtail
  6. render images with the ``<amp-img>`` tag when a user is visiting a page on the
  7. AMP version of the site.
  8. Overview
  9. --------
  10. In the next section, we will add a new URL entry that points at Wagtail's
  11. internal ``serve()`` view which will have the effect of rendering the whole
  12. site again under the ``/amp`` prefix.
  13. Then, we will add some utilities that will allow us to track whether the
  14. current request is in the ``/amp`` prefixed version of the site without needing
  15. a request object.
  16. After that, we will add a template context processor to allow us to check from
  17. within templates which version of the site is being rendered.
  18. Then, finally, we will modify the behaviour of the ``{% image %}`` tag to make it
  19. render ``<amp-img>`` tags when rendering the AMP version of the site.
  20. Creating the second page tree
  21. -----------------------------
  22. We can render the whole site at a different prefix by duplicating the Wagtail
  23. URL in the project ``urls.py`` file and giving it a prefix. This must be before
  24. the default URL from Wagtail, or it will try to find ``/amp`` as a page:
  25. .. code-block:: python
  26. # <project>/urls.py
  27. urlpatterns += [
  28. # Add this line just before the default ``include(wagtail_urls)`` line
  29. path('amp/', include(wagtail_urls)),
  30. path('', include(wagtail_urls)),
  31. ]
  32. If you now open ``http://localhost:8000/amp/`` in your browser, you should see
  33. the homepage.
  34. Making pages aware of "AMP mode"
  35. --------------------------------
  36. All the pages will now render under the ``/amp`` prefix, but right now there
  37. isn't any difference between the AMP version and the normal version.
  38. To make changes, we need to add a way to detect which URL was used to render
  39. the page. To do this, we will have to wrap Wagtail's ``serve()`` view and
  40. set a thread-local to indicate to all downstream code that AMP mode is active.
  41. .. note:: Why a thread-local?
  42. (feel free to skip this part if you're not interested)
  43. Modifying the ``request`` object would be the most common way to do this.
  44. However, the image tag rendering is performed in a part of Wagtail that
  45. does not have access to the request.
  46. Thread-locals are global variables that can have a different value for each
  47. running thread. As each thread only handles one request at a time, we can
  48. use it as a way to pass around data that is specific to that request
  49. without having to pass the request object everywhere.
  50. Django uses thread-locals internally to track the currently active language
  51. for the request.
  52. Please be aware though: In Django 3.x and above, you will need to use an
  53. ``asgiref.Local`` instead.
  54. This is because Django 3.x handles multiple requests in a single thread
  55. so thread-locals will no longer be unique to a single request.
  56. Now let's create that thread-local and some utility functions to interact with it,
  57. save this module as ``amp_utils.py`` in an app in your project:
  58. .. code-block:: python
  59. # <app>/amp_utils.py
  60. from contextlib import contextmanager
  61. from threading import local
  62. # FIXME: For Django 3.0 support, replace this with asgiref.Local
  63. _amp_mode_active = local()
  64. @contextmanager
  65. def activate_amp_mode():
  66. """
  67. A context manager used to activate AMP mode
  68. """
  69. _amp_mode_active.value = True
  70. try:
  71. yield
  72. finally:
  73. del _amp_mode_active.value
  74. def amp_mode_active():
  75. """
  76. Returns True if AMP mode is currently active
  77. """
  78. return hasattr(_amp_mode_active, 'value')
  79. This module defines two functions:
  80. - ``activate_amp_mode`` is a context manager which can be invoked using Python's
  81. ``with`` syntax. In the body of the ``with`` statement, AMP mode would be active.
  82. - ``amp_mode_active`` is a function that returns ``True`` when AMP mode is active.
  83. Next, we need to define a view that wraps Wagtail's builtin ``serve`` view and
  84. invokes the ``activate_amp_mode`` context manager:
  85. .. code-block:: python
  86. # <app>/amp_views.py
  87. from django.template.response import SimpleTemplateResponse
  88. from wagtail.core.views import serve as wagtail_serve
  89. from .amp_utils import activate_amp_mode
  90. def serve(request, path):
  91. with activate_amp_mode():
  92. response = wagtail_serve(request, path)
  93. # Render template responses now while AMP mode is still active
  94. if isinstance(response, SimpleTemplateResponse):
  95. response.render()
  96. return response
  97. Then we need to create a ``amp_urls.py`` file in the same app:
  98. .. code-block:: python
  99. # <app>/amp_urls.py
  100. from django.urls import re_path
  101. from wagtail.core.urls import serve_pattern
  102. from . import amp_views
  103. urlpatterns = [
  104. re_path(serve_pattern, amp_views.serve, name='wagtail_amp_serve')
  105. ]
  106. Finally, we need to update the project's main ``urls.py`` to use this new URLs
  107. file for the ``/amp`` prefix:
  108. .. code-block:: python
  109. # <project>/urls.py
  110. from myapp import amp_urls as wagtail_amp_urls
  111. urlpatterns += [
  112. # Change this line to point at your amp_urls instead of Wagtail's urls
  113. path('amp/', include(wagtail_amp_urls)),
  114. re_path(r'', include(wagtail_urls)),
  115. ]
  116. After this, there shouldn't be any noticeable difference to the AMP version of
  117. the site.
  118. Write a template context processor so that AMP state can be checked in templates
  119. --------------------------------------------------------------------------------
  120. This is optional, but worth doing so we can confirm that everything is working
  121. so far.
  122. Add a ``amp_context_processors.py`` file into your app that contains the
  123. following:
  124. .. code-block:: python
  125. # <app>/amp_context_processors.py
  126. from .amp_utils import amp_mode_active
  127. def amp(request):
  128. return {
  129. 'amp_mode_active': amp_mode_active(),
  130. }
  131. Now add the path to this context processor to the
  132. ``['OPTIONS']['context_processors']`` key of the ``TEMPLATES`` setting:
  133. .. code-block:: python
  134. # Either <project>/settings.py or <project>/settings/base.py
  135. TEMPLATES = [
  136. {
  137. ...
  138. 'OPTIONS': {
  139. 'context_processors': [
  140. ...
  141. # Add this after other context processors
  142. 'myapp.amp_context_processors.amp',
  143. ],
  144. },
  145. },
  146. ]
  147. You should now be able to use the ``amp_mode_active`` variable in templates.
  148. For example:
  149. .. code-block:: html+Django
  150. {% if amp_mode_active %}
  151. AMP MODE IS ACTIVE!
  152. {% endif %}
  153. Using a different page template when AMP mode is active
  154. -------------------------------------------------------
  155. You're probably not going to want to use the same templates on the AMP site as
  156. you do on the normal web site. Let's add some logic in to make Wagtail use a
  157. separate template whenever a page is served with AMP enabled.
  158. We can use a mixin, which allows us to re-use the logic on different page types.
  159. Add the following to the bottom of the amp_utils.py file that you created earlier:
  160. .. code-block:: python
  161. # <app>/amp_utils.py
  162. import os.path
  163. ...
  164. class PageAMPTemplateMixin:
  165. @property
  166. def amp_template(self):
  167. # Get the default template name and insert `_amp` before the extension
  168. name, ext = os.path.splitext(self.template)
  169. return name + '_amp' + ext
  170. def get_template(self, request):
  171. if amp_mode_active():
  172. return self.amp_template
  173. return super().get_template(request)
  174. Now add this mixin to any page model, for example:
  175. .. code-block:: python
  176. # <app>/models.py
  177. from .amp_utils import PageAMPTemplateMixin
  178. class MyPageModel(PageAMPTemplateMixin, Page):
  179. ...
  180. When AMP mode is active, the template at ``app_label/mypagemodel_amp.html``
  181. will be used instead of the default one.
  182. If you have a different naming convention, you can override the
  183. ``amp_template`` attribute on the model. For example:
  184. .. code-block:: python
  185. # <app>/models.py
  186. from .amp_utils import PageAMPTemplateMixin
  187. class MyPageModel(PageAMPTemplateMixin, Page):
  188. amp_template = 'my_custom_amp_template.html'
  189. Overriding the ``{% image %}`` tag to output ``<amp-img>`` tags
  190. ---------------------------------------------------------------
  191. Finally, let's change Wagtail's ``{% image %}`` tag, so it renders an ``<amp-img>``
  192. tags when rendering pages with AMP enabled. We'll make the change on the
  193. `Rendition` model itself so it applies to both images rendered with the
  194. ``{% image %}`` tag and images rendered in rich text fields as well.
  195. Doing this with a :ref:`Custom image model <custom_image_model>` is easier, as
  196. you can override the ``img_tag`` method on your custom ``Rendition`` model to
  197. return a different tag.
  198. For example:
  199. .. code-block:: python
  200. from django.forms.utils import flatatt
  201. from django.utils.safestring import mark_safe
  202. from wagtail.images.models import AbstractRendition
  203. ...
  204. class CustomRendition(AbstractRendition):
  205. def img_tag(self, extra_attributes):
  206. attrs = self.attrs_dict.copy()
  207. attrs.update(extra_attributes)
  208. if amp_mode_active():
  209. return mark_safe('<amp-img{}>'.format(flatatt(attrs)))
  210. else:
  211. return mark_safe('<img{}>'.format(flatatt(attrs)))
  212. ...
  213. Without a custom image model, you will have to monkey-patch the builtin
  214. ``Rendition`` model.
  215. Add this anywhere in your project where it would be imported on start:
  216. .. code-block:: python
  217. from django.forms.utils import flatatt
  218. from django.utils.safestring import mark_safe
  219. from wagtail.images.models import Rendition
  220. def img_tag(rendition, extra_attributes={}):
  221. """
  222. Replacement implementation for Rendition.img_tag
  223. When AMP mode is on, this returns an <amp-img> tag instead of an <img> tag
  224. """
  225. attrs = rendition.attrs_dict.copy()
  226. attrs.update(extra_attributes)
  227. if amp_mode_active():
  228. return mark_safe('<amp-img{}>'.format(flatatt(attrs)))
  229. else:
  230. return mark_safe('<img{}>'.format(flatatt(attrs)))
  231. Rendition.img_tag = img_tag