Skip to content

Architecture

Renick.Content treats content as structured data and layout as replaceable presentation. Everything in the plugin hangs off three layers:

  1. 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.
  2. Editor — a TipTap 3 / React 18 island, mounted by the contenteditor form widget in any backend form. The bundle is a committed Vite build (assets/dist); consumers never run a JS toolchain.
  3. RenderingBlockRenderer turns 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:

TablePurpose
renick_content_versionsfull document snapshots per save; doubles as the audit log
renick_content_templatesreusable document/subtree templates
renick_content_media_usagewhich media paths are referenced by which host + block
renick_content_ai_usageper-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 root

Key entry points for integrators: