Metadata-Version: 2.4
Name: wagtail-headless-preview
Version: 0.9.0
Summary: Enhance Wagtail previews in headless setups.
Keywords: Wagtail,Django,headless
Author-email: Dan Braghis <dan.braghis@torchbox.com>
Requires-Python: >=3.10
Description-Content-Type: text/markdown
Classifier: Development Status :: 4 - Beta
Classifier: Environment :: Web Environment
Classifier: Intended Audience :: Developers
Classifier: Operating System :: OS Independent
Classifier: License :: OSI Approved :: BSD License
Classifier: Programming Language :: Python
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Framework :: Django
Classifier: Framework :: Django :: 4.2
Classifier: Framework :: Django :: 5.2
Classifier: Framework :: Django :: 6.0
Classifier: Framework :: Wagtail
Classifier: Framework :: Wagtail :: 7
Classifier: Topic :: Internet :: WWW/HTTP
Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content
License-File: LICENSE
Requires-Dist: Wagtail>=7.0
Requires-Dist: django-cors-headers ; extra == "testing"
Requires-Dist: tox>=4 ; extra == "testing"
Requires-Dist: coverage[toml]>=7.0,<8.0 ; extra == "testing"
Project-URL: Changelog, https://github.com/torchbox/wagtail-headless-preview/blob/main/CHANGELOG.md
Project-URL: Issues, https://github.com/torchbox/wagtail-headless-preview/issues
Project-URL: Repository, https://github.com/torchbox/wagtail-headless-preview
Provides-Extra: testing

# [Wagtail Headless Preview](https://pypi.org/project/wagtail-headless-preview/)

[![Build status](https://img.shields.io/github/actions/workflow/status/torchbox/wagtail-headless-preview/test.yml)](https://github.com/torchbox/wagtail-headless-preview/actions)
[![PyPI](https://img.shields.io/pypi/v/wagtail-headless-preview.svg)](https://pypi.org/project/wagtail-headless-preview/)
[![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)
[![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)


## Overview

With 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.

## Setup

Install using pip:
```sh
pip install wagtail-headless-preview
```

After installing the module, add `wagtail_headless_preview` to installed apps in your settings file:

```python
# settings.py

INSTALLED_APPS = [
    # ...
    "wagtail_headless_preview",
]
```

Run migrations:

```sh
$ python manage.py migrate
```

Then configure the preview client URL using the `CLIENT_URLS` option in the `WAGTAIL_HEADLESS_PREVIEW` setting.

## Configuration

`wagtail_headless_preview` uses a single settings dictionary:

```python
# settings.py

WAGTAIL_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 client URL instead of using an iframe
    "ENFORCE_TRAILING_SLASH": True,  # set to False in order to disable the trailing slash enforcement
}
```

### Single site setup

For single sites, add the front-end URL as the default entry:

```python
WAGTAIL_HEADLESS_PREVIEW = {
    "CLIENT_URLS": {
        "default": "http://localhost:8020",
    }
}
```

If you have configured your Wagtail `Site` entry to use the front-end URL, then you can update your configuration to:

```python
WAGTAIL_HEADLESS_PREVIEW = {
    "CLIENT_URLS": {
        "default": "{SITE_ROOT_URL}",
    }
}
```

The `{SITE_ROOT_URL}` placeholder is replaced with the `root_url` property of the `Site` the preview page belongs to.


### Multi-site setup

For a multi-site setup, add each site as a separate entry in the `CLIENT_URLS` option in the `WAGTAIL_HEADLESS_PREVIEW` setting:

```python
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 URL

To make the editing experience seamles and to avoid server errors due to missing templates,
you can use the `HeadlessMixin` which combines the `HeadlessServeMixin` and `HeadlessPreviewMixin` mixins.

`HeadlessServeMixin` overrides the Wagtail `Page.serve` method to redirect to the client URL. By default,
it uses the hosts defined in `CLIENT_URLS`. However, you can provide a single URL to rule them all:

```python
# settings.py

WAGTAIL_HEADLESS_PREVIEW = {
    # ...
    "SERVE_BASE_URL": "https://my.headless.site",
}
```

### Enforce trailing slash

By default, `wagtail_headless_preview` enforces a trailing slash on the client URL. You can disable this behaviour by
setting `ENFORCE_TRAILING_SLASH` to `False`:

```python
# settings.py
WAGTAIL_HEADLESS_PREVIEW = {
    # ...
    "ENFORCE_TRAILING_SLASH": False
}
```

### Redirect vs. iframe

By default, the preview will render an iframe with the front-end URL. This means that the live preview panel will be displayed using an iframe nested within Wagtail's default preview iframe. Prior to Wagtail 7.1, this was necessary if the frontend is hosted on a separate domain, as the preview panel did not properly handle cross-origin iframes.

If you are using Wagtail 7.1+, it is highly recommended to use a redirect to the frontend URL instead of a nested iframe, regardless whether the frontend is hosted on the same domain or not. This allows Wagtail to enable scroll restoration and other integrations within the page editor. This can be enabled by setting `REDIRECT_ON_PREVIEW` to `True`:

```python
# settings.py
WAGTAIL_HEADLESS_PREVIEW = {
    # ...
    "REDIRECT_ON_PREVIEW": True,
}
```

### User bar and accessibility checker

If the Wagtail userbar is loaded in the front-end, Wagtail's page editor can restore the scroll position when the preview is updated, as well as enable other integrations such as the accessibility checker and content metrics. See Wagtail's documentation on enabling [the user bar](https://docs.wagtail.org/en/latest/advanced_topics/headless.html#headless-user-bar) and [the accessibility checker](https://docs.wagtail.org/en/latest/advanced_topics/headless.html#headless-accessibility-checker) on headless setups for more information. This requires Wagtail 7.1+ and the `REDIRECT_ON_PREVIEW` setting set to `True`.

## Usage

To enable preview as well as wire in the "View live" button in the Wagtail UI, add the `HeadlessMixin`
to your `Page` class:

```python
from wagtail.models import Page
from wagtail_headless_preview.models import HeadlessMixin


class MyWonderfulPage(HeadlessMixin, Page):
    pass
```

If you require more granular control, or if you've modified you `Page` model's `serve` method, you can
add `HeadlessPreviewMixin` to your `Page` class to only handle previews:

```python
from wagtail.models import Page
from wagtail_headless_preview.models import HeadlessPreviewMixin


class MyWonderfulPage(HeadlessPreviewMixin, Page):
    pass
```

## How 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 using [GraphQL](https://wagtail.io/blog/getting-started-with-wagtail-and-graphql/) instead.

### Example

This 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.

* Add `wagtail.api.v2` to the installed apps:
```python
# settings.py

INSTALLED_APPS = [
    # ...
    "wagtail.api.v2",
]
```

* create an `api.py` file in your project directory:

```python
from django.contrib.contenttypes.models import ContentType

from wagtail.api.v2.router import WagtailAPIRouter
from wagtail.api.v2.views import PagesAPIViewSet

from wagtail_headless_preview.models import PagePreview
from rest_framework.response import Response


# Create the router. "wagtailapi" is the URL namespace
api_router = WagtailAPIRouter("wagtailapi")


class PagePreviewAPIViewSet(PagesAPIViewSet):
    known_query_parameters = PagesAPIViewSet.known_query_parameters.union(
        ["content_type", "token"]
    )

    def listing_view(self, request):
        # Delegate to detail_view, specifically so there's no
        # difference between serialization formats.
        self.action = "detail_view"
        return self.detail_view(request, 0)

    def detail_view(self, request, pk):
        page = self.get_object()
        serializer = self.get_serializer(page)
        return Response(serializer.data)

    def get_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()
        if not page.pk:
            # fake primary key to stop API URL routing from complaining
            page.pk = 0

        return page


api_router.register_endpoint("page_preview", PagePreviewAPIViewSet)
```

* Register the API URLs so Django can route requests into the API:

```python
# urls.py

from .api import api_router

urlpatterns = [
    # ...
    path("api/v2/", api_router.urls),
    # ...
    # Ensure that the api_router line appears above the default Wagtail page serving route
    path("", include(wagtail_urls)),
]
```

For 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)

* Next, add a `client/index.html` file in your project root. This will query the API to display our preview:

```html
<!DOCTYPE html>
<html>
<head>
    <script>
        function go() {
            var querystring = window.location.search.replace(/^\?/, '');
            var params = {};
            querystring.replace(/([^=&]+)=([^&]*)/g, function(m, key, value) {
                params[decodeURIComponent(key)] = decodeURIComponent(value);
            });

            var apiUrl = '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>
<body onload="go()"></body>
</html>
```


* Install [django-cors-headers](https://pypi.org/project/django-cors-headers/): `pip install django-cors-headers`
* Add CORS config to your settings file to allow the front-end to access the API

```python
# settings.py
CORS_ORIGIN_ALLOW_ALL = True
CORS_URLS_REGEX = r"^/api/v2/"
```

and follow the rest of the [setup instructions for django-cors-headers](https://github.com/ottoyiu/django-cors-headers#setup).

* Start up your site as normal: `python manage.py runserver 127.0.0.1:8000`
* Serve the front-end `client/index.html` at `http://localhost:8020/`
   - this can be done by running `python3 -m http.server 8020` from inside the client directory
* From the wagtail admin interface, edit (or create) and preview a page that uses `HeadlessPreviewMixin`

The 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.

## Contributing

All contributions are welcome!

Note that this project uses [pre-commit](https://github.com/pre-commit/pre-commit). To set up locally:

```shell
# if you don't have it yet
$ pip install pre-commit
# go to the project directory
$ cd wagtail-headless-preview
# initialize pre-commit
$ pre-commit install

# Optional, run all checks once for this, then the checks will run only on the changed files
$ pre-commit run --all-files
```

### How to run tests

Now you can run tests as shown below:

```sh
tox -p
```

or, you can run them for a specific environment `tox -e python3.14-django5.2-wagtail7.4` or specific test
`tox -e python3.14-django5.2-wagtail7.4 -- wagtail_headless_preview.tests.test_frontend.TestFrontendViews.test_redirect_on_preview`

## Credits

- Matthew Westcott ([@gasman](https://github.com/gasman)), initial proof of concept
- Karl Hobley ([@kaedroho](https://github.com/kaedroho)), PoC improvements

