{"id":13532277,"url":"https://github.com/torchbox/wagtail-headless-preview","last_synced_at":"2025-05-16T02:08:02.617Z","repository":{"id":34842867,"uuid":"184295187","full_name":"torchbox/wagtail-headless-preview","owner":"torchbox","description":"Previews for headless Wagtail setups","archived":false,"fork":false,"pushed_at":"2025-04-07T20:20:15.000Z","size":121,"stargazers_count":128,"open_issues_count":6,"forks_count":20,"subscribers_count":18,"default_branch":"main","last_synced_at":"2025-05-08T13:42:57.199Z","etag":null,"topics":["headless","wagtail","wagtail-cms","wagtail-plugin"],"latest_commit_sha":null,"homepage":"","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"bsd-3-clause","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/torchbox.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2019-04-30T16:22:22.000Z","updated_at":"2025-04-28T14:23:01.000Z","dependencies_parsed_at":"2024-04-22T22:42:52.612Z","dependency_job_id":"8a138ce6-5428-4d7b-b7c8-e64169dfc1ba","html_url":"https://github.com/torchbox/wagtail-headless-preview","commit_stats":{"total_commits":85,"total_committers":8,"mean_commits":10.625,"dds":"0.15294117647058825","last_synced_commit":"761babfb7d64931e5530d82834a44cef4cf56043"},"previous_names":[],"tags_count":12,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/torchbox%2Fwagtail-headless-preview","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/torchbox%2Fwagtail-headless-preview/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/torchbox%2Fwagtail-headless-preview/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/torchbox%2Fwagtail-headless-preview/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/torchbox","download_url":"https://codeload.github.com/torchbox/wagtail-headless-preview/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":254453653,"owners_count":22073617,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["headless","wagtail","wagtail-cms","wagtail-plugin"],"created_at":"2024-08-01T07:01:09.683Z","updated_at":"2025-05-16T02:08:02.595Z","avatar_url":"https://github.com/torchbox.png","language":"Python","funding_links":[],"categories":["Apps"],"sub_categories":["Content Management"],"readme":"# [Wagtail Headless Preview](https://pypi.org/project/wagtail-headless-preview/)\n\n[![Build status](https://img.shields.io/github/actions/workflow/status/torchbox/wagtail-headless-preview/test.yml)](https://github.com/torchbox/wagtail-headless-preview/actions)\n[![PyPI](https://img.shields.io/pypi/v/wagtail-headless-preview.svg)](https://pypi.org/project/wagtail-headless-preview/)\n[![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)\n[![pre-commit.ci status](https://results.pre-commit.ci/badge/github/torchbox/wagtail-headless-preview/main.svg)](https://results.pre-commit.ci/latest/github/torchbox/wagtail-headless-preview/main)\n\n\n## Overview\n\nWith Wagtail as the backend, and a separate app for the front-end (for example a single page React app), editors are no\nlonger able to preview their changes. This is because the front-end is no longer within Wagtail's direct control.\nThe preview data therefore needs to be exposed to the front-end app.\n\nThis package enables previews for Wagtail pages when used in a headless setup by routing the preview to the specified\nfront-end URL.\n\n## Setup\n\nInstall using pip:\n```sh\npip install wagtail-headless-preview\n```\n\nAfter installing the module, add `wagtail_headless_preview` to installed apps in your settings file:\n\n```python\n# settings.py\n\nINSTALLED_APPS = [\n    # ...\n    \"wagtail_headless_preview\",\n]\n```\n\nRun migrations:\n\n```sh\n$ python manage.py migrate\n```\n\nThen configure the preview client URL using the `CLIENT_URLS` option in the `WAGTAIL_HEADLESS_PREVIEW` setting.\n\n## Configuration\n\n`wagtail_headless_preview` uses a single settings dictionary:\n\n```python\n# settings.py\n\nWAGTAIL_HEADLESS_PREVIEW = {\n    \"CLIENT_URLS\": {},  # defaults to an empty dict. You must at the very least define the default client URL.\n    \"SERVE_BASE_URL\": None,  # can be used for HeadlessServeMixin\n    \"REDIRECT_ON_PREVIEW\": False,  # set to True to redirect to the preview instead of using the Wagtail default mechanism\n    \"ENFORCE_TRAILING_SLASH\": True,  # set to False in order to disable the trailing slash enforcement\n}\n```\n\n### Single site setup\n\nFor single sites, add the front-end URL as the default entry:\n\n```python\nWAGTAIL_HEADLESS_PREVIEW = {\n    \"CLIENT_URLS\": {\n        \"default\": \"http://localhost:8020\",\n    }\n}\n```\n\nIf you have configured your Wagtail `Site` entry to use the front-end URL, then you can update your configuration to:\n\n```python\nWAGTAIL_HEADLESS_PREVIEW = {\n    \"CLIENT_URLS\": {\n        \"default\": \"{SITE_ROOT_URL}\",\n    }\n}\n```\n\nThe `{SITE_ROOT_URL}` placeholder is replaced with the `root_url` property of the `Site` the preview page belongs to.\n\n\n### Multi-site setup\n\nFor a multi-site setup, add each site as a separate entry in the `CLIENT_URLS` option in the `WAGTAIL_HEADLESS_PREVIEW` setting:\n\n```python\nWAGTAIL_HEADLESS_PREVIEW = {\n    \"CLIENT_URLS\": {\n        \"default\": \"https://wagtail.org\",  # adjust to match your front-end URL. e.g. locally it may be something like http://localhost:8020\n        \"cms.wagtail.org\": \"https://wagtail.org\",\n        \"cms.torchbox.com\": \"http://torchbox.com\",\n    },\n    # ...\n}\n```\n\n### Serve URL\n\nTo make the editing experience seamles and to avoid server errors due to missing templates,\nyou can use the `HeadlessMixin` which combines the `HeadlessServeMixin` and `HeadlessPreviewMixin` mixins.\n\n`HeadlessServeMixin` overrides the Wagtail `Page.serve` method to redirect to the client URL. By default,\nit uses the hosts defined in `CLIENT_URLS`. However, you can provide a single URL to rule them all:\n\n```python\n# settings.py\n\nWAGTAIL_HEADLESS_PREVIEW = {\n    # ...\n    \"SERVE_BASE_URL\": \"https://my.headless.site\",\n}\n```\n\n### Enforce trailing slash\n\nBy default, `wagtail_headless_preview` enforces a trailing slash on the client URL. You can disable this behaviour by\nsetting `ENFORCE_TRAILING_SLASH` to `False`:\n\n```python\n# settings.py\nWAGTAIL_HEADLESS_PREVIEW = {\n    # ...\n    \"ENFORCE_TRAILING_SLASH\": False\n}\n```\n\n## Usage\n\nTo enable preview as well as wire in the \"View live\" button in the Wagtail UI, add the `HeadlessMixin`\nto your `Page` class:\n\n```python\nfrom wagtail.models import Page\nfrom wagtail_headless_preview.models import HeadlessMixin\n\n\nclass MyWonderfulPage(HeadlessMixin, Page):\n    pass\n```\n\nIf you require more granular control, or if you've modified you `Page` model's `serve` method, you can\nadd `HeadlessPreviewMixin` to your `Page` class to only handle previews:\n\n```python\nfrom wagtail.models import Page\nfrom wagtail_headless_preview.models import HeadlessPreviewMixin\n\n\nclass MyWonderfulPage(HeadlessPreviewMixin, Page):\n    pass\n```\n\n## How will my front-end app display preview content?\n\nThis depends on your project, as it will be dictated by the requirements of your front-end app.\n\nThe following example uses a Wagtail API endpoint to access previews -\nyour app may opt to access page previews using [GraphQL](https://wagtail.io/blog/getting-started-with-wagtail-and-graphql/) instead.\n\n### Example\n\nThis example sets up an API endpoint which will return the preview for a page, and then displays that data\non a simplified demo front-end app.\n\n* Add `wagtail.api.v2` to the installed apps:\n```python\n# settings.py\n\nINSTALLED_APPS = [\n    # ...\n    \"wagtail.api.v2\",\n]\n```\n\n* create an `api.py` file in your project directory:\n\n```python\nfrom django.contrib.contenttypes.models import ContentType\n\nfrom wagtail.api.v2.router import WagtailAPIRouter\nfrom wagtail.api.v2.views import PagesAPIViewSet\n\nfrom wagtail_headless_preview.models import PagePreview\nfrom rest_framework.response import Response\n\n\n# Create the router. \"wagtailapi\" is the URL namespace\napi_router = WagtailAPIRouter(\"wagtailapi\")\n\n\nclass PagePreviewAPIViewSet(PagesAPIViewSet):\n    known_query_parameters = PagesAPIViewSet.known_query_parameters.union(\n        [\"content_type\", \"token\"]\n    )\n\n    def listing_view(self, request):\n        # Delegate to detail_view, specifically so there's no\n        # difference between serialization formats.\n        self.action = \"detail_view\"\n        return self.detail_view(request, 0)\n\n    def detail_view(self, request, pk):\n        page = self.get_object()\n        serializer = self.get_serializer(page)\n        return Response(serializer.data)\n\n    def get_object(self):\n        app_label, model = self.request.GET[\"content_type\"].split(\".\")\n        content_type = ContentType.objects.get(app_label=app_label, model=model)\n\n        page_preview = PagePreview.objects.get(\n            content_type=content_type, token=self.request.GET[\"token\"]\n        )\n        page = page_preview.as_page()\n        if not page.pk:\n            # fake primary key to stop API URL routing from complaining\n            page.pk = 0\n\n        return page\n\n\napi_router.register_endpoint(\"page_preview\", PagePreviewAPIViewSet)\n```\n\n* Register the API URLs so Django can route requests into the API:\n\n```python\n# urls.py\n\nfrom .api import api_router\n\nurlpatterns = [\n    # ...\n    path(\"api/v2/\", api_router.urls),\n    # ...\n    # Ensure that the api_router line appears above the default Wagtail page serving route\n    path(\"\", include(wagtail_urls)),\n]\n```\n\nFor further information about configuring the wagtail API, refer to the [Wagtail API v2 Configuration Guide](https://docs.wagtail.io/en/stable/advanced_topics/api/v2/configuration.html)\n\n* Next, add a `client/index.html` file in your project root. This will query the API to display our preview:\n\n```html\n\u003c!DOCTYPE html\u003e\n\u003chtml\u003e\n\u003chead\u003e\n    \u003cscript\u003e\n        function go() {\n            var querystring = window.location.search.replace(/^\\?/, '');\n            var params = {};\n            querystring.replace(/([^=\u0026]+)=([^\u0026]*)/g, function(m, key, value) {\n                params[decodeURIComponent(key)] = decodeURIComponent(value);\n            });\n\n            var apiUrl = 'http://localhost:8000/api/v2/page_preview/1/?content_type=' + encodeURIComponent(params['content_type']) + '\u0026token=' + encodeURIComponent(params['token']) + '\u0026format=json';\n            fetch(apiUrl).then(function(response) {\n                response.text().then(function(text) {\n                    document.body.innerText = text;\n                });\n            });\n        }\n    \u003c/script\u003e\n\u003c/head\u003e\n\u003cbody onload=\"go()\"\u003e\u003c/body\u003e\n\u003c/html\u003e\n```\n\n\n* Install [django-cors-headers](https://pypi.org/project/django-cors-headers/): `pip install django-cors-headers`\n* Add CORS config to your settings file to allow the front-end to access the API\n\n```python\n# settings.py\nCORS_ORIGIN_ALLOW_ALL = True\nCORS_URLS_REGEX = r\"^/api/v2/\"\n```\n\nand follow the rest of the [setup instructions for django-cors-headers](https://github.com/ottoyiu/django-cors-headers#setup).\n\n* Start up your site as normal: `python manage.py runserver 0:8000`\n* Serve the front-end `client/index.html` at `http://localhost:8020/`\n   - this can be done by running `python3 -m http.server 8020` from inside the client directory\n* From the wagtail admin interface, edit (or create) and preview a page that uses `HeadlessPreviewMixin`\n\nThe preview page should now show you the API response for the preview! 🎉\n\nThis is where a real front-end would take over and display the preview as it would be seen on the live site.\n\n## Contributing\n\nAll contributions are welcome!\n\nNote that this project uses [pre-commit](https://github.com/pre-commit/pre-commit). To set up locally:\n\n```shell\n# if you don't have it yet\n$ pip install pre-commit\n# go to the project directory\n$ cd wagtail-headless-preview\n# initialize pre-commit\n$ pre-commit install\n\n# Optional, run all checks once for this, then the checks will run only on the changed files\n$ pre-commit run --all-files\n```\n\n### How to run tests\n\nNow you can run tests as shown below:\n\n```sh\ntox -p\n```\n\nor, you can run them for a specific environment `tox -e py311-django4.2-wagtail5.1` or specific test\n`tox -e py311-django4.2-wagtail5.0 -- wagtail_headless_preview.tests.test_frontend.TestFrontendViews.test_redirect_on_preview`\n\n## Credits\n\n- Matthew Westcott ([@gasman](https://github.com/gasman)), initial proof of concept\n- Karl Hobley ([@kaedroho](https://github.com/kaedroho)), PoC improvements\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftorchbox%2Fwagtail-headless-preview","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ftorchbox%2Fwagtail-headless-preview","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftorchbox%2Fwagtail-headless-preview/lists"}