Ragtail provides a Wagtail-inspired StreamField for flexible page content: an ordered list of typed blocks (Markdown, HTML, images, structured fields, and custom templates). Block data is stored as JSON in the page row's page_data column — no extra database migration is required when you add a StreamField to a page model.
from oxyde import Field
from ragtail import Page, register_page_model
from ragtail.streamfield import (
HtmlTextBlock,
ImageBlock,
MarkdownTextBlock,
StreamField,
StreamValue,
render_stream_value_html,
)
@register_page_model
class ContentPage(Page):
content: StreamValue | None = StreamField(
[
MarkdownTextBlock(),
HtmlTextBlock(),
ImageBlock(renditions=("width-800",)),
],
default=None,
)
async def get_context(self, request, route):
stream_html = await render_stream_value_html(self.content)
return {"stream_html": stream_html}In a PyJSX template, output the rendered HTML with HTMLDontEscape:
from pyjsx.jsx import HTMLDontEscape
<div class="prose">{HTMLDontEscape(context["stream_html"])}</div>See examples/demo/pages.py and examples/demo/site_templates/content_page.px.
Each block is stored as a JSON object in page_data:
[
{"id": "a1b2c3d4", "type": "markdown_text", "value": "## Hello\n\nWorld"},
{"id": "e5f6g7h8", "type": "html_text", "value": "<p>Rich HTML</p>"},
{"id": "i9j0k1l2", "type": "image", "value": 42}
]typematches the block'sname(e.g.markdown_text,cta_button)valueis a string, integer (image ID), or object (struct blocks)
cast_page() resolves image IDs to Image instances where applicable. The admin editor serialises blocks into a hidden JSON field on save.
| Block | block_kind |
Stored value | Admin widget |
|---|---|---|---|
MarkdownTextBlock |
markdown |
Markdown string | Textarea |
HtmlTextBlock |
html |
Sanitised HTML | TipTap visual editor + HTML code toggle |
ImageBlock |
image |
Image ID (int) | Image chooser |
CharBlock |
char |
Plain text | Single-line input |
URLBlock |
url |
URL string | URL input (auto-prefixes https://) |
StructBlock |
struct |
Object {field: value} |
One input per child field |
Rendered on the public site via the same pipeline as the page body field (markdown-it + bleach).
Edited in the admin with TipTap (visual mode) or a raw HTML code tab. Stored HTML is sanitised with bleach before save.
Uses the image library chooser. Optional renditions on ImageBlock control which sizes are generated for the public site and API.
ImageBlock(renditions=("width-800", "fill-1200x480"))from ragtail.streamfield import CharBlock
CharBlock(name="caption", label="Caption")Without a template, values render as a <p> on the public site.
from ragtail.streamfield import URLBlock
URLBlock(name="link", label="Link")Relative paths (/about/) and absolute URLs are accepted. Bare domains are normalised to https://….
Subclass CharBlock (or another block) and set a template with {value} placeholders:
from ragtail.streamfield import CharBlock
class HighlightBlock(CharBlock):
template = '<mark class="bg-yellow-200 px-1">{value}</mark>'
def __init__(self) -> None:
super().__init__(name="highlight", label="Highlight")Placeholder values are HTML-escaped when rendered.
Combine child blocks and render through an HTML template with {field_name} placeholders:
from ragtail.streamfield import CharBlock, StructBlock, URLBlock
class CtaButtonBlock(StructBlock):
def __init__(self) -> None:
super().__init__(
name="cta_button",
label="Button / Link",
fields={
"label": CharBlock(name="label", label="Button text"),
"url": URLBlock(name="url", label="Link URL"),
},
template=(
'<a href="{url}" class="btn">{label}</a>'
),
)Register the block in your StreamField list like any built-in type. Full demo examples live in examples/demo/stream_blocks.py.
All block types inherit from ragtail.streamfield.Block. You can subclass Block directly for full control:
from ragtail.streamfield import Block
class QuoteBlock(Block):
block_kind = "char"
template = "<blockquote><p>{value}</p></blockquote>"
def __init__(self) -> None:
super().__init__(name="quote", label="Quote")Override prepare_value() or render_value() on the base class when you need custom validation or output.
StreamField properties appear automatically in the page editor when declared on a registered page model. Editors can:
- add blocks from the buttons below the stream area
- reorder blocks (Up / Down)
- remove blocks
The streamfield script (streamfield.js) is loaded when the page type includes a StreamField. HTML blocks load TipTap from the same bundle family as the main rich text editor.
Fields named body still use the separate page-level TipTap widget; stream Markdown blocks use a plain textarea.
from ragtail.streamfield import render_stream_value_html
html = await render_stream_value_html(page.content)Pass block_definitions if the StreamValue was loaded without definitions attached.
from ragtail.streamfield import render_stream_block_html
fragment = await render_stream_block_html(
"markdown_text",
"## Title",
block_definitions=content_field_blocks,
)Iterate page.content.blocks and branch on block.type instead of using render_stream_value_html() if you need full layout control.
GET /api/cms/pages/{path} includes StreamField data under fields.<name> when the page type defines extra fields. Image blocks include rendition metadata when configured on ImageBlock.
- Flat blocks only — no nested StreamBlocks inside StreamBlocks
- StructBlock child fields are
CharBlockandURLBlockin the admin (extensible in code, not yet pluggable in the UI) - No block-level preview in the admin
- Documents, workflows, and permissions are not covered here