Skip to main content

uncial-wagtail

Wagtail integration for Uncial. It stores an Uncial editor document in an ordinary JSONField, renders the Uncial block editor inside the Wagtail admin, and serves the saved document as a headless JSON payload with a resolved image sidecar.

Because the body is just JSON, there is no custom database column type, no StreamField block registry to migrate, and no server-side rendering coupling: the same document you edit in the admin is the document you fetch over HTTP and render with the Uncial renderer (Svelte, a web component, or your own runtime) on the frontend.

Requirements

  • Python >= 3.11
  • Django >= 5.2
  • Wagtail >= 7.0

Install

uv add uncial-wagtail
# or
pip install uncial-wagtail

Add the app to INSTALLED_APPS:

INSTALLED_APPS = [
    # ...
    "uncial_wagtail",
]

Add an Uncial body to a page

There are two ways to attach the editor. Pick one.

UncialField is a JSONField subclass that renders the Uncial editor through a plain FieldPanel — no special panel required. It defaults to blank=True and a valid empty document, so migrations and fresh instances are safe out of the box.

from wagtail.admin.panels import FieldPanel
from wagtail.models import Page

from uncial_wagtail.fields import UncialField
from uncial_wagtail.schema import UncialEditorConfig


class ArticlePage(Page):
    body = UncialField(
        config=UncialEditorConfig(
            allowed_blocks=["wagtail.image"],
            toolbar_features=["bold", "italic", "link", "heading", "bulletList"],
        )
    )

    content_panels = Page.content_panels + [FieldPanel("body")]

UncialPanel on a plain JSONField

If you'd rather keep the field a stock models.JSONField (for example to reuse an existing column), attach the editor at the panel level instead. UncialPanel swaps in the Uncial widget for that field and lets you set the config per panel.

from django.db import models
from wagtail.models import Page

from uncial_wagtail.panels import UncialPanel
from uncial_wagtail.schema import UncialEditorConfig


class ArticlePage(Page):
    body = models.JSONField(default=dict, blank=True)

    content_panels = Page.content_panels + [
        UncialPanel("body", config=UncialEditorConfig(allowed_blocks=["wagtail.image"]))
    ]

The editor config is presentation-only. Like Wagtail's own RichTextField, it is deliberately excluded from deconstruct()/migrations, so changing toolbar options never generates migration noise.

Configuring the editor: UncialEditorConfig

UncialEditorConfig maps one-to-one onto the Uncial editor options and is serialized into the widget as JSON.

Field Purpose
allowed_blocks Block ids the editor may insert (e.g. "wagtail.image", plus any custom block ids).
allowed_marks Inline mark allowlist ("bold", "italic", "link", "code", …).
toolbar_features Which rich-text toolbar controls to show, in order.
toolbar_extensions Keys into window.uncialWagtail.toolbarExtensions for custom toolbar buttons.
custom_blocks Keys into window.uncialWagtail.customBlocks for app-defined blocks.
image_renditions Rendition specs offered for wagtail.image blocks. Defaults to the project allowlist.
UncialEditorConfig(
    allowed_blocks=["wagtail.image", "callout", "card"],
    allowed_marks=["bold", "italic", "link", "code"],
    toolbar_features=["bold", "italic", "link", "heading", "bulletList", "blockquote"],
    custom_blocks=["callout", "card"],
)

Headless API

Mount the package URLs to expose page bodies and to back the admin image chooser:

# urls.py
from django.urls import include, path

urlpatterns = [
    # ...
    path("api/uncial/", include("uncial_wagtail.urls")),
]

This adds three routes under the uncial_wagtail namespace:

Route Name Purpose
pages/<id>/ page_detail Headless JSON for a live, public page.
images/ image_chooser_fallback JSON image list used when the Wagtail chooser modal is unavailable.
images/<id>/preview/ image_preview Redirect to a rendition URL for admin previews.

page_detail returns the document plus a references sidecar — every wagtail.image block referenced in the body, resolved to a concrete rendition URL and dimensions — so a frontend can render images without a second round trip:

{
  "id": 4,
  "type": "pages.ArticlePage",
  "title": "Hello world",
  "body": { "type": "doc", "content": [/* … */], "version": 1 },
  "references": {
    "wagtail.image:12:width-1200": {
      "kind": "wagtail.image",
      "id": 12,
      "rendition": "width-1200",
      "url": "/media/images/photo.width-1200.jpg",
      "width": 1200,
      "height": 800,
      "alt": "A photo",
      "title": "photo.jpg"
    }
  }
}

Serializing your own responses

page_detail is a convenience view. To serialize inside your own DRF/JSON view — for example a different body field, or a page type with extra fields — use the serializer helpers directly. Renditions are prefetched in a single query, so resolving N image references does not issue N lookups.

from uncial_wagtail.serializers import serialize_page, serialize_uncial_body

serialize_page(page)                       # id/type/title + body + references
serialize_page(page, body_field="content") # non-default field name
serialize_uncial_body(page.body)           # just {"body": ..., "references": ...}

Reference index and image renditions

UncialField.extract_references feeds Wagtail's ReferenceIndex, so images used inside a document show up under Usage for that image and are protected from accidental deletion — the same as images in a RichTextField.

The renditions offered in the admin and used when resolving the sidecar come from an allowlist. Override it project-wide:

# settings.py
UNCIAL_IMAGE_RENDITIONS = [
    "width-800",
    "width-1600",
    "fill-1200x630",
    "original",
]

A reference whose stored rendition is not in the allowlist falls back to width-1200 (or the first allowlisted spec) rather than failing the response.

Custom blocks and toolbar extensions

Custom blocks and toolbar buttons are authored on the frontend with the published uncial block API (defineSvelteBlock) and registered on a shared window.uncialWagtail global before the admin bundle initializes. The admin bundle then instantiates each block whose id appears in a panel's custom_blocks config. Registration is by key, so always merge into the global rather than replacing it — the chooser bridge writes its own keys onto the same object:

// your-admin-blocks.ts — bundled and loaded in the admin before the editor
import { defineSvelteBlock } from 'uncial/core';
import Callout from './Callout.svelte';

function createCalloutBlock() {
	return defineSvelteBlock({ id: 'callout', label: 'Callout', attributes: { tone: 'info' }, component: Callout });
}

// Merge, never overwrite: `window.uncialWagtail` may already hold the chooser bridge.
const uncialWagtail = (window.uncialWagtail ??= {});
uncialWagtail.customBlocks = { ...uncialWagtail.customBlocks, callout: createCalloutBlock };

Load that bundle ahead of the editor. The reliable ordering hook is insert_global_admin_js, which Wagtail renders before form-media JS:

# wagtail_hooks.py
from django.templatetags.static import static
from django.utils.html import format_html
from wagtail import hooks


@hooks.register("insert_global_admin_js")
def register_uncial_blocks():
    return format_html('<script src="{}"></script>', static("app/admin-blocks.js"))

Then list the block id in the panel config: custom_blocks=["callout"] (and allowed_blocks=["callout"] so it can be inserted).

Demo project

A runnable Wagtail project lives in demo/ — including the callout/card custom blocks and image blocks wired end to end. See its README to start it locally.

Metadata

Release files for uncial-wagtail 3.0.0

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

Source distribution (sdist)

Source distribution for uncial-wagtail 3.0.0
File Size Uploaded
uncial_wagtail-3.0.0.tar.gz 266.3 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for uncial-wagtail 3.0.0
File Interpreter ABI Platform
uncial_wagtail-3.0.0-py3-none-any.whl Python 3 none any Details

Total release size: 501.1 kB

Release files / uncial_wagtail-3.0.0.tar.gz

Download URL uncial_wagtail-3.0.0.tar.gz
Size 266.3 kB
Tags Source
SHA-256 checksum
How to use checksums
756102cec1634721ec2c2eb632b9acee26c18d471f59c360b5feacbad4d6b0d5
BLAKE2b-256 checksum
How to use checksums
4d3fac98dfefd5864dce079d20706ddc76d84e5f97bc110ead98c7a72c390c9e
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via uv/0.12.18 {"installer":{"name":"uv","version":"0.12.18","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

Release files / uncial_wagtail-3.0.0-py3-none-any.whl

Download URL uncial_wagtail-3.0.0-py3-none-any.whl
Size 234.8 kB
Tags Python 3
SHA-256 checksum
How to use checksums
55a383df1f34f764036cc329d5beef8ab59888cc8b20638b3f0caea2d0342af1
BLAKE2b-256 checksum
How to use checksums
435896c090a753dfa766ef542bb0b7888f5641bdff134d34d679a22fe6163f48
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via uv/0.12.18 {"installer":{"name":"uv","version":"0.12.18","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

Release history Release notifications | RSS feed

5.1.0

2 release files

5.0.0

2 release files

4.0.0

2 release files

This release

3.0.0 This release

2 release files

2.0.3

2 release files

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