Craft CMS Operations
Versions verified 2026-10-05 against Packagist and the official docs. Craft 5.x is current (5.11); Craft 4 and 3 are past security support; Craft 6 is in alpha.
scripts/check-craft-facts.py --livere-checks the plugin majors on a schedule.
The recurring agency shape: Craft 5 (or a 4/3 site awaiting upgrade) on DDEV, with SEOmatic, Blitz, Formie, and CKEditor, a Vite (or legacy Laravel Mix) front end, and modules tested with Codeception and ECS. This file is the procedure and the router; each topic lives in one reference.
Step 1 - Orient before touching anything
| Check | Where | Tells you |
|-------|-------|-----------|
| Craft + plugin versions | composer.json, ddev composer show craftcms/cms | Which line of the version matrix applies - Craft 4 and 5 APIs differ |
| Local environment | .ddev/config.yaml | PHP/DB versions; run everything as ddev craft, ddev composer, ddev npm |
| Schema | config/project/*.yaml | Source of truth for sections, fields, entry types, plugin settings |
| Config | config/general.php, config/<plugin>.php, .env | Environment-specific behaviour (devMode, allowAdminChanges, caching) |
| Front-end build | package.json + config/vite.php, or webpack.mix.js | craft-vite or Laravel Mix |
| Templates | templates/_layouts, _partials, _partials/entry/<type>.twig | Layout inheritance and element partials |
| PHP | modules/, tests/, codeception.yml, ecs.php | Where logic and tests live |
| Page cache | config/blitz.php, Blitz utility | Whether what you see is cached HTML - check before debugging "my change isn't showing" |
Step 2 - Route the task
| Task | Read |
|------|------|
| Listing pages, relations, Matrix/nested entries, pagination, multi-site | element-queries.md |
| Printing anything user-influenced, forms that POST, JS data, rich text output | twig-security.md |
| Meta tags, JSON-LD, sitemaps, robots.txt, hreflang | seomatic.md |
| Static caching, "changes don't show", cache warming, dynamic bits on cached pages | blitz.md |
| Building, theming, or debugging forms; spam; form emails not sending | formie.md |
| Rich-text fields, nested entries inside rich text, Redactor conversion | ckeditor.md |
| Craft on DDEV: the craftcms type, ddev craft, Craft's upload_dirs (generic DDEV: ddev-ops) | ddev.md |
| Tests for modules/plugins, fixtures, ECS/PHPStan | codeception.md |
| Front-end assets, Vite dev server, critical CSS | craft-vite.md |
| Moving off Laravel Mix/Webpack, or Vue 2 to Vue 3 | frontend-upgrade-ops (the full migration playbook) |
| Upgrading Craft 3 → 4 → 5, plugin version lines, Craft 6 status | upgrades.md |
| Slow pages, images, queue setup, production config | performance.md |
| Core Web Vitals failing (LCP, INP, CLS), PageSpeed or Search Console flags | web-perf-ops (field data first; its references/craft.md maps the Craft levers) |
| Headless front ends, GraphQL schemas and tokens | graphql.md |
| Modules, plugins, events, migrations, queue jobs | plugin-development.md |
| Modeling a new flexible page type | assets/entry-type-field-layout.md |
Step 3 - Hold the non-negotiables
- Eager-load before you loop.
.with([...])up front;.eagerly()inside shared partials (Craft 5 only). One query per card is the most common Craft perf bug. - Escape by context. Never
|rawuser-influenced values;|e('js')inside JS strings;{{ csrfInput() }}in every POST form -csrfInput({ async: true })on cached pages. - Schema only through Project Config. Change it in the CP locally, commit
config/project/, runphp craft upon deploy. Production runsallowAdminChanges => false; never hand-edit the YAML. - Data changes are content migrations (
php craft migrate/create), tested on a copy of production - not CP clicking on live. - Production runs a queue worker. Blitz regeneration, Formie emails and integrations, transform pre-generation, and resaves are all queue jobs.
- Fix queries, then cache. Blitz for public pages; on Blitz-cached pages drop
{% cache %}(or setenableTemplateCachingfalse). - Logic lives in module services, not Twig - services are testable.
- Stay on the latest Craft 5 patch. Craft ships regular Twig/RCE security fixes.
Version matrix
| Package | Craft 3 | Craft 4 | Craft 5 (current) |
|---------|---------|---------|-------------------|
| craftcms/cms | 3.9 (EOL) | 4.18 (EOL Apr 2026) | Craft 5.x (5.11); Craft 6 in alpha |
| nystudio107/craft-seomatic | 3.x | 4.x | SEOmatic 5 |
| putyourlightson/craft-blitz | 3.x | 4.x | Blitz 5 (5.13 needs Craft 5.6+) |
| verbb/formie | 1.x | 2.x | Formie 3 (Formie 4 in beta) |
| Rich text | Redactor | Redactor or craftcms/ckeditor 3.x | CKEditor plugin 5.x (needs Craft 5.10+; 4.x below that) |
| nystudio107/craft-vite | 1.x | 4.x | craft-vite 5 |
| nystudio107/craft-imageoptimize | 1.x | 4.x | ImageOptimize 5 |
| spacecatninja/imager-x | 3.x | 4.x | Imager-X 6 (5.x still maintained) |
| codeception/codeception | match core's require-dev | match core's require-dev | Codeception 5 |
Requirements for Craft 5: PHP 8.2+, MySQL 8.0.17+ / MariaDB 10.4.6+ / PostgreSQL 13+. Details and plugin upgrade notes: upgrades.md.
Everyday commands
| Command (prefix ddev locally) | Does |
|---------------------------------|------|
| craft up | Pending migrations + Project Config - run on every deploy |
| craft project-config/apply / project-config/rebuild | Apply YAML to the DB / rebuild YAML from the DB |
| craft clear-caches/all | Data, template, and asset caches |
| craft queue/run / queue/info / queue/retry all | Work and inspect the queue |
| craft blitz/cache/refresh | Refresh Blitz after template deploys (Blitz tracks content, not templates) |
| craft migrate/create <name> | New content migration |
| craft make <type> | Scaffold modules/plugins/components (craftcms/generator) |
| craft entrify/categories <group> | Convert categories (or tags, global-set) to entries |
Craft 5 content model
| Concept | What it is | Craft 5 change |
|---------|-----------|----------------|
| Section | Single, Channel, or Structure; holds entry types + URI formats | - |
| Entry type | The unit of content shape | Global and reusable across sections and Matrix fields |
| Field | Reusable input | Global, with multi-instance use in one layout |
| Matrix field | Repeatable nested content | Stores nested entries, not blocks |
| CKEditor field | Rich text | Can hold nested entries inline |
| Element partials | _partials/entry/<typeHandle>.twig | .render() renders nested entries through them |
| Project Config | config/project/ YAML | Source of truth - commit it |
Pick the section type by shape: Single for one-off pages (home, contact), Channel for streams (news, events), Structure for hierarchies (pages, docs). Prefer flat entry types plus a Matrix "page builder" field over many near-identical sections. Starter shape: entry-type-field-layout.md.
Gotchas
| Symptom | Cause | Fix |
|---------|-------|-----|
| Listing page runs hundreds of queries | Relation fetched per item | .with() / .eagerly() |
| Change visible in CP, not on site | Blitz/CDN serving cached HTML | blitz.md |
| Form submissions save, emails never send | Queue not running in production | Queue daemon (performance.md) |
| Assets unstyled in production only | Vite 5+ manifest is in dist/.vite/ | Set manifestPath (craft-vite.md) |
| {% set seomatic.meta.seoTitle = ... %} does nothing | set only reads | {% do seomatic.meta.seoTitle('...') %} |
| Staging copy of the site got indexed / prod de-indexed | SEOmatic environment wrong | Check robots in view-source (seomatic.md) |
| Project Config conflicts between developers | CP edits on shared/prod environments | allowAdminChanges false outside local; one schema change per PR |
| Event handler fires several times per save | Drafts, revisions, propagation | Guard with ElementHelper::isDraftOrRevision() + propagating |
| craft.matrixBlocks() errors after upgrade | Removed in Craft 5 | craft.entries().field(...).owner(...) |
Bundled resources
| File | Use |
|------|-----|
| assets/entry-type-field-layout.md | Content-modeling starter: section + entry type + Matrix-as-entries, mapped to Project Config |
| assets/craft-facts.json | The version facts this skill documents (package, major, prose token) |
| scripts/check-craft-facts.py | Staleness verifier: --offline (prose still states the facts) / --live (Packagist majors) |
See also
ddev-ops(generic DDEV: version pinning, env files, snapshots and sanitised pulls, Mutagen, add-ons, Xdebug, and an auditor for.ddev/mistakes)laravel-ops(Composer/PHP tooling; Craft 6 is Laravel-based) ·sql-ops(indexes behind sloworderBy) ·nginx-ops(serving Craft, Blitz rewrites) ·perf-ops(profiling) ·tailwind-ops·playwright-ops(browser tests against the DDEV URL)web-perf-ops(Core Web Vitals: CrUX/RUM first, then the failing subpart; Blitz, transforms, craft-vite critical CSS and SEOmatic/Formie script placement as INP/LCP levers)frontend-upgrade-ops(Laravel Mix or Webpack to Vite via craft-vite, Vue 2 to Vue 3, Vue islands in Twig, and when a widget should become Alpine instead)a11y-ops(WCAG 2.2 for Twig sites: heading levels across partials, asset alt text, Formie and CKEditor markup, multi-sitelang; see itsreferences/server-rendered-templates.md)security-ops(Craft/Twig/PHP security: CSRF and Formie,allowAnonymous, devMode and the security key, GraphQL scoping, uploads, advisories and Craft 3/4 end of life; itsreferences/craft-*.md,twig-*.md,php-*.md)- Craft 5 docs · Plugin Store · Craft security advisories
Why this shape: a 2026 survey of 57 Craft-agency repositories found Craft in 36 (Craft 5 ×17, 4 ×13, 3 ×6), DDEV in 36, SEOmatic in 33, Blitz 21, Formie 18, CKEditor 18, Laravel Mix 18 versus craft-vite 12, Tailwind 12, ECS 11, and Codeception 10. The references follow that frequency; upgrades matter because half the Craft sites were on an EOL major.