Hosting the field
The editor is an October form widget registered as contenteditor. It works in any form: Tailor blueprints, plugin backend forms, your own models.
Backend forms (fields.yaml)
fields:
content:
label: Content
type: contenteditor
# all optional:
allowBlocks: [heading, richtext, image] # whitelist of block handles (default: all)
maxDepth: 3 # nesting cap (default: 20)
previewCss: [/themes/demo/assets/css/theme.css] # default: the global "Preview stylesheets" setting
fillHeight: false # default: fill the remaining viewport height
showPreview: false # default: show the live-preview column
blockSettings: false # default: the global "Show block settings in the editor" setting
seoTab: false # default: the global "Show the SEO tab in the editor" setting
previewLayout: default # CMS layout of the fallback preview (default: the theme's 'default')
previewWrapClass: 'container' # class(es) around the document in the fallback preview
previewVar: article # extra page variable for the record in a Tailor real-page preview (default: post/entry)
draftContext: article # AI drafts fit an article column: no hero, no full-width sections (default: page)draftContext: article suits fields whose page renders its own title, cover and narrow column, such as blog posts: AI drafts then skip heroes and full-width sections. allowBlocks and maxDepth are also enforced on save by DocumentSaveGuard. Violations already in the stored document are grandfathered.
Practical live preview
The preview renders the blocks inside a real theme layout (previewLayout), or a minimal page if that layout fails. Links are inert and the frame is script-sandboxed. RainLab.Pages static pages use the page's own layout.
Tailor blueprints
The same contenteditor field type (with allowBlocks / maxDepth) works in blueprints. It adds itself to the model's jsonables and creates a nullable longText column.
Preview through the real page
A Tailor entry previews through the page that binds its blueprint with [section] isDefault = 1, including that page's container. This works for drafts and new entries and shows unsaved edits without saving. The record is injected as post and entry (add a name with previewVar); previewWrapClass is ignored here.
Without such a page, or if it fails to render, the preview uses a layout: previewLayout, else the section page's layout, else the RainLab.Pages page's layout, else the theme's default.
What the real-page preview does not reproduce
The page's PHP onStart does not run, so SEO tags, previous/next links and other computed variables are missing. Headline and cover image come from the last saved scalar fields.
Plain models
Use a nullable longText column and add the field to $jsonable. The widget saves normalized JSON; reads return the decoded array.
public $jsonable = ['content'];
// migration
$table->longText('content')->nullable();Rendering the document
{# CMS component: also injects the frontend CSS/JS (includeAssets, includeBootstrap) #}
[renickContent]
==
{% component 'renickContent' document=post.content %}
{# Layout without its own container: root blocks with no stored width get one #}
{% component 'renickContent' document=entry.content rootWidth='container' %}
{# Twig function and filter: HTML only #}
{{ content_render(post.content)|raw }}
{{ post.content|rc_blocks }}All three call BlockRenderer::renderDocument() and accept a JSON string or a decoded array. Rendering never throws: a broken block becomes an HTML comment (a visible card in debug mode).
The editor shows Container as every block's default width but stores nothing, so a block keeps its full width unless the layout boxes it in. A layout without an outer container (like Dawn's pages) passes rootWidth (container or narrow): root blocks that store no width get it, and so do the children of a root section that stores none. Blocks declared fullBleed: true (hero and CTA bands) stay full width.
Upgrading to 1.0.7: |content is now |rc_blocks
The old filter hid October's own |content filter. Rename {{ record.content|content }} (with or without |raw) to {{ record.content|rc_blocks }} wherever it renders a block document, then run php artisan cache:clear. Keep |content on rich editor HTML. A document left on |content prints raw JSON, or throws for a decoded array (a Tailor field).
Site-restricted blocks fail closed
A block with visibility.sites renders only when the active site is known and listed. In CLI or tests, pass it: BlockRenderer::renderDocument($doc, ['siteId' => 1]).
Plugin integrations
All integrations are off by default and inert without their plugin. A block whose integration is missing or off renders nothing; the editor preview says why.
The RainLab.Pages and RainLab.Blog switches are in Settings → Content → Content Editor → Editor → Integrations. They never touch existing content: only empty fields and block documents get the block editor, and block documents keep it when the switch is turned off.
Static Pages (RainLab.Pages)
Layout variable (RainLab.Pages 2.x and 3.x)
[staticPage]
useContent = 0
==
{variable type="contenteditor" name="content" label="Content" tab="Content"}{/variable}
{% page %}- The only option on Pages 3.x, whose editor ignores the settings switch. Dawn's
staticlayout ships it. - One
contenteditorvariable per layout. The document is stored in the page markup, so{% page %}renders it and assets are injected. - Pages whose content is still HTML open read-only and keep their HTML.
- On 3.x, translations save to
content/static-pages-<locale>/; one identical to the base page stays empty. History is per site. - Pages 3.x does not warn about unsaved changes in layout variables.
Settings switch (RainLab.Pages 2.x)
"Use the block editor for Static Pages" replaces the content field on layouts without the variable.
Blog posts (RainLab.Blog)
"Use the block editor for blog posts" edits content as blocks and renders content_html from it on save and load, so blogPost and blogPosts keep working. Markdown posts keep the Markdown editor. AI drafts use the article context there (draftContext: article).
Shop (OFFLINE.Mall)
Settings → Content → Mall Integration (only while OFFLINE.Mall is installed) adds the Mall Products and Mall Category blocks, display defaults and a status list of the pages Mall needs.
- Shop scaffold (button or
renick:content:mall-scaffold): copies ashoplayout, the product, category, cart, checkout, account, address and login pages, and partial overrides fromtemplates/mall/in the theme lineage (fallback: neutral Bootstrap 5 templates) and links them in Mall's settings. Existing files are kept. - Product data is resolved in
renick.content.beforeRenderBlock, cached per currency and customer group, and flushed on product, variant, price and category saves. - Mall ships no frontend CSS. Dawn's shop layout loads
offline-mall-integration.cssand.js; other themes stay unstyled.
User submissions (Tailor)
On October CMS 4.4+, Settings → Content → Submission Forms adds the Submission Form block and the tailorSubmission component, which extends Tailor\Components\SubmissionComponent. The blueprint keeps its validation, moderation state, spam sweep, notifications, October's throttle and honeypot. The plugin adds:
- the local captcha per block (Always on works with the global setting off),
- mandatory fields that can't be hidden, and a block that refuses to render if a required field can't be shown,
- private uploads (
uploads/protected, backend only) with type, count and size checked before posting, - an HMAC-signed
_rc_tsffield, verified oncms.page.init, so cached block HTML can reach the handler.
Override partials/renick-content/tailor-submission-form.htm to theme it; no {% framework %} is needed. php artisan theme:seed dawn installs a demo at /en/appointment and switches the integration on (4.4+ only).
Writing your own integration
Extend Integration and return it from renick.content.registerIntegrations; a built-in with the same code() wins. Only code() and isAvailable() are required, and only available integrations boot or register components. Optional: boot(), settingsPage(), blockDefinitions() (while enabled), blockHandles(), unavailableNotice(), components(), extendSettingsController(), cacheKeySuffix(), flushCaches(). isEnabled() reads <code>_enabled (dashes as underscores) from the Content Editor settings unless you override settingsModel() / settingKey().
use Renick\Content\Classes\Integrations\Integration;
final class AcmeShopIntegration extends Integration
{
public function code(): string { return 'acme-shop'; }
public function isAvailable(): bool
{
return \System\Classes\PluginManager::instance()->hasPlugin('Acme.Shop');
}
public function blockDefinitions(): array
{
return [/* renick.content.registerBlocks payload */];
}
}
// Plugin.php: register(), not boot(), because Renick.Content collects integrations in its own boot()
public function register()
{
Event::listen('renick.content.registerIntegrations', fn () => [new AcmeShopIntegration()]);
}