Package: wagtail-webstories
wagtail-webstoriesThis package adds support forAMP web storiesto Wagtail. Stories created elsewhere can be linked or embedded from their original URL locations, or imported as Wagtail pages, including optionally importing images and videos into the Wagtail media library.wagtail-webstoriesis compatible with Wagtail 4.1 and above.InstallationInstall with pip:pipinstallwagtail-webstoriesAdd to INSTALLED_APPS:INSTALLED_APPS=[# ...'wagtail_webstories',# ...]Run migrations:./manage.pymigrateEmbedding and linking external storiesTo embed a web story into a regular (non-AMP) StreamField-based page, include thewagtail_webstories.blocks.ExternalStoryEmbedBlockblock type in your StreamField definition:fromwagtail_webstories.blocksimportExternalStoryEmbedBlockclassBlogPage(Page):body=StreamField([('heading',blocks.CharBlock()),('paragraph',blocks.RichTextBlock()),('story_embed',ExternalStoryEmbedBlock()),])This block allows the page author to provide the URL to an AMP web story, which will render on the front-end template (when using{% include_block %}) as an<amp-story-player>element; your template should includethe necessary scripts for rendering this, along with a CSS rule to specify appropriate dimensions:<scriptasyncsrc="https://cdn.ampproject.org/amp-story-player-v0.js"></script><linkhref="https://cdn.ampproject.org/amp-story-player-v0.css"rel="stylesheet"type="text/css"><style>amp-story-player{width:360px;height:600px;}</style>To include a link to a story rather than embedding it,wagtail_webstories.blocks.ExternalStoryBlockcan be used in place ofExternalStoryEmbedBlock. The default templatewagtail_webstories/blocks/external_story_poster_link.htmloutputs a 'card' rendering of the story using the story's poster image, to be used with the following CSS:.webstory-poster{display:block;width:300px;height:400px;border-radius:15px;background-size:cover;position:relative;}.webstory-poster.webstory-info{position:absolute;bottom:0;width:100%;background-color:#ccc;color:black;border-bottom-left-radius:15px;border-bottom-right-radius:15px;}.webstory-poster.title{font-size:1.5em;padding:10px;}.webstory-poster.publisher{padding:010px20px10px;}.webstory-poster.publisherimg{vertical-align:middle;}Embedding and linking external stories without StreamFieldExternal stories are handled through the modelwagtail_webstories.models.ExternalStory. To obtain an ExternalStory instance for a given URL, use:ExternalStory.get_for_url(story_url). The story's metadata is cached within the ExternalStory model to avoid having to re-fetch the story on every request - the available metadata fields areurl,title,publisher,publisher_logo_src,poster_portrait_src,poster_square_srcandposter_landscape_src.The StreamField block templateswagtail_webstories/blocks/external_story_embed_block.htmlandwagtail_webstories/blocks/external_story_poster_link.htmlcan be used to render an ExternalStory object, by passing it as the variablestory:# models.pyclassBlogPostWithStoryPage(Page):story_url=model.URLField(max_length=2048)defget_context(self,request):context=super().get_context(request)context['story_obj']=ExternalStory.get_for_url(self.story_url)returncontext{# blog_post_with_story_page.html #}{%include"wagtail_webstories/blocks/external_story_embed_block.html"withstory=story_obj%}ImportingTo allow stories to be imported as Wagtail pages, define a model that extendswagtail_webstories.models.BaseWebStoryPage:fromwagtail_webstories.modelsimportBaseWebStoryPageclassStoryPage(BaseWebStoryPage):passAlternatively, if your project has an existing base page class that all page types must inherit from (which would prevent the use of BaseWebStoryPage in addition to that base class), extendwagtail_webstories.models.WebStoryPageMixinand definecontent_panelsandpromote_panelsto incorporate its panel definitions:fromwagtail_webstories.modelsimportWebStoryPageMixinclassStoryPage(WebStoryPageMixin,BasePage):content_panels=BasePage.content_panels+WebStoryPageMixin.web_story_content_panelspromote_panels=BasePage.promote_panels+WebStoryPageMixin.web_story_promote_panelsNow create a corresponding template that extendswagtail_webstories/base_web_story_page.html:{%extends"wagtail_webstories/base_web_story_page.html"%}Define a settingWAGTAIL_WEBSTORIES_IMPORT_MODELpointing to the page model to use:WAGTAIL_WEBSTORIES_IMPORT_MODEL='myapp.StoryPage'This will now add a "Web stories" item to the Wagtail admin menu, allowing you to import stories by URL.HTML cleaningBy default, all HTML elements and attributes not permitted by the AMP web story specification will be stripped out on importing and saving. To disable this, setWAGTAIL_WEBSTORIES_CLEAN_HTMLto False:WAGTAIL_WEBSTORIES_CLEAN_HTML=FalseImage importingBy default, image references within imported stories are left at their original URLs. BaseWebStoryPage provides a methodimport_images()to fetch all referenced images and import them into the Wagtail image library, de-duplicating if they already exist. It is recommended that you call this from apost_savesignal handler:# myapp/signals.pyfromdjango.db.models.signalsimportpost_savefromdjango.dispatchimportreceiverfrom.modelsimportStoryPage@receiver(post_save,sender=StoryPage)defimport_story_images(sender,instance,**kwargs):changed=instance.import_images()ifchanged:instance.save()# myapp/apps.pyfromdjango.appsimportAppConfigclassMyappConfig(AppConfig):name='myapp'defready(self):importmyapp.signals# noqa# myapp/__init__.pydefault_app_config='myapp.apps.MyappConfig'Since importing images can be a time-consuming process, you may wish to offload the call toimport_imagesto a background task using Celery or similar, to avoid this blocking a web server thread.To customise the creation of new images (e.g. to assign imported images to a particular collection, or to populate additional metadata fields on a custom image model), override the story page model's_create_imagemethod:classStoryPage(BaseWebStoryPage):def_create_image(self,file,title=None):image=super()._create_image(file,title=title)image.copyright="All rights reserved"returnimageVideo importingIf you havewagtailmediainstalled, you can similarly import videos into the local media library by callingimport_videos(). The signal handler above then becomes:# myapp/signals.py@receiver(post_save,sender=StoryPage)defimport_story_images(sender,instance,**kwargs):images_changed=instance.import_images()videos_changed=instance.import_videos()ifimages_changedorvideos_changed:instance.save()Linking and embedding imported storiesTo embed or link an imported web story into a regular (non-AMP) StreamField-based page, include thewagtail_webstories.blocks.StoryEmbedBlockorwagtail_webstories.blocks.StoryChooserBlockblock type in your StreamField definition. These work similarly to ExternalStoryEmbedBlock and ExternalStoryBlock, but provide the page author with a page chooser interface rather than a URL field.fromwagtail_webstories.blocksimportStoryEmbedBlockclassBlogPage(Page):body=StreamField([('heading',blocks.CharBlock()),('paragraph',blocks.RichTextBlock()),('local_story_embed',StoryEmbedBlock(target_model=StoryPage)),])Thetarget_modelargument is optional - by default, any page type inheriting from BaseWebStoryPage can be chosen. As with ExternalStoryEmbedBlock and ExternalStoryBlock, your page template must contain the appropriate JavaScript or CSS include for rendering the block.The templateswagtail_webstories/blocks/story_poster_link.htmlandwagtail_webstories/blocks/story_embed_block.htmlexpect a single variablepagecontaining the story page instance, so these can also be used outside of StreamField:# models.pyclassStoryIndexPage(Page):defget_context(self,request):context=super().get_context(request)context['stories']=StoryPage.objects.child_of(self).live().order_by('-first_published_at')returncontext{# story_index_page.html #}{%forstoryinstories%}{%include"wagtail_webstories/blocks/story_poster_link.html"withpage=story%}{%endfor%}