wagtail-visual-editor 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- wagtail_visual_editor-0.1.0/.gitignore +5 -0
- wagtail_visual_editor-0.1.0/LICENSE +21 -0
- wagtail_visual_editor-0.1.0/PKG-INFO +259 -0
- wagtail_visual_editor-0.1.0/README.md +240 -0
- wagtail_visual_editor-0.1.0/pyproject.toml +32 -0
- wagtail_visual_editor-0.1.0/tests/test_checks.py +18 -0
- wagtail_visual_editor-0.1.0/wagtail_visual_editor/__init__.py +1 -0
- wagtail_visual_editor-0.1.0/wagtail_visual_editor/apps.py +10 -0
- wagtail_visual_editor-0.1.0/wagtail_visual_editor/checks.py +27 -0
- wagtail_visual_editor-0.1.0/wagtail_visual_editor/panels.py +62 -0
- wagtail_visual_editor-0.1.0/wagtail_visual_editor/sample_values.py +84 -0
- wagtail_visual_editor-0.1.0/wagtail_visual_editor/static/wagtail_visual_editor/social_cards.css +329 -0
- wagtail_visual_editor-0.1.0/wagtail_visual_editor/static/wagtail_visual_editor/social_cards.js +67 -0
- wagtail_visual_editor-0.1.0/wagtail_visual_editor/static/wagtail_visual_editor/visual_editor.css +348 -0
- wagtail_visual_editor-0.1.0/wagtail_visual_editor/static/wagtail_visual_editor/visual_editor.js +460 -0
- wagtail_visual_editor-0.1.0/wagtail_visual_editor/templates/wagtail_visual_editor/social_card_preview_panel.html +90 -0
- wagtail_visual_editor-0.1.0/wagtail_visual_editor/templates/wagtail_visual_editor/visual_stream_panel.html +81 -0
- wagtail_visual_editor-0.1.0/wagtail_visual_editor/views.py +44 -0
- wagtail_visual_editor-0.1.0/wagtail_visual_editor/wagtail_hooks.py +18 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Eric Kersten
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: wagtail-visual-editor
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Visual editor for headless Wagtail: edit a StreamField on top of your frontend's live preview.
|
|
5
|
+
Author-email: Eric Kersten <eric.kersten@egodesign.io>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Keywords: headless,preview,streamfield,visual editor,wagtail
|
|
9
|
+
Classifier: Development Status :: 3 - Alpha
|
|
10
|
+
Classifier: Environment :: Web Environment
|
|
11
|
+
Classifier: Framework :: Django
|
|
12
|
+
Classifier: Framework :: Wagtail
|
|
13
|
+
Classifier: Framework :: Wagtail :: 7
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Requires-Python: >=3.10
|
|
17
|
+
Requires-Dist: wagtail<7.5,>=7.4
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
|
|
20
|
+
# wagtail-visual-editor
|
|
21
|
+
|
|
22
|
+
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:
|
|
23
|
+
|
|
24
|
+
- hover a block in the preview to get **Edit, ↑, ↓, Duplicate, Delete and "+ Add below"**;
|
|
25
|
+
- each block is edited in a **modal** that uses Wagtail's own block form (choosers, rich text, `TabbedStructBlock`, validation, everything);
|
|
26
|
+
- a collapsible **tree index** of the page, with an eye toggle to **hide a block** from the live site without deleting it;
|
|
27
|
+
- **mobile / tablet / desktop** widths;
|
|
28
|
+
- the **block picker previews each block** rendered by your frontend;
|
|
29
|
+
- live **social share previews** (Google, WhatsApp, Facebook, X, LinkedIn, Slack, Pinterest) for the promote tab.
|
|
30
|
+
|
|
31
|
+
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.
|
|
32
|
+
|
|
33
|
+
The package has two parts, released together with the same version number:
|
|
34
|
+
|
|
35
|
+
| Part | Package | Where it runs |
|
|
36
|
+
| --- | --- | --- |
|
|
37
|
+
| Django app | [`wagtail-visual-editor`](https://pypi.org/project/wagtail-visual-editor/) (PyPI) | Wagtail admin |
|
|
38
|
+
| Preview bridge | [`wagtail-visual-editor-bridge`](https://www.npmjs.com/package/wagtail-visual-editor-bridge) (npm) | Your frontend's preview route |
|
|
39
|
+
|
|
40
|
+
> The admin UI and the preview action bar are currently in Spanish. Translations are planned.
|
|
41
|
+
|
|
42
|
+
## Compatibility
|
|
43
|
+
|
|
44
|
+
| wagtail-visual-editor | Wagtail | Python | Django |
|
|
45
|
+
| --- | --- | --- | --- |
|
|
46
|
+
| 0.1.x | 7.4 | 3.10+ | as required by Wagtail |
|
|
47
|
+
|
|
48
|
+
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`.
|
|
49
|
+
|
|
50
|
+
## Requirements
|
|
51
|
+
|
|
52
|
+
- A **headless preview** that already works: Wagtail's preview must redirect to your frontend, for example with [wagtail-headless-preview](https://github.com/torchbox/wagtail-headless-preview).
|
|
53
|
+
- 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.
|
|
54
|
+
- 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`.
|
|
55
|
+
|
|
56
|
+
## Installation
|
|
57
|
+
|
|
58
|
+
### Backend
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
pip install wagtail-visual-editor
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
```python
|
|
65
|
+
# settings.py
|
|
66
|
+
INSTALLED_APPS = [
|
|
67
|
+
# ...
|
|
68
|
+
"wagtail_visual_editor",
|
|
69
|
+
]
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
There is nothing to add to `urls.py`: the app registers its admin URLs through Wagtail's `register_admin_urls` hook.
|
|
73
|
+
|
|
74
|
+
### Frontend
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
npm install wagtail-visual-editor-bridge
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
See [Frontend integration](#frontend-integration) below.
|
|
81
|
+
|
|
82
|
+
## Usage
|
|
83
|
+
|
|
84
|
+
### 1. Add the visual editor panel
|
|
85
|
+
|
|
86
|
+
Use `VisualStreamPanel` instead of `FieldPanel` for the StreamField. Give it its own tab: it uses the full width and height of the screen.
|
|
87
|
+
|
|
88
|
+
```python
|
|
89
|
+
from wagtail.admin.panels import FieldPanel, ObjectList, TabbedInterface
|
|
90
|
+
from wagtail.fields import StreamField
|
|
91
|
+
from wagtail.models import Page
|
|
92
|
+
from wagtail_headless_preview.models import HeadlessMixin
|
|
93
|
+
from wagtail_visual_editor.panels import VisualStreamPanel
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class BasePage(HeadlessMixin, Page):
|
|
97
|
+
body = StreamField([...])
|
|
98
|
+
|
|
99
|
+
content_panels = Page.content_panels # title and other fields
|
|
100
|
+
|
|
101
|
+
edit_handler = TabbedInterface([
|
|
102
|
+
ObjectList(content_panels, heading="Content"),
|
|
103
|
+
ObjectList([VisualStreamPanel("body")], heading="Visual editor"),
|
|
104
|
+
ObjectList(Page.promote_panels, heading="Promote"),
|
|
105
|
+
ObjectList(Page.settings_panels, heading="Settings"),
|
|
106
|
+
])
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
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.
|
|
110
|
+
|
|
111
|
+
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.
|
|
112
|
+
|
|
113
|
+
### 2. Social share previews (optional)
|
|
114
|
+
|
|
115
|
+
```python
|
|
116
|
+
from wagtail.admin.panels import FieldPanel, MultiFieldPanel
|
|
117
|
+
from wagtail_visual_editor.panels import SocialCardPreviewPanel
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
class BasePage(HeadlessMixin, Page):
|
|
121
|
+
og_image = models.ForeignKey("wagtailimages.Image", null=True, blank=True, on_delete=models.SET_NULL, related_name="+")
|
|
122
|
+
|
|
123
|
+
promote_panels = Page.promote_panels + [
|
|
124
|
+
MultiFieldPanel([FieldPanel("og_image"), SocialCardPreviewPanel()], heading="Sharing"),
|
|
125
|
+
]
|
|
126
|
+
|
|
127
|
+
def get_social_preview_defaults(self, site):
|
|
128
|
+
# Optional: the image used when og_image is empty (for example from a site setting)
|
|
129
|
+
return {"image_url": "https://example.com/default-share.jpg"}
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
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.
|
|
133
|
+
|
|
134
|
+
### 3. Block previews in the block picker (optional)
|
|
135
|
+
|
|
136
|
+
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=...`.
|
|
137
|
+
|
|
138
|
+
Preview values come from `Meta.preview_value`, or are generated from the block's structure with `SamplePreviewMixin`:
|
|
139
|
+
|
|
140
|
+
```python
|
|
141
|
+
from wagtail import blocks
|
|
142
|
+
from wagtail_visual_editor.sample_values import SamplePreviewMixin
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
class HeroBlock(SamplePreviewMixin, blocks.StructBlock):
|
|
146
|
+
title = blocks.CharBlock()
|
|
147
|
+
image = ImageChooserBlock()
|
|
148
|
+
# no preview_value needed: a title and the first image in the library are used
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
Your backend serves the stored value to the frontend, serialized exactly like a block of a page:
|
|
152
|
+
|
|
153
|
+
```python
|
|
154
|
+
from wagtail_visual_editor.views import get_block_preview
|
|
155
|
+
|
|
156
|
+
def block_preview_api(request, token):
|
|
157
|
+
data = get_block_preview(token) # {"type": "hero", "value": {...}} (raw StreamField format) or None
|
|
158
|
+
...
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
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.
|
|
162
|
+
|
|
163
|
+
### 4. Hide blocks without deleting them (optional)
|
|
164
|
+
|
|
165
|
+
Root blocks that have a field named `hidden` get an eye toggle in the tree index:
|
|
166
|
+
|
|
167
|
+
```python
|
|
168
|
+
class BaseBlock(blocks.StructBlock):
|
|
169
|
+
hidden = blocks.BooleanBlock(required=False, help_text="Keep the block but don't show it on the site")
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
The editor only toggles the value. Skipping hidden blocks when you serialize the page for the API is up to your project.
|
|
173
|
+
|
|
174
|
+
## Frontend integration
|
|
175
|
+
|
|
176
|
+
The frontend needs two routes. Both must accept being framed by the Wagtail admin.
|
|
177
|
+
|
|
178
|
+
### Page preview: `/preview`
|
|
179
|
+
|
|
180
|
+
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:
|
|
181
|
+
|
|
182
|
+
```html
|
|
183
|
+
<div data-block-id="{block.id}" style="display: contents">
|
|
184
|
+
<!-- your block component -->
|
|
185
|
+
</div>
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
`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.
|
|
189
|
+
|
|
190
|
+
Next.js (App Router):
|
|
191
|
+
|
|
192
|
+
```tsx
|
|
193
|
+
"use client";
|
|
194
|
+
import { useEffect } from "react";
|
|
195
|
+
import { startVisualEditBridge } from "wagtail-visual-editor-bridge";
|
|
196
|
+
|
|
197
|
+
export function VisualEditBridge({ adminOrigin }: { adminOrigin: string }) {
|
|
198
|
+
useEffect(() => startVisualEditBridge(adminOrigin), [adminOrigin]);
|
|
199
|
+
return null;
|
|
200
|
+
}
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
Astro:
|
|
204
|
+
|
|
205
|
+
```astro
|
|
206
|
+
---
|
|
207
|
+
const { adminOrigin } = Astro.props;
|
|
208
|
+
---
|
|
209
|
+
<div hidden data-admin-origin={adminOrigin}></div>
|
|
210
|
+
<script>
|
|
211
|
+
import { startVisualEditBridge } from "wagtail-visual-editor-bridge";
|
|
212
|
+
|
|
213
|
+
const origin = document.querySelector<HTMLElement>("[data-admin-origin]")?.dataset.adminOrigin;
|
|
214
|
+
if (origin) startVisualEditBridge(origin);
|
|
215
|
+
</script>
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
### Block preview: `/preview/block?token=...`
|
|
219
|
+
|
|
220
|
+
Fetch the block from your backend (see `get_block_preview` above) and render it alone, without the site header and footer.
|
|
221
|
+
|
|
222
|
+
### Incomplete blocks
|
|
223
|
+
|
|
224
|
+
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.
|
|
225
|
+
|
|
226
|
+
### Message protocol
|
|
227
|
+
|
|
228
|
+
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.
|
|
229
|
+
|
|
230
|
+
| Direction | `action` | Payload |
|
|
231
|
+
| --- | --- | --- |
|
|
232
|
+
| preview → admin | `ready` | — |
|
|
233
|
+
| preview → admin | `edit`, `moveUp`, `moveDown`, `duplicate`, `delete`, `insertAfter` | `id` of the block |
|
|
234
|
+
| admin → preview | `scrollTo` | `id` of the block |
|
|
235
|
+
|
|
236
|
+
## Settings
|
|
237
|
+
|
|
238
|
+
| Setting | Default | Description |
|
|
239
|
+
| --- | --- | --- |
|
|
240
|
+
| `WAGTAIL_VISUAL_EDITOR_SOCIAL_RENDITION` | `"fill-1200x630"` | Image crop shown in the social share previews |
|
|
241
|
+
|
|
242
|
+
## Limitations
|
|
243
|
+
|
|
244
|
+
- One `VisualStreamPanel` per page.
|
|
245
|
+
- The block picker previews replace Wagtail's block previews for every StreamField of the admin, not only the visual editor.
|
|
246
|
+
- Hidden blocks don't appear in the preview, so they can only be edited from the tree index.
|
|
247
|
+
|
|
248
|
+
## Development
|
|
249
|
+
|
|
250
|
+
```bash
|
|
251
|
+
python -m unittest discover -s tests # Django app
|
|
252
|
+
cd bridge && npm install && npm run build # bridge
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
Releasing: bump the version in `pyproject.toml`, `wagtail_visual_editor/__init__.py` and `bridge/package.json`, then push a tag `vX.Y.Z`. The release workflow checks that the three versions match the tag and publishes both packages.
|
|
256
|
+
|
|
257
|
+
## License
|
|
258
|
+
|
|
259
|
+
MIT
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
# wagtail-visual-editor
|
|
2
|
+
|
|
3
|
+
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:
|
|
4
|
+
|
|
5
|
+
- hover a block in the preview to get **Edit, ↑, ↓, Duplicate, Delete and "+ Add below"**;
|
|
6
|
+
- each block is edited in a **modal** that uses Wagtail's own block form (choosers, rich text, `TabbedStructBlock`, validation, everything);
|
|
7
|
+
- a collapsible **tree index** of the page, with an eye toggle to **hide a block** from the live site without deleting it;
|
|
8
|
+
- **mobile / tablet / desktop** widths;
|
|
9
|
+
- the **block picker previews each block** rendered by your frontend;
|
|
10
|
+
- live **social share previews** (Google, WhatsApp, Facebook, X, LinkedIn, Slack, Pinterest) for the promote tab.
|
|
11
|
+
|
|
12
|
+
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.
|
|
13
|
+
|
|
14
|
+
The package has two parts, released together with the same version number:
|
|
15
|
+
|
|
16
|
+
| Part | Package | Where it runs |
|
|
17
|
+
| --- | --- | --- |
|
|
18
|
+
| Django app | [`wagtail-visual-editor`](https://pypi.org/project/wagtail-visual-editor/) (PyPI) | Wagtail admin |
|
|
19
|
+
| Preview bridge | [`wagtail-visual-editor-bridge`](https://www.npmjs.com/package/wagtail-visual-editor-bridge) (npm) | Your frontend's preview route |
|
|
20
|
+
|
|
21
|
+
> The admin UI and the preview action bar are currently in Spanish. Translations are planned.
|
|
22
|
+
|
|
23
|
+
## Compatibility
|
|
24
|
+
|
|
25
|
+
| wagtail-visual-editor | Wagtail | Python | Django |
|
|
26
|
+
| --- | --- | --- | --- |
|
|
27
|
+
| 0.1.x | 7.4 | 3.10+ | as required by Wagtail |
|
|
28
|
+
|
|
29
|
+
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`.
|
|
30
|
+
|
|
31
|
+
## Requirements
|
|
32
|
+
|
|
33
|
+
- A **headless preview** that already works: Wagtail's preview must redirect to your frontend, for example with [wagtail-headless-preview](https://github.com/torchbox/wagtail-headless-preview).
|
|
34
|
+
- 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.
|
|
35
|
+
- 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`.
|
|
36
|
+
|
|
37
|
+
## Installation
|
|
38
|
+
|
|
39
|
+
### Backend
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
pip install wagtail-visual-editor
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
```python
|
|
46
|
+
# settings.py
|
|
47
|
+
INSTALLED_APPS = [
|
|
48
|
+
# ...
|
|
49
|
+
"wagtail_visual_editor",
|
|
50
|
+
]
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
There is nothing to add to `urls.py`: the app registers its admin URLs through Wagtail's `register_admin_urls` hook.
|
|
54
|
+
|
|
55
|
+
### Frontend
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
npm install wagtail-visual-editor-bridge
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
See [Frontend integration](#frontend-integration) below.
|
|
62
|
+
|
|
63
|
+
## Usage
|
|
64
|
+
|
|
65
|
+
### 1. Add the visual editor panel
|
|
66
|
+
|
|
67
|
+
Use `VisualStreamPanel` instead of `FieldPanel` for the StreamField. Give it its own tab: it uses the full width and height of the screen.
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
from wagtail.admin.panels import FieldPanel, ObjectList, TabbedInterface
|
|
71
|
+
from wagtail.fields import StreamField
|
|
72
|
+
from wagtail.models import Page
|
|
73
|
+
from wagtail_headless_preview.models import HeadlessMixin
|
|
74
|
+
from wagtail_visual_editor.panels import VisualStreamPanel
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class BasePage(HeadlessMixin, Page):
|
|
78
|
+
body = StreamField([...])
|
|
79
|
+
|
|
80
|
+
content_panels = Page.content_panels # title and other fields
|
|
81
|
+
|
|
82
|
+
edit_handler = TabbedInterface([
|
|
83
|
+
ObjectList(content_panels, heading="Content"),
|
|
84
|
+
ObjectList([VisualStreamPanel("body")], heading="Visual editor"),
|
|
85
|
+
ObjectList(Page.promote_panels, heading="Promote"),
|
|
86
|
+
ObjectList(Page.settings_panels, heading="Settings"),
|
|
87
|
+
])
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
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.
|
|
91
|
+
|
|
92
|
+
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.
|
|
93
|
+
|
|
94
|
+
### 2. Social share previews (optional)
|
|
95
|
+
|
|
96
|
+
```python
|
|
97
|
+
from wagtail.admin.panels import FieldPanel, MultiFieldPanel
|
|
98
|
+
from wagtail_visual_editor.panels import SocialCardPreviewPanel
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
class BasePage(HeadlessMixin, Page):
|
|
102
|
+
og_image = models.ForeignKey("wagtailimages.Image", null=True, blank=True, on_delete=models.SET_NULL, related_name="+")
|
|
103
|
+
|
|
104
|
+
promote_panels = Page.promote_panels + [
|
|
105
|
+
MultiFieldPanel([FieldPanel("og_image"), SocialCardPreviewPanel()], heading="Sharing"),
|
|
106
|
+
]
|
|
107
|
+
|
|
108
|
+
def get_social_preview_defaults(self, site):
|
|
109
|
+
# Optional: the image used when og_image is empty (for example from a site setting)
|
|
110
|
+
return {"image_url": "https://example.com/default-share.jpg"}
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
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.
|
|
114
|
+
|
|
115
|
+
### 3. Block previews in the block picker (optional)
|
|
116
|
+
|
|
117
|
+
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=...`.
|
|
118
|
+
|
|
119
|
+
Preview values come from `Meta.preview_value`, or are generated from the block's structure with `SamplePreviewMixin`:
|
|
120
|
+
|
|
121
|
+
```python
|
|
122
|
+
from wagtail import blocks
|
|
123
|
+
from wagtail_visual_editor.sample_values import SamplePreviewMixin
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
class HeroBlock(SamplePreviewMixin, blocks.StructBlock):
|
|
127
|
+
title = blocks.CharBlock()
|
|
128
|
+
image = ImageChooserBlock()
|
|
129
|
+
# no preview_value needed: a title and the first image in the library are used
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Your backend serves the stored value to the frontend, serialized exactly like a block of a page:
|
|
133
|
+
|
|
134
|
+
```python
|
|
135
|
+
from wagtail_visual_editor.views import get_block_preview
|
|
136
|
+
|
|
137
|
+
def block_preview_api(request, token):
|
|
138
|
+
data = get_block_preview(token) # {"type": "hero", "value": {...}} (raw StreamField format) or None
|
|
139
|
+
...
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
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.
|
|
143
|
+
|
|
144
|
+
### 4. Hide blocks without deleting them (optional)
|
|
145
|
+
|
|
146
|
+
Root blocks that have a field named `hidden` get an eye toggle in the tree index:
|
|
147
|
+
|
|
148
|
+
```python
|
|
149
|
+
class BaseBlock(blocks.StructBlock):
|
|
150
|
+
hidden = blocks.BooleanBlock(required=False, help_text="Keep the block but don't show it on the site")
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
The editor only toggles the value. Skipping hidden blocks when you serialize the page for the API is up to your project.
|
|
154
|
+
|
|
155
|
+
## Frontend integration
|
|
156
|
+
|
|
157
|
+
The frontend needs two routes. Both must accept being framed by the Wagtail admin.
|
|
158
|
+
|
|
159
|
+
### Page preview: `/preview`
|
|
160
|
+
|
|
161
|
+
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:
|
|
162
|
+
|
|
163
|
+
```html
|
|
164
|
+
<div data-block-id="{block.id}" style="display: contents">
|
|
165
|
+
<!-- your block component -->
|
|
166
|
+
</div>
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
`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.
|
|
170
|
+
|
|
171
|
+
Next.js (App Router):
|
|
172
|
+
|
|
173
|
+
```tsx
|
|
174
|
+
"use client";
|
|
175
|
+
import { useEffect } from "react";
|
|
176
|
+
import { startVisualEditBridge } from "wagtail-visual-editor-bridge";
|
|
177
|
+
|
|
178
|
+
export function VisualEditBridge({ adminOrigin }: { adminOrigin: string }) {
|
|
179
|
+
useEffect(() => startVisualEditBridge(adminOrigin), [adminOrigin]);
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
Astro:
|
|
185
|
+
|
|
186
|
+
```astro
|
|
187
|
+
---
|
|
188
|
+
const { adminOrigin } = Astro.props;
|
|
189
|
+
---
|
|
190
|
+
<div hidden data-admin-origin={adminOrigin}></div>
|
|
191
|
+
<script>
|
|
192
|
+
import { startVisualEditBridge } from "wagtail-visual-editor-bridge";
|
|
193
|
+
|
|
194
|
+
const origin = document.querySelector<HTMLElement>("[data-admin-origin]")?.dataset.adminOrigin;
|
|
195
|
+
if (origin) startVisualEditBridge(origin);
|
|
196
|
+
</script>
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
### Block preview: `/preview/block?token=...`
|
|
200
|
+
|
|
201
|
+
Fetch the block from your backend (see `get_block_preview` above) and render it alone, without the site header and footer.
|
|
202
|
+
|
|
203
|
+
### Incomplete blocks
|
|
204
|
+
|
|
205
|
+
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.
|
|
206
|
+
|
|
207
|
+
### Message protocol
|
|
208
|
+
|
|
209
|
+
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.
|
|
210
|
+
|
|
211
|
+
| Direction | `action` | Payload |
|
|
212
|
+
| --- | --- | --- |
|
|
213
|
+
| preview → admin | `ready` | — |
|
|
214
|
+
| preview → admin | `edit`, `moveUp`, `moveDown`, `duplicate`, `delete`, `insertAfter` | `id` of the block |
|
|
215
|
+
| admin → preview | `scrollTo` | `id` of the block |
|
|
216
|
+
|
|
217
|
+
## Settings
|
|
218
|
+
|
|
219
|
+
| Setting | Default | Description |
|
|
220
|
+
| --- | --- | --- |
|
|
221
|
+
| `WAGTAIL_VISUAL_EDITOR_SOCIAL_RENDITION` | `"fill-1200x630"` | Image crop shown in the social share previews |
|
|
222
|
+
|
|
223
|
+
## Limitations
|
|
224
|
+
|
|
225
|
+
- One `VisualStreamPanel` per page.
|
|
226
|
+
- The block picker previews replace Wagtail's block previews for every StreamField of the admin, not only the visual editor.
|
|
227
|
+
- Hidden blocks don't appear in the preview, so they can only be edited from the tree index.
|
|
228
|
+
|
|
229
|
+
## Development
|
|
230
|
+
|
|
231
|
+
```bash
|
|
232
|
+
python -m unittest discover -s tests # Django app
|
|
233
|
+
cd bridge && npm install && npm run build # bridge
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
Releasing: bump the version in `pyproject.toml`, `wagtail_visual_editor/__init__.py` and `bridge/package.json`, then push a tag `vX.Y.Z`. The release workflow checks that the three versions match the tag and publishes both packages.
|
|
237
|
+
|
|
238
|
+
## License
|
|
239
|
+
|
|
240
|
+
MIT
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "wagtail-visual-editor"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Visual editor for headless Wagtail: edit a StreamField on top of your frontend's live preview."
|
|
5
|
+
authors = [{ name = "Eric Kersten", email = "eric.kersten@egodesign.io" }]
|
|
6
|
+
readme = "README.md"
|
|
7
|
+
license = "MIT"
|
|
8
|
+
license-files = ["LICENSE"]
|
|
9
|
+
requires-python = ">=3.10"
|
|
10
|
+
keywords = ["wagtail", "headless", "preview", "visual editor", "streamfield"]
|
|
11
|
+
classifiers = [
|
|
12
|
+
"Development Status :: 3 - Alpha",
|
|
13
|
+
"Environment :: Web Environment",
|
|
14
|
+
"Framework :: Django",
|
|
15
|
+
"Framework :: Wagtail",
|
|
16
|
+
"Framework :: Wagtail :: 7",
|
|
17
|
+
"Intended Audience :: Developers",
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
]
|
|
20
|
+
# Capped: the app relies on Wagtail's internal StreamField JS API. Only raise the cap after testing
|
|
21
|
+
# each new minor (see "Compatibility" in the README and WAGTAIL_TESTED in checks.py).
|
|
22
|
+
dependencies = ["wagtail>=7.4,<7.5"]
|
|
23
|
+
|
|
24
|
+
[tool.hatch.build.targets.sdist]
|
|
25
|
+
exclude = ["bridge", ".github"]
|
|
26
|
+
|
|
27
|
+
[build-system]
|
|
28
|
+
requires = ["hatchling"]
|
|
29
|
+
build-backend = "hatchling.build"
|
|
30
|
+
|
|
31
|
+
[tool.hatch.build.targets.wheel]
|
|
32
|
+
packages = ["wagtail_visual_editor"]
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import unittest
|
|
2
|
+
|
|
3
|
+
from wagtail_visual_editor.checks import version_warnings
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class VersionWarningsTest(unittest.TestCase):
|
|
7
|
+
def test_tested_versions_pass(self):
|
|
8
|
+
self.assertEqual(version_warnings((7, 4, 0, "final", 0)), [])
|
|
9
|
+
self.assertEqual(version_warnings((7, 4, 3, "final", 0)), [])
|
|
10
|
+
|
|
11
|
+
def test_untested_versions_warn(self):
|
|
12
|
+
for version in [(7, 3, 0), (7, 5, 0), (8, 0, 0)]:
|
|
13
|
+
[warning] = version_warnings(version)
|
|
14
|
+
self.assertEqual(warning.id, "wagtail_visual_editor.W001")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
if __name__ == "__main__":
|
|
18
|
+
unittest.main()
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
from django.apps import AppConfig
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class WagtailVisualEditorConfig(AppConfig):
|
|
5
|
+
name = "wagtail_visual_editor"
|
|
6
|
+
label = "wagtail_visual_editor"
|
|
7
|
+
verbose_name = "Wagtail visual editor"
|
|
8
|
+
|
|
9
|
+
def ready(self):
|
|
10
|
+
from . import checks # noqa: F401 (registra el system check)
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
from django.core.checks import Warning, register
|
|
2
|
+
|
|
3
|
+
# Rango de Wagtail probado: [mínima, siguiente minor sin probar). Va en sincronía con pyproject.toml.
|
|
4
|
+
WAGTAIL_TESTED = ((7, 4), (7, 5))
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def version_warnings(version):
|
|
8
|
+
"""Avisos si la versión de Wagtail (tupla) está fuera del rango probado. pip se puede saltear el límite."""
|
|
9
|
+
low, high = WAGTAIL_TESTED
|
|
10
|
+
if low <= tuple(version[:2]) < high:
|
|
11
|
+
return []
|
|
12
|
+
return [
|
|
13
|
+
Warning(
|
|
14
|
+
f"wagtail-visual-editor no está probado con Wagtail {version[0]}.{version[1]} "
|
|
15
|
+
f"(probado: >={low[0]}.{low[1]},<{high[0]}.{high[1]}).",
|
|
16
|
+
hint="El editor usa la API JS interna del StreamField: probalo antes de usarlo o instalá una "
|
|
17
|
+
"versión de wagtail-visual-editor compatible (ver la tabla del README).",
|
|
18
|
+
id="wagtail_visual_editor.W001",
|
|
19
|
+
)
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@register()
|
|
24
|
+
def check_wagtail_version(app_configs, **kwargs):
|
|
25
|
+
from wagtail import VERSION
|
|
26
|
+
|
|
27
|
+
return version_warnings(VERSION)
|