Skip to main content

wagtail-visual-editor

A visual page editor for headless Wagtail. Your editors work on top of the real frontend preview (Next.js, Astro, or anything else) instead of a long form:

  • hover a block in the preview to get Edit, move up, move down, Duplicate and Delete, and a + on its top and bottom edges to add a block above or below;
  • each block is edited in a modal that uses Wagtail's own block form (choosers, rich text, TabbedStructBlock, validation, everything);
  • a collapsible tree index of the page, with an eye toggle to hide a block from the live site without deleting it, and a full screen button that hides it;
  • copy a block from the tree index and paste it on any other page from the block picker;
  • a Show StreamField button to switch to Wagtail's standard StreamField and back, at any moment;
  • mobile / tablet / desktop widths;
  • the preview's action bar takes your site's colors through CSS variables;
  • the block picker previews each block rendered by your frontend;
  • live social share previews (Google, WhatsApp, Facebook, X, LinkedIn, Slack, Pinterest) for the promote tab.

Wagtail's native StreamField stays in the form, hidden. It is still the source of truth: saving, drafts, autosave, revisions and validation work exactly as before. The editor only drives it through its JavaScript API.

The package has two parts, released together with the same version number:

Part Package Where it runs
Django app wagtail-visual-editor (PyPI) Wagtail admin
Preview bridge wagtail-visual-editor-bridge (npm) Your frontend's preview route

Compatibility

wagtail-visual-editor Wagtail Python Django
0.1.x 7.4 3.10+ as required by Wagtail

The Wagtail range is capped on purpose: the editor uses Wagtail's internal StreamField JavaScript API, which can change in any minor release. A new Wagtail minor is supported only after it has been tested. If you force an untested version, manage.py check reports the warning wagtail_visual_editor.W001.

Requirements

  • A headless preview that already works: Wagtail's preview must redirect to your frontend, for example with wagtail-headless-preview.
  • The page's Site points to the frontend. The editor trusts messages only from the Site's root_url (scheme, hostname and port), so it must be the origin that serves the preview.
  • The frontend's preview routes can be embedded in an iframe by the Wagtail admin: allow the admin origin in Content-Security-Policy: frame-ancestors (and don't send X-Frame-Options: DENY) on /preview and /preview/block.

Installation

Backend

pip install wagtail-visual-editor
# settings.py
INSTALLED_APPS = [
    # ...
    "wagtail_visual_editor",
]

There is nothing to add to urls.py: the app registers its admin URLs through Wagtail's register_admin_urls hook.

Frontend

npm install wagtail-visual-editor-bridge

See Frontend integration below.

Usage

1. Add the visual editor panel

Use VisualStreamPanel instead of FieldPanel for the StreamField. Give it its own tab: it uses the full width and height of the screen.

from wagtail.admin.panels import FieldPanel, ObjectList, TabbedInterface
from wagtail.fields import StreamField
from wagtail.models import Page
from wagtail_headless_preview.models import HeadlessMixin
from wagtail_visual_editor.panels import VisualStreamPanel


class BasePage(HeadlessMixin, Page):
    body = StreamField([...])

    content_panels = Page.content_panels  # title and other fields

    edit_handler = TabbedInterface([
        ObjectList(content_panels, heading="Content"),
        ObjectList([VisualStreamPanel("body")], heading="Visual editor"),
        ObjectList(Page.promote_panels, heading="Promote"),
        ObjectList(Page.settings_panels, heading="Settings"),
    ])

Put the panel on the generic page your other page types inherit from, and they all get the visual editor. Subclasses can still change the blocks of the StreamField.

It works on new pages too. Note that Wagtail 7.4's autosave creates the page as soon as the editor changes something; the editor follows that switch from "add" to "edit" by itself.

2. Social share previews (optional)

from wagtail.admin.panels import FieldPanel, MultiFieldPanel
from wagtail_visual_editor.panels import SocialCardPreviewPanel


class BasePage(HeadlessMixin, Page):
    og_image = models.ForeignKey("wagtailimages.Image", null=True, blank=True, on_delete=models.SET_NULL, related_name="+")

    promote_panels = Page.promote_panels + [
        MultiFieldPanel([FieldPanel("og_image"), SocialCardPreviewPanel()], heading="Sharing"),
    ]

    def get_social_preview_defaults(self, site):
        # Optional: the image used when og_image is empty (for example from a site setting)
        return {"image_url": "https://example.com/default-share.jpg"}

The previews update live from the form: seo_title (falls back to title), search_description, slug and og_image. The chosen image is shown with the crop that gets shared, fill-1200x630 by default.

3. Block previews in the block picker (optional)

When the editor adds a block, the picker shows each block type rendered by your frontend. To make that possible, the app replaces Wagtail's block preview view: it stores the block's preview value in Django's cache under a random token and redirects to <frontend>/preview/block?token=....

Preview values come from Meta.preview_value, or are generated from the block's structure with SamplePreviewMixin:

from wagtail import blocks
from wagtail_visual_editor.sample_values import SamplePreviewMixin


class HeroBlock(SamplePreviewMixin, blocks.StructBlock):
    title = blocks.CharBlock()
    image = ImageChooserBlock()
    # no preview_value needed: a title and the first image in the library are used

Your backend serves the stored value to the frontend, serialized exactly like a block of a page:

from wagtail_visual_editor.views import get_block_preview

def block_preview_api(request, token):
    data = get_block_preview(token)  # {"type": "hero", "value": {...}} (raw StreamField format) or None
    ...

Tokens live in Django's cache for 10 minutes. In production with more than one process, configure a shared cache (Redis, Memcached or the database cache); the default local-memory cache is per process.

4. Hide blocks without deleting them (optional)

Root blocks that have a field named hidden get an eye toggle in the tree index:

class BaseBlock(blocks.StructBlock):
    hidden = blocks.BooleanBlock(required=False, help_text="Keep the block but don't show it on the site")

The editor only toggles the value. Skipping hidden blocks when you serialize the page for the API is up to your project.

5. When the frontend is not there

  • No frontend yet (a new project): set WAGTAIL_VISUAL_EDITOR_ENABLED = False, for example from an environment variable. Pages are edited with Wagtail's standard StreamField, with no errors in the admin. Turn it on once the preview routes exist.
  • Frontend down or unreachable: if the preview doesn't answer (the bridge never says ready), the editor falls back to Wagtail's standard StreamField after a few seconds, with a notice and a Retry button. Nothing is lost: it is the same field that gets saved.

6. Copy and paste blocks between pages

Each root block in the tree index has a Copy block button, next to the eye. Then, on any page with the visual editor, the block picker shows Paste copied block above the block types. The pasted block gets new ids, nested blocks included, like Duplicate.

The option is disabled when the destination StreamField doesn't have that block type or its max_num / block_counts are reached. If the block type changed its fields since it was copied and the copy no longer fits, the editor says so instead of pasting.

7. Switch to the standard StreamField

Show StreamField, at the right of the preview bar, shows Wagtail's standard StreamField instead of the visual editor, for example to reorder many blocks at once. Back to visual editor, above it, redraws the tree and reloads the preview with the changes. A block open in the edit dialog is closed without applying it.

The choice isn't remembered: pages always open in the visual editor. The button only exists while the visual editor is on (WAGTAIL_VISUAL_EDITOR_ENABLED).

Frontend integration

The frontend needs two routes. Both must accept being framed by the Wagtail admin.

Page preview: /preview

The route your headless preview already has. When it renders the StreamField for a preview, wrap each root block with its id, and start the bridge with the origin of the Wagtail admin:

<div data-block-id="{block.id}" style="display: contents">
  <!-- your block component -->
</div>

display: contents means the wrapper has no box of its own, so it doesn't change the layout. Only add the wrappers in the preview, not on the public site.

Next.js (App Router):

"use client";
import { useEffect } from "react";
import { startVisualEditBridge } from "wagtail-visual-editor-bridge";

export function VisualEditBridge({ adminOrigin }: { adminOrigin: string }) {
	useEffect(() => startVisualEditBridge(adminOrigin), [adminOrigin]);
	return null;
}

Astro:

---
const { adminOrigin } = Astro.props;
---
<div hidden data-admin-origin={adminOrigin}></div>
<script>
	import { startVisualEditBridge } from "wagtail-visual-editor-bridge";

	const origin = document.querySelector<HTMLElement>("[data-admin-origin]")?.dataset.adminOrigin;
	if (origin) startVisualEditBridge(origin);
</script>

Colors of the action bar

The bridge's buttons and outline read CSS variables, so they can follow your site's colors. Define them in the CSS of /preview; without them, the bar looks as always.

Variable Default Used for
--wve-primary #006df9 Background of Edit, Move up/down and Duplicate
--wve-on-primary #fff Text and icon of those buttons
--wve-danger #d1291e Background of Delete
--wve-on-danger #fff Text and icon of Delete
--wve-outline --wve-primary Outline around the hovered block
--wve-insert --wve-primary Background of the "+" buttons
--wve-on-insert --wve-on-primary Icon of the "+" buttons
/* CSS of your /preview route */
:root {
	--wve-primary: #0f766e;
	--wve-danger: #b91c1c;
}

Buttons darken a little on hover and focus, whatever their color, so there are no hover variables.

Block preview: /preview/block?token=...

Fetch the block from your backend (see get_block_preview above) and render it alone, without the site header and footer.

Incomplete blocks

Wagtail doesn't enforce required fields in drafts and previews. A block that was just added is usually empty, so the preview must tolerate missing values. A good pattern is to validate each block on its own and replace the ones that fail with a placeholder ("Incomplete block, click Edit"), instead of failing the whole page.

Message protocol

The admin and the preview talk through postMessage. Every message carries source: "wagtail-visual-editor", and each side checks the other's origin: the admin accepts only the Site's root_url, the bridge accepts only the admin origin you pass to it.

Direction action Payload
preview → admin ready —
preview → admin edit, moveUp, moveDown, duplicate, delete, insertBefore, insertAfter id of the block
admin → preview scrollTo id of the block
admin → preview labels labels: {action: [text, title]} for the action bar, in the admin user's language (icon-only buttons use just the title)

Translations

The UI follows each admin user's language, like the rest of Wagtail. The preview action bar too: the admin sends its translated labels to the bridge when the preview loads (the bridge falls back to English).

Available languages: English and Spanish. Strings that Wagtail already has, such as "Cancel", "Edit", "Delete" or the preview sizes, use Wagtail's own translations, so they are translated in every language Wagtail supports.

To add a language, from the wagtail_visual_editor directory:

django-admin makemessages -l fr --no-location
# translate locale/fr/LC_MESSAGES/django.po
django-admin compilemessages

Settings

Setting Default Description
WAGTAIL_VISUAL_EDITOR_ENABLED True False turns VisualStreamPanel into a plain FieldPanel and restores Wagtail's own block previews. Useful while the frontend doesn't exist yet
WAGTAIL_VISUAL_EDITOR_SOCIAL_RENDITION "fill-1200x630" Image crop shown in the social share previews

Limitations

  • One VisualStreamPanel per page.
  • The block picker previews replace Wagtail's block previews for every StreamField of the admin, not only the visual editor.
  • Hidden blocks don't appear in the preview, so they can only be edited from the tree index.
  • Copy and paste uses the browser's localStorage: it works between pages and tabs of the same admin in the same browser, not between environments (from staging to production) or browsers.

Development

python -m unittest discover -s tests   # Django app
cd bridge && npm install && npm run build   # bridge

Releasing: bump the version in pyproject.toml, wagtail_visual_editor/__init__.py and bridge/package.json, then push a tag X.Y.Z (no v prefix). The release workflow checks that the three versions match the tag and publishes both packages.

License

MIT

Metadata

Release files for wagtail-visual-editor 0.1.6

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for wagtail-visual-editor 0.1.6
File Size Uploaded Provenance
wagtail_visual_editor-0.1.6.tar.gz 31.5 kB With provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for wagtail-visual-editor 0.1.6
File Interpreter ABI Platform
wagtail_visual_editor-0.1.6-py3-none-any.whl Python 3 none any Details

Total release size: 69.1 kB

Release files / wagtail_visual_editor-0.1.6.tar.gz

Download URL wagtail_visual_editor-0.1.6.tar.gz
Size 31.5 kB
Tags Source
SHA-256 checksum
How to use checksums
1a091f1688c149402e6fd1eeb3fdcfac23a84b0348e8589b120f3e7029382f3b
BLAKE2b-256 checksum
How to use checksums
4d0e57d2b5686578d7e2c3d19f17854c5e636d95a30fd1f7800da48d3ea46c80
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 22, 2026.

Transparency log

Release files / wagtail_visual_editor-0.1.6-py3-none-any.whl

Download URL wagtail_visual_editor-0.1.6-py3-none-any.whl
Size 37.7 kB
Tags Python 3
SHA-256 checksum
How to use checksums
60a4f2759e6d67da526f00a23ef1540a56e00391b2d96856826cbb3463255f3c
BLAKE2b-256 checksum
How to use checksums
4f5fbb73ee44df75b577dfeb28cefa0a807bcc63ea6c93a1f42c51aa0ab4e065
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 22, 2026.

Transparency log

Security for wagtail-visual-editor 0.1.6

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

Full provenance

All 2 files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for wagtail_visual_editor-0.1.6.tar.gz

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 22, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page