Theme
Craft CMS 5 technical blueprint
This document turns the source specification into an implementation-facing Craft CMS 5 plan. It defines the intended runtime, content schema, template boundaries, operational controls, and acceptance criteria for the Flexible Page Builder.
IMPORTANT
This is a proposed build contract, not evidence that the target Craft project has already been configured. Handles and infrastructure choices remain proposals until they are created in the actual Craft repository and committed through Project Config. The decision gates at the end of this page still block a final implementation.
Implementation status
| Area | Status | Meaning |
|---|---|---|
| Craft 5 with nested Matrix entries | Confirmed | This is the agreed page-builder architecture. |
| Seven component entry types | Confirmed | The component set is fixed for the initial release. |
| Wheel Form 4.0.4 | Confirmed baseline | The plugin is Craft 5 compatible; its field value and render flow must be verified in the target project. |
| Field and entry-type handles below | Proposed | Adopt these handles before Project Config is generated, or record an explicit replacement manifest. |
| Responsive and heading rules | Partially open | The specification and design evidence disagree in several places. |
| Hosting, database, cache, queue, and mail services | Not yet inventoried | Confirm these in the Craft application environment before implementation starts. |
Platform baseline
Craft 5 is a self-hosted PHP application using a relational database and Twig for server-rendered templates. The page builder does not need a separate headless front end or third-party visual-builder plugin.
Runtime requirements
The current Craft 5 requirements establish this minimum platform:
| Dependency | Minimum | Project recommendation |
|---|---|---|
| PHP | 8.2+ | Use a supported 8.2+ release with OPcache enabled. |
| Database | MySQL 8.0.17+, MariaDB 10.4.6+, or PostgreSQL 13+ | Prefer MySQL 8.0.36+ or PostgreSQL 16+; do not choose MariaDB for a new build without a hosting constraint. |
| PHP memory | 256 MB | Allocate at least 512 MB for control-panel actions, transforms, and queues. |
| Composer | 2.0+ | Use Composer 2 and commit composer.lock. |
| Image library | GD or ImageMagick | Prefer ImageMagick and verify support for every requested output format. |
| Writable paths | storage/, config/license.key, web/cpresources/ | Grant the PHP user only the required write access; never use 0777. |
The native Link field replaced the URL field in Craft 5.3, so the effective feature floor for this specification is Craft 5.3. That is not a recommended patch target: install the latest supported Craft 5 release allowed by the project constraint, test it, and freeze the resolved dependency graph in composer.lock.
Package policy
- Require Craft CMS within major version 5 and keep the resolved patch version locked.
- Pin Wheel Form to the approved compatible release until its target-project integration passes.
- Install and lock the first-party CKEditor plugin used by the constrained rich-text fields.
- Perform dependency updates in development, review release notes and migrations, and deploy the resulting lockfile. Production must use
composer install, not an unreviewedcomposer update. - Run
composer auditin CI and subscribe to Craft critical-release notifications.
Craft 5 remains in active support, but support status is not a substitute for patching. Critical Craft and plugin updates should be handled as operational work, not deferred to feature releases.
Application architecture
Request and rendering flow
text
HTTP request
→ Craft route resolves a Page entry
→ Page template queries pageBuilder
→ Matrix returns ordered nested entries
→ dispatcher selects a template by entry-type handle
→ isolated Twig partial renders one component
→ semantic HTML, responsive assets, and minimal JavaScriptThe Page entry owns presentation order. Each Matrix item owns only its component data. Global navigation, metadata, canonical URLs, the page H1 contract, and site-wide layout remain the responsibility of the page template or layout—not an arbitrary component partial.
Proposed template structure
text
config/
general.php
project/
ckeditor/
templates/
_layouts/
base.twig
_entry/
page.twig
_components/
headerBanner.twig
oneColumn.twig
twoColumn.twig
accordion.twig
bannerText.twig
formComponent.twig
fourColumn.twig
_macros/
components.twig
web/
index.php
assets/Only web/ should be publicly reachable. Application source, .env, Composer files, templates, configuration, storage, database backups, and vendor metadata stay above the web root.
Content model
Root Matrix field
Create one pageBuilder Matrix field and attach it to the approved Page entry type. Configure it with the seven component entry types in the order editors should encounter them:
headerBanneroneColumntwoColumnaccordionbannerTextformComponentfourColumn
Nested entries remain owned by the Page entry and must not receive independent public URLs. Use an inline Blocks or Cards authoring view after testing both with real content; the selected mode must retain drag-and-drop ordering and make collapsed items identifiable.
Shared fields
| Handle | Craft field type | Configuration | Used by |
|---|---|---|---|
ctaLabel | Plain Text | Single line; optional; sensible character limit | Header Banner, One Column, Two Column baseline, Banner Text |
ctaLink | Link | URL, Entry, Email, and Phone; optional | Same components as ctaLabel |
heroImage | Assets | Images volume; image kinds only; max 1 | Header Banner |
bannerImage | Assets | Images volume; image kinds only; max 1 | Banner Text |
Keep the current two-field CTA model until product ownership approves a change. Its invariant is strict: ctaLabel and ctaLink must either both be empty or both be populated. A field-layout condition may improve the editor experience, but server-side validation must enforce the pair.
The native Link field can supply its own label. Consolidating the CTA into one Link field is a valid later simplification, but it changes the source specification and should be recorded as a model decision before Project Config is created.
Component entry types
| Entry type | Proposed fields | Validation and behaviour |
|---|---|---|
headerBanner | bannerHeading, heroImage, description, ctaLabel, ctaLink | Heading and image required; description and CTA optional. The page-level heading rule determines whether the heading renders as H1 or H2. |
oneColumn | contentType, body, contentImage, ctaLabel, ctaLink | contentType is text or image; exactly the matching content field is required. |
twoColumn | layoutVariant, columnOneText, columnOneImage, columnTwoText, columnTwoImage, ctaLabel, ctaLink | Allow only textText, imageImage, or textImage; require the fields selected by the variant. CTA cardinality remains a decision gate. |
accordion | accordionItems | Nested Matrix; at least one enabled accordionItem. |
bannerText | bannerImage, bannerCopy, ctaLabel, ctaLink | Image and copy required; CTA optional. |
formComponent | form | Exactly one existing Wheel Form form required. Do not duplicate form definitions inside the component. |
fourColumn | columns | Nested Matrix; minimum four enabled fourColumnItem entries; no maximum. Render all items generically. |
Nested entry types
| Entry type | Owner field | Fields | Rules |
|---|---|---|---|
accordionItem | accordionItems | accordionHeading, accordionContent | Both required; heading is Plain Text and content uses constrained CKEditor. |
fourColumnItem | columns | columnImage, columnText | Image required; supporting Plain Text optional. |
Use generated or descriptive nested-entry titles so cards and collapsed blocks remain scannable. Prefer the visible heading or text value where one exists; otherwise use the component label and its position. Add editor instructions, recommended image dimensions, icons, and colours to every entry type before author testing.
Conditions and validation
Conditional field visibility is an authoring aid, not the only validation layer.
| Rule | Authoring condition | Save-time invariant |
|---|---|---|
| One Column text | Show body when contentType = text | body is present and contentImage is ignored. |
| One Column image | Show contentImage when contentType = image | One valid image is present and body is ignored. |
| Two Column variant | Show only fields used by the selected variant | Every visible column value is present; stale hidden values do not render. |
| CTA | Show label and link together | Both fields are empty or both are populated. |
| Assets | Limit source, type, and cardinality | Exactly one image when the field is required. |
| Accordion | Inline nested items | At least one enabled, valid item. |
| Four Column | Reorderable nested items | At least four enabled, valid items; items five and above still render. |
| Form | Select an existing form | One form exists, is enabled, and renders successfully. |
Test validation on drafts, previews, revisions, duplicated components, and canonical entries. Nested-entry behaviour can differ while an owner is in draft state, so preview testing is part of the model acceptance—not only a front-end task.
Twig rendering contract
Explicit dispatcher
Use an allowlisted dispatcher rather than deriving an unrestricted include path from content:
twig
{% set componentTemplates = {
headerBanner: '_components/headerBanner.twig',
oneColumn: '_components/oneColumn.twig',
twoColumn: '_components/twoColumn.twig',
accordion: '_components/accordion.twig',
bannerText: '_components/bannerText.twig',
formComponent: '_components/formComponent.twig',
fourColumn: '_components/fourColumn.twig',
} %}
{% for block in entry.pageBuilder.all() %}
{% set componentTemplate = componentTemplates[block.type.handle] ?? null %}
{% if componentTemplate %}
{% include componentTemplate with {
block: block,
position: loop.index,
} only %}
{% endif %}
{% endfor %}An unknown type should be visible in development diagnostics and omitted safely in production. Add an automated test that fails when an allowed Matrix entry type has no dispatcher mapping or partial.
Partial rules
Every component partial must:
- Accept
blockandpositionas its complete public input. - Render one meaningful outer region with a stable component class or
data-componentvalue. - Resolve each relation once and reuse the result.
- Avoid global Page queries or mutations.
- Omit empty optional elements instead of producing empty wrappers, links, or headings.
- Use Twig's default escaping for plain text and attributes.
- Render only content from an approved rich-text field configuration as markup; never apply
rawto arbitrary editor or request data. - Keep component JavaScript optional and progressively enhanced.
- Never emit an H1 independently of the page-level heading contract.
CTA output
Use Craft's normalized Link value and encode the label through the tag helper:
twig
{% if block.ctaLabel and block.ctaLink %}
{{ tag('a', {
href: block.ctaLink,
class: 'button button--primary',
target: block.ctaLink.target,
rel: block.ctaLink.target == '_blank' ? 'noopener noreferrer' : null,
text: block.ctaLabel,
}) }}
{% endif %}Do not output a link with an empty label. Do not infer a new-tab target for internal links, and always pair _blank with noopener noreferrer.
Rich text
Create a dedicated CKEditor configuration for component copy. The starting toolbar is:
- Paragraph, H2, and H3.
- Bold and italic.
- Link.
- Bulleted and numbered lists.
Exclude H1, tables, source/HTML editing, embedded images, arbitrary styles, and media embeds unless a later requirement explicitly adds them. Configure HTML Purifier for the same allowed vocabulary so pasted or migrated markup cannot bypass the visual toolbar.
Keep headings semantic rather than selecting a level for visual size. Component CSS supplies presentation; the editor supplies document structure. Test pasted content from common office tools, nested lists, long links, empty paragraphs, and unsupported elements.
Image pipeline
Named transforms
Create named transforms in Craft so their definitions are captured in Project Config:
| Transform | Baseline output | Used by | Loading priority |
|---|---|---|---|
heroBanner | 1920 × 800 crop, quality 82 | Header Banner | Eager only when it is the actual above-the-fold LCP image. |
fullWidthBanner | 1920 × 600 crop | Banner Text | Lazy unless measurement proves it is above the fold. |
contentImage | 800 × 600 constrained crop or fit | One and Two Column | Lazy. |
columnThumb | 400 × 400 crop | Four Column | Lazy. |
Dimensions are a starting point from the source specification. Confirm aspect ratios against final design tokens before generating production assets. Disable upscaling unless a specific art direction requires it, preserve editor focal points for crops, and confirm ImageMagick supports the chosen WebP or AVIF output.
Responsive markup
twig
{% set image = block.heroImage.eagerly().one() %}
{% if image %}
{% set hero = {
mode: 'crop',
width: 640,
height: 267,
quality: 82,
} %}
{% do image.setTransform(hero) %}
{{ tag('img', {
src: image.url,
srcset: image.getSrcset(['1.5x', '2x', '3x']),
sizes: '100vw',
width: image.width,
height: image.height,
alt: image.alt ?? image.title,
loading: 'eager',
fetchpriority: 'high',
}) }}
{% endif %}Set sizes to the image's actual layout width; 100vw is correct for a full-bleed hero, not for column images. Always output intrinsic width and height to reserve layout space. Require meaningful alternative text for informative images and an empty alt value for deliberately decorative images; do not use the filename as a silent fallback in the finished implementation.
Only the first real LCP candidate receives eager loading and fetchpriority="high". Every below-the-fold image uses loading="lazy". Pre-generate or queue high-traffic transforms during deployment or content publication so the first visitor does not pay the full transform cost.
Wheel Form integration
The component stores a reference to one existing Wheel Form form. The source specification expects the selected field value to provide the plugin's form rendering object, but that contract must be verified against Wheel Form 4.0.4 inside the target Craft project before the template is finalized.
The implementation must provide:
- One form element and one submit path per component instance.
- CSRF and plugin-required hidden inputs from the plugin's supported open/close flow.
- A summary error region plus errors associated with individual fields.
- Persistent, non-sensitive user input after a failed validation attempt.
- Programmatic labels, descriptions, required state, and error state.
- Keyboard focus moved to the error summary or first invalid field after failure.
- Honeypot, reCAPTCHA, or another approved spam-control strategy.
- Tested mail transport, sender policy, recipient routing, reply-to handling, and failure logging.
- A success state that works with and without JavaScript enhancement.
Never place the form, its CSRF token, validation errors, or personalized success state inside a Twig fragment cache. If Blitz or another full-page cache exists, document and test the plugin's CSRF-refresh strategy before enabling caching on pages containing forms.
The design reference shows a fixed contact-form wireframe while the source specification allows editors to select any existing form. Product ownership must decide whether the wireframe is only a visual example or a fixed schema before bespoke field markup is built.
Accessibility and SEO
Page heading contract
Every rendered page has exactly one H1. Prefer the Page entry title as the canonical source. A Header Banner may render its heading as the H1 only when all of these are true:
- It is the first enabled content component.
- The page template deliberately delegates its H1 to that component.
- No separate Page H1 is rendered.
Otherwise, component headings begin at H2 and nested headings at H3. Editors cannot create H1 elements in CKEditor.
Component requirements
- Header and banner text must maintain sufficient text/background contrast across every permitted image.
- Accordion should use native
detailsandsummaryunless animation requirements justify a tested ARIA implementation. Its state, focus, and keyboard behaviour must work without a mouse. - Two- and four-column visual order must match DOM and keyboard order at every breakpoint.
- Links must describe their destination; repeated generic labels require accessible context.
- Images require an explicit informative or decorative alternative-text decision.
- Form controls need persistent labels, helpful instructions, announced errors, and visible focus.
- Motion enhancement must respect
prefers-reduced-motion.
SEO title, description, canonical URL, robots policy, and social metadata belong to the Page entry or SEO layer. Individual components must not generate competing canonical tags or duplicate the Page H1.
Performance and caching
- Start with the simple
entry.pageBuilder.all()query, profile it, then use lazy eager-loading via.eagerly()in partials or a deliberate.with()map for relational fields that produce N+1 queries. - Eager-load transform indexes for repeated assets when profiling shows transform lookups are significant.
- Render only enabled nested entries and populated optional values.
- Keep component CSS shared and component JavaScript small, deferred, and loaded only when needed.
- Use Twig fragment caching only around expensive, non-personalized output. Never cache CSRF inputs or form state.
- Run a real queue worker in production if automatic HTTP queue execution is disabled. Image transforms, search indexing, and bulk resaves depend on a healthy queue.
- Restart long-running queue workers after code, plugin, or schema deployments.
- Establish representative page fixtures: minimum content, one of every component, image-heavy content, long copy, and multiple forms.
- Measure server response time, query count, page weight, LCP, CLS, and INP before launch and after material model changes.
Security and environment configuration
Required controls
- Keep
devModeoff in production. - Set
allowAdminChangesto false outside development so production schema cannot drift from Project Config. - Store the Craft security key, database credentials, mail credentials, API keys, and environment-specific URLs in environment variables or aliases—not Project Config or Git.
- Serve only
web/; deny direct HTTP access toconfig/,storage/,templates/,vendor/,.env, and backup files. - Preserve Craft's CSRF protection and use Wheel Form's supported request flow.
- Grant control-panel and Page Builder permissions by role and least privilege.
- Apply security headers and trusted-proxy settings for the actual hosting topology.
- Back up the database and user-uploaded assets, and test restoration.
- Review Craft and plugin updates routinely; treat published critical fixes as urgent.
Twig auto-escapes unsafe output by default. Maintain that protection by using text rather than html in tag helpers, limiting trusted rich text to configured fields, and avoiding raw on request data or plain-text fields.
Project Config and deployment
All schema work must originate in a development environment and flow one way through version control.
Version-controlled application state
Commit these together:
composer.jsonandcomposer.lock.config/project/changes generated by Craft.- CKEditor and HTML Purifier configuration.
- Twig templates, modules, front-end source, and built-asset manifest as defined by the application.
- Content migrations required for data changes that Project Config cannot express.
Do not commit .env, database dumps, uploaded assets, runtime caches, logs, generated control-panel resources, or secrets.
Deployment sequence
- Put the release through CI: dependency install, linting, static analysis, template checks, front-end build, and automated tests.
- Back up the database and confirm a recoverable asset snapshot.
- Deploy the exact reviewed commit.
- Run
composer install --no-dev --prefer-dist --optimize-autoloader. - Run
php craft upto apply migrations and Project Config. - Restart PHP workers and any long-running Craft queue workers.
- Process or observe queued jobs, especially image transforms and element resaves.
- Smoke-test the control panel, a representative Page, preview, every component, form success/failure, and transformed images.
- Record the deployed commit, Craft version, plugin versions, and migration result.
A code rollback is safe only when its code, dependency graph, Project Config, and database schema remain compatible. If a migration is not backward-compatible, restore the matching database backup instead of merely switching application files.
Verification strategy
Content-model tests
- Editors can add, reorder, duplicate, disable, enable, and delete every component.
- Conditional layouts show the correct fields and reject incomplete values.
- CTA pairing, image cardinality, Accordion minimum, and Four Column minimum are enforced.
- Draft, preview, revision, propagation, and canonical-save behaviour are correct.
- Collapsed cards have useful labels and no two component types are visually ambiguous.
Rendering tests
- Each allowed entry type resolves to exactly one existing partial.
- Every component renders populated, optional-empty, and boundary-length content without errors.
- Unknown or disabled blocks do not break the page.
- The page contains one H1, valid landmarks, stable DOM order, and no empty links or headings.
- Images produce valid transformed URLs, accurate dimensions, useful
srcset/sizes, and correct loading priority.
Interaction tests
- Accordion is keyboard-operable and exposes its state to assistive technology.
- Forms pass success, server validation, spam rejection, CSRF expiry, mail failure, and retry scenarios.
- Focus indicators remain visible and error messages are announced.
- Components work at the approved breakpoint boundaries, not only named device widths.
- The page remains usable when optional enhancement JavaScript fails or is disabled.
Operational tests
composer installandphp craft upcomplete non-interactively on a clean release candidate.- Queue jobs are processed and failed jobs are observable.
- Database and asset restoration has been rehearsed.
- Production exposes no source/configuration paths and runs with
devModeand admin changes disabled. - Logs and health checks detect PHP errors, failed queue jobs, failed form mail, transform failures, and unsuccessful deployments.
Decision gates before implementation
These decisions remain open and must be recorded before their affected templates or fields are finalized:
- Form contract: selectable existing Wheel Form form or one fixed contact-form schema.
- Two Column CTA: one component-level CTA or an independent CTA per column.
- Four Column mobile layout: 2×2 grid from the latest design evidence or single-column stack from the PDF.
- Text/Image mobile order: explicitly approve image-first ordering or retain document order.
- Breakpoint token: reuse the site's existing token or approve one shared new value.
- H1 ownership: Page template H1 or conditional first Header Banner H1.
- Target environment inventory: exact Craft, PHP, database, CKEditor, Wheel Form, cache, queue, mail, storage, and deployment topology.
Until these are resolved, implement only the model and rendering work that does not encode one side of a disputed behaviour.
Official references
This blueprint was checked on 17 July 2026 against the current Craft 5 documentation and plugin record: