Craft CMS Operations

SkillAI & models

Craft CMS 5 development - content modeling, Twig templating, element queries, GraphQL, plugins, and the Craft 4-to-5 Matrix-as-entries change. Use for: craft cms, craftcms, craft 5, twig, pixel & tonic, matrix field, entry types, sections, element query, eager loading, blitz, project config, headless craft, craft graphql, craft plugin, craft 4 to 5 upgrade.

Available today. Use it from your connected AI after setup.

Connect ahel once, and every AI you use reads what you have installed.

Then ask your AI: use the Craft CMS Operations skill

What this skill tells your AI

The instructions your AI receives, as published by 0xdarkmatter/claude-mods in skills/craftcms-ops/SKILL.md and read by ahel’s review.

Facts verified as of 2026-07.

Authoritative reference for Craft CMS 5.x development: content modeling, Twig templating, element-query optimization, GraphQL/headless setups, plugin development, and the Craft 4 → 5 migration. Craft is a self-hosted PHP application built on Yii 2, backed by MySQL or PostgreSQL.

Version note (verified against craftcms.com/docs/5.x, 2026-06): Craft 5 is current; Craft 6 exists. The defining Craft 5 change is that Matrix is now an entries-based field — Matrix "blocks" are gone, replaced by nested entries with entry types. Fields are globally reusable across all field layouts. Don't ship Craft 3/4 "Matrix block" guidance.


Craft 5 architecture at a glance

ConceptWhat it isCraft 5 change
SectionContainer exposing entry types + URL rulesThree kinds: Single, Channel, Structure
Entry TypeAtomic unit of content (fields, title, slug)Now global + reusable across sections, with per-section aliases
EntryAn instance of an entry typeCan be top-level or nested (inside Matrix/CKEditor)
FieldReusable input attached via field layoutsGlobally reusable — no per-field-instance duplication
Matrix fieldRepeatable nested contentNow stores entries (entry types), not "blocks". Nesting supported natively
Project ConfigVersion-controlled schema (config/project/)Source of truth for sections/fields/settings

Section types

TypeUse forHas URLs?Hierarchy?
SingleOne-off pages (home, about)Optional fixed URINo
ChannelStreams (blog, news, products)Yes, per-entry-type URI formatNo
StructureNested/ordered content (docs, nav)YesYes (drag-to-order, levels)

Element queries (the 80/20)

Everything readable in Craft is an element (entries, assets, users, categories, tags). You fetch them with element queries.

{# Channel entries, newest first #}
{% set posts = craft.entries()
  .section('blog')
  .type('article')
  .orderBy('postDate DESC')
  .limit(10)
  .all() %}

{# Eager-load relations to kill N+1 #}
{% set posts = craft.entries()
  .section('blog')
  .with(['author', 'featuredImage', 'categories'])
  .all() %}

{# Single entry by slug #}
{% set page = craft.entries().section('pages').slug('about').one() %}

{# Relations: entries related to a given category #}
{% set related = craft.entries().relatedTo(category).all() %}
NeedMethod
Filter by section.section('handle')
Filter by entry type.type('handle')
Eager-load relations.with(['field', 'field.subfield'])
Status.status('live') / .status(['live','expired'])
One vs many.one() / .all() / .count() / .exists()
Pagination{% paginate query as pageInfo, entries %}
Eager-load nested Matrix entries.with(['matrixField']) then loop nested entries

Eager-loading nested entries (Craft 5): because Matrix content is now entries, eager-load the Matrix field then iterate the nested entries by their entry type:

{% set page = craft.entries().section('pages').with(['body']).one() %}
{% for block in page.body.all() %}
  {% switch block.type.handle %}
    {% case 'text' %}{{ block.richText }}
    {% case 'image' %}{{ block.image.one().url }}
  {% endswitch %}
{% endfor %}

See references/twig-and-queries.md for the full query parameter catalog, pagination, and Twig patterns.


Twig conventions

PatternRule
Private templatesPrefix with _ (_layouts/, _partials/) so they're not directly routable
Layout inheritance{% extends '_layouts/base' %} + {% block content %}
Reusable markup{% include '_partials/card' with { entry: entry } %} or {{ include() }}
Avoid logic in templatesPush business logic to a module/plugin service, not Twig
Caching{% cache %}only after queries are optimized, never to mask N+1

Headless / GraphQL

Craft ships a GraphQL API for decoupled frontends (Next.js, Nuxt, Astro, etc.).

ConcernApproach
SchemaDefine GraphQL schemas + scopes in Control Panel; generate a token per schema
AuthBearer token per schema; public schema for anonymous reads
AlternativeElement API plugin for custom JSON endpoints when GraphQL is overkill
CORSConfigure allowed origins for the headless frontend
Eager loadingGraphQL resolves relations efficiently; still design queries to avoid over-fetching

See references/graphql-and-plugins.md for schema setup, query shape, and plugin/module development.


Performance decision table

SymptomFix
Slow listing pagesEager-load with .with([...]) — the #1 Craft perf bug is N+1 inside loops
Repeated identical render{% cache %} tag (after query optimization)
Whole-site cache neededBlitz plugin (static page caching, granular invalidation)
Slow orderBy on custom fieldEnsure the underlying column/field is indexed
Heavy asset transformsPre-generate transforms; use Imgix/CDN

Project Config & deployment

  • Project Config (config/project/*.yaml) is the version-controlled source of truth for sections, fields, entry types, settings. Commit it.
  • Apply on deploy: php craft up (runs migrations + applies project config).
  • Environment-specific values go in .env and config/general.php (use App::env() / getenv()).
  • Data transformations belong in content migrations, not manual DB edits.

Craft 4 → 5 upgrade checklist

AreaWhat changedAction
MatrixBlocks → entries with entry typesTemplates iterating .type.handle mostly survive; re-check block-type field handles
FieldsNow globally reusableExpect field/entry-type proliferation post-upgrade — consolidate duplicates
Content storageReworked internal storageRun php craft up; test queries on staging
PHP/DBCraft 5 needs PHP 8.2+Verify host before upgrading
PluginsMany need a Craft 5-compatible releaseAudit plugin compatibility first

Full upgrade guidance: https://craftcms.com/docs/5.x/upgrade.html


Common gotchas

GotchaWhyFix
N+1 queries in loopsElement relations lazy-loadAlways .with([...]) before iterating
{% cache %} masking slow queriesCache hides, doesn't fixOptimize queries first, cache second
Business logic in TwigHard to test/reuseMove to a module/plugin service
Project Config drift in teamsOut-of-band CP editsTreat config/project/ as source of truth; php craft up on deploy
Untested migrations to prodData loss riskTest on staging clone first
Over-using MatrixComplexity + perf costUse simpler structures when nesting isn't needed
Calling old "Matrix block" APIsRemoved in Craft 5Use entry/entry-type APIs

Assets

FileUse
assets/entry-type-field-layout.mdAnnotated content-modeling starter: section + entry type + field layout + Matrix-as-entries shape, mapped to Project Config

See also

  • laravel-ops — shared PHP/Composer/Twig-adjacent tooling, Eloquent patterns for comparison
  • sql-ops — index strategy behind slow orderBy/relation queries
  • nginx-ops — serving Craft, caching headers, reverse proxy for headless

Key external resources

Signals

GitHub stars
36
Forks
5
Last commit
Aug 2026
Advanced
Catalog kind
skill
Gateway key
craftcms-ops
Source
github.com/0xdarkmatter/claude-mods