Architecture
Renick.Content has three layers:
- Content. A document is a list of typed blocks, stored as JSON in a host field: a Tailor entry, a model attribute, a RainLab.Pages page or a RainLab.Blog post. JSON Schema v1 defines the format; every write is validated.
- Editor. A TipTap 3 / React 19 island mounted by the
contenteditorform widget. The bundle inassets/distis committed, so installs never run a JS toolchain. - Rendering.
BlockRendererturns the JSON into HTML per request (viarenickContent,content_render()or|rc_blocks) and applies visibility rules server-side.
Component map
On save, DocumentSaveGuard normalizes the document, validates it strictly and checks BlockRules. Rule violations the stored document already had don't block the save. After the host is saved, a version is captured and the media usage index is refreshed.
The document lives in the host. The plugin's own tables are sidecars: renick_content_versions (snapshots, audit log), renick_content_templates, renick_content_media_usage, renick_content_shared (Shared Content), renick_content_scripts (Custom Scripts), renick_content_api_tokens (hashed) and renick_content_ai_usage (monthly AI budget).
Where things live
plugins/renick/content/
├── Plugin.php # registrations only
├── routes.php # REST API (off by default), tile/thumb proxies, form endpoints
├── blocks/ # <handle>/<handle>.yaml definition + <handle>.htm partial
├── classes/ # renderer, document model, Content/ContentApi facades, Integrations/, TailorList/, ai/
├── components/ # renickContent, Entry List, submission form
├── contentfields/ # Tailor content field
├── controllers/ # backend pages (tokens, audit log, scripts, shared content, templates)
├── formwidgets/ # ContentEditor, icon picker
├── console/ # renick:content:* commands
├── models/
├── schema/document-v1.json
├── assets/ # src/ editor source; dist/ and frontend/ committed bundles
└── tests/ # PHPUnit; Playwright e2e lives in /tests-e2eStart with Hosting the field, Authoring blocks, the PHP API or the REST API.