Package: wagtail-headless-preview
Wagtail Headless PreviewOverviewWith Wagtail as the backend, and a separate app for the front-end (for example a single page React app), editors are no
longer able to preview their changes. This is because the front-end is no longer within Wagtail's direct control.
The preview data therefore needs to be exposed to the front-end app.This package enables previews for Wagtail pages when used in a headless setup by routing the preview to the specified
front-end URL.SetupInstall using pip:pipinstallwagtail-headless-previewAfter installing the module, addwagtail_headless_previewto installed apps in your settings file:# settings.pyINSTALLED_APPS=[# ..."wagtail_headless_preview",]Run migrations:$pythonmanage.pymigrateThen configure the preview client URL using theCLIENT_URLSoption in theWAGTAIL_HEADLESS_PREVIEWsetting.Configurationwagtail_headless_previewuses a single settings dictionary:# settings.pyWAGTAIL_HEADLESS_PREVIEW={"CLIENT_URLS":{},# defaults to an empty dict. You must at the very least define the default client URL."SERVE_BASE_URL":None,# can be used for HeadlessServeMixin"REDIRECT_ON_PREVIEW":False,# set to True to redirect to the preview instead of using the Wagtail default mechanism"ENFORCE_TRAILING_SLASH":True,# set to False in order to disable the trailing slash enforcement}Single site setupFor single sites, add the front-end URL as the default entry:WAGTAIL_HEADLESS_PREVIEW={"CLIENT_URLS":{"default":"http://localhost:8020",}}If you have configured your WagtailSiteentry to use the front-end URL, then you can update your configuration to:WAGTAIL_HEADLESS_PREVIEW={"CLIENT_URLS":{"default":"{SITE_ROOT_URL}",}}The{SITE_ROOT_URL}placeholder is replaced with theroot_urlproperty of theSitethe preview page belongs to.Multi-site setupFor a multi-site setup, add each site as a separate entry in theCLIENT_URLSoption in theWAGTAIL_HEADLESS_PREVIEWsetting:WAGTAIL_HEADLESS_PREVIEW={"CLIENT_URLS":{"default":"https://wagtail.org",# adjust to match your front-end URL. e.g. locally it may be something like http://localhost:8020"cms.wagtail.org":"https://wagtail.org","cms.torchbox.com":"http://torchbox.com",},# ...}Serve URLTo make the editing experience seamles and to avoid server errors due to missing templates,
you can use theHeadlessMixinwhich combines theHeadlessServeMixinandHeadlessPreviewMixinmixins.HeadlessServeMixinoverrides the WagtailPage.servemethod to redirect to the client URL. By default,
it uses the hosts defined inCLIENT_URLS. However, you can provide a single URL to rule them all:# settings.pyWAGTAIL_HEADLESS_PREVIEW={# ..."SERVE_BASE_URL":"https://my.headless.site",}Enforce trailing slashBy default,wagtail_headless_previewenforces a trailing slash on the client URL. You can disable this behaviour by
settingENFORCE_TRAILING_SLASHtoFalse:# settings.pyWAGTAIL_HEADLESS_PREVIEW={# ..."ENFORCE_TRAILING_SLASH":False}UsageTo enable preview as well as wire in the "View live" button in the Wagtail UI, add theHeadlessMixinto yourPageclass:fromwagtail.modelsimportPagefromwagtail_headless_preview.modelsimportHeadlessMixinclassMyWonderfulPage(HeadlessMixin,Page):passIf you require more granular control, or if you've modified youPagemodel'sservemethod, you can
addHeadlessPreviewMixinto yourPageclass to only handle previews:fromwagtail.modelsimportPagefromwagtail_headless_preview.modelsimportHeadlessPreviewMixinclassMyWonderfulPage(HeadlessPreviewMixin,Page):passHow will my front-end app display preview content?This depends on your project, as it will be dictated by the requirements of your front-end app.The following example uses a Wagtail API endpoint to access previews -
your app may opt to access page previews usingGraphQLinstead.ExampleThis example sets up an API endpoint which will return the preview for a page, and then displays that data
on a simplified demo front-end app.Addwagtail.api.v2to the installed apps:# settings.pyINSTALLED_APPS=[# ..."wagtail.api.v2",]create anapi.pyfile in your project directory:fromdjango.contrib.contenttypes.modelsimportContentTypefromwagtail.api.v2.routerimportWagtailAPIRouterfromwagtail.api.v2.viewsimportPagesAPIViewSetfromwagtail_headless_preview.modelsimportPagePreviewfromrest_framework.responseimportResponse# Create the router. "wagtailapi" is the URL namespaceapi_router=WagtailAPIRouter("wagtailapi")classPagePreviewAPIViewSet(PagesAPIViewSet):known_query_parameters=PagesAPIViewSet.known_query_parameters.union(["content_type","token"])deflisting_view(self,request):# Delegate to detail_view, specifically so there's no# difference between serialization formats.self.action="detail_view"returnself.detail_view(request,0)defdetail_view(self,request,pk):page=self.get_object()serializer=self.get_serializer(page)returnResponse(serializer.data)defget_object(self):app_label,model=self.request.GET["content_type"].split(".")content_type=ContentType.objects.get(app_label=app_label,model=model)page_preview=PagePreview.objects.get(content_type=content_type,token=self.request.GET["token"])page=page_preview.as_page()ifnotpage.pk:# fake primary key to stop API URL routing from complainingpage.pk=0returnpageapi_router.register_endpoint("page_preview",PagePreviewAPIViewSet)Register the API URLs so Django can route requests into the API:# urls.pyfrom.apiimportapi_routerurlpatterns=[# ...path("api/v2/",api_router.urls),# ...# Ensure that the api_router line appears above the default Wagtail page serving routepath("",include(wagtail_urls)),]For further information about configuring the wagtail API, refer to theWagtail API v2 Configuration GuideNext, add aclient/index.htmlfile in your project root. This will query the API to display our preview:<!DOCTYPE html><html><head><script>functiongo(){varquerystring=window.location.search.replace(/^\?/,'');varparams={};querystring.replace(/([^=&]+)=([^&]*)/g,function(m,key,value){params[decodeURIComponent(key)]=decodeURIComponent(value);});varapiUrl='http://localhost:8000/api/v2/page_preview/1/?content_type='+encodeURIComponent(params['content_type'])+'&token='+encodeURIComponent(params['token'])+'&format=json';fetch(apiUrl).then(function(response){response.text().then(function(text){document.body.innerText=text;});});}</script></head><bodyonload="go()"></body></html>Installdjango-cors-headers:pip install django-cors-headersAdd CORS config to your settings file to allow the front-end to access the API# settings.pyCORS_ORIGIN_ALLOW_ALL=TrueCORS_URLS_REGEX=r"^/api/v2/"and follow the rest of thesetup instructions for django-cors-headers.Start up your site as normal:python manage.py runserver 0:8000Serve the front-endclient/index.htmlathttp://localhost:8020/this can be done by runningpython3 -m http.server 8020from inside the client directoryFrom the wagtail admin interface, edit (or create) and preview a page that usesHeadlessPreviewMixinThe preview page should now show you the API response for the preview! 🎉This is where a real front-end would take over and display the preview as it would be seen on the live site.ContributingAll contributions are welcome!Note that this project usespre-commit. To set up locally:# if you don't have it yet$pipinstallpre-commit# go to the project directory$cdwagtail-headless-preview# initialize pre-commit$pre-commitinstall# Optional, run all checks once for this, then the checks will run only on the changed files$pre-commitrun--all-filesHow to run testsNow you can run tests as shown below:tox-por, you can run them for a specific environmenttox -e py311-django4.2-wagtail5.1or specific testtox -e py311-django4.2-wagtail5.0 -- wagtail_headless_preview.tests.test_frontend.TestFrontendViews.test_redirect_on_previewCreditsMatthew Westcott (@gasman), initial proof of conceptKarl Hobley (@kaedroho), PoC improvements