Architecture
Renick.Content treats content as structured data and layout as replaceable presentation. Everything in the plugin hangs off three layers:
- Content (JSON) — a document is a list of typed blocks stored as JSON in a host field (a Tailor entry column, a plain model attribute). The format is a published contract: JSON Schema v1, validated on every write.
- Editor — a TipTap 3 / React 18 island, mounted by the
contenteditorform widget in any backend form. The bundle is a committed Vite build (assets/dist); consumers never run a JS toolchain. - Rendering —
BlockRendererturns the JSON into HTML at request time through a fallback-safe partial pipeline (theme override → plugin default → placeholder), applying visibility rules server-side.
Component map
The editor serializes the document into a hidden <textarea> inside the widget partial; on save, ContentEditor::getSaveValue() runs the value through ContentDocument::normalize() (repairs loose input), DocumentValidator (strict schema check) and BlockRules (hierarchy constraints) before it reaches the host field. After a successful host save, a version snapshot is captured and the media usage index is refreshed.
The document itself lives in the host — the plugin's own tables are sidecars:
| Table | Purpose |
|---|---|
renick_content_versions | full document snapshots per save; doubles as the audit log |
renick_content_templates | reusable document/subtree templates |
renick_content_media_usage | which media paths are referenced by which host + block |
renick_content_ai_usage | per-request AI token accounting for the monthly budget |
Where things live
plugins/renick/content/
├── Plugin.php # registrations: widgets, content fields, component,
│ # twig markup, permissions, settings, schedule
├── routes.php # optional REST API (/api/renick/content/v1, off by default)
├── blocks/ # 11 shipped block definitions (YAML) + default partials
├── classes/
│ ├── BlockManager, BlockDefinition, BlockRenderer, BlockRules
│ ├── ContentDocument, DocumentValidator, VisibilityRules
│ ├── Content, ContentApi # PHP facades
│ ├── MediaUsageIndex, MediaDeleteGuard, HostResolver
│ └── ai/ # ProviderManager, drivers, Translator, Drafter, FirecrawlClient
├── components/ # renickContent CMS component
├── contentfields/ # Tailor content field wrapper
├── formwidgets/ # the ContentEditor widget + mount partial
├── console/ # prune-versions, reindex-media commands
├── models/ # Version, Template, AiSetting, AiUsage
├── schema/document-v1.json
├── assets/src/ # editor source (TypeScript + React + TipTap 3)
├── assets/dist/ # committed editor bundle (ESM + CSS) — what ships
└── tests/ # PHPUnit; Playwright e2e lives in /tests-e2e at repo rootKey entry points for integrators:
- Hosting the field — put the editor into a form and render its output.
- Authoring blocks — add block types from a theme or another plugin.
- PHP API and REST API — manipulate documents programmatically.