Help us improve
Share bugs, ideas, or general feedback.
From craft-developer
Craft CMS development expertise for building sites, debugging issues, and providing community support. Use when working with Craft CMS templates (Twig), element queries, GraphQL API, Matrix fields, relational fields, eager loading, caching, plugins, or answering Craft CMS technical questions. Trigger this skill for ANY Craft CMS question — entry queries, Matrix nesting, relatedTo directions, GraphQL mutations, template debugging, CP issues, `.all()` vs `.one()`, conditional field layouts, or "why does my query return nothing." Also trigger when helping in Craft Discord or Stack Exchange, when the user pastes Twig code with `craft.entries()`, or when debugging N+1 queries or cache invalidation. Covers Craft 4 and Craft 5 patterns including all breaking changes between versions and features through 5.9.
npx claudepluginhub design-machines-studio/depot --plugin craft-developerHow this skill is triggered — by the user, by Claude, or both
Slash command
/craft-developer:craft-developmentThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Patterns and solutions for Craft CMS development. Use this for building sites, debugging, or answering community questions.
Guides technical evaluation of code review feedback: read fully, restate for understanding, verify against codebase, respond with reasoning or pushback before implementing.
Share bugs, ideas, or general feedback.
Patterns and solutions for Craft CMS development. Use this for building sites, debugging, or answering community questions.
All content access starts with element queries. Chain parameters, then execute:
{% set posts = craft.entries()
.section('blog')
.orderBy('postDate DESC')
.limit(10)
.all() %}
Execution methods:
.all() - Array of all results.one() - Single element or null.exists() - Boolean check.count() - Integer count.ids() - Array of IDs onlyMatrix fields contain nested entries. Query the parent, access nested content:
{# Get parent entry #}
{% set recipe = craft.entries().section('recipes').slug('cookies').one() %}
{# Access Matrix field - returns entry query #}
{% set steps = recipe.recipeSteps.all() %}
{% for step in steps %}
{# Each block is an entry with its own fields #}
<div class="step">
<h3>{{ step.title }}</h3>
{{ step.instructions|md }}
</div>
{% endfor %}
Query entries WITH Matrix content:
{# Entries that have at least one nested entry in Matrix field #}
{% set entries = craft.entries()
.myMatrixField(':notempty:')
.all() %}
Craft 5 GraphQL for Matrix:
query RecipeSteps {
entries(section: "recipes") {
... on recipe_Entry {
recipeSteps {
... on step_Entry {
title
instructions
}
}
}
}
}
The relatedTo parameter finds elements connected via relational fields.
Direction matters:
sourceElement - "Find what THIS element points to"targetElement - "Find what points TO this element"{# Products in this category (category is the target) #}
{% set products = craft.entries()
.section('products')
.relatedTo({
targetElement: category,
field: 'productCategories'
})
.all() %}
{# Categories this product belongs to (product is the source) #}
{% set categories = craft.categories()
.relatedTo({
sourceElement: product,
field: 'productCategories'
})
.all() %}
Combining conditions:
{# AND - must match ALL #}
{% set results = craft.entries()
.relatedTo(['and', category1, category2])
.all() %}
{# OR (default) - match ANY #}
{% set results = craft.entries()
.relatedTo([category1, category2])
.all() %}
Prevent N+1 queries when accessing related content in loops.
Upfront eager loading:
{% set posts = craft.entries()
.section('blog')
.with([
'featureImage',
'author',
['categories', { limit: 3 }],
])
.all() %}
Lazy eager loading (Craft 5+):
{% for post in posts %}
{% set image = post.featureImage.eagerly().one() %}
{% endfor %}
Lazy eager loading with count:
{% for recipe in recipes %}
{{ recipe.steps.eagerly().count() }} Step(s)
{% endfor %}
Nested eager loading:
{% set entries = craft.entries()
.with([
'matrixField.nestedAssetField',
'matrixField.nestedEntriesField',
])
.all() %}
Entry types exist in a global pool. The same entry type can be used in:
This eliminates duplication — define an entry type once, use it everywhere.
query BlogPosts {
entries(section: "blog", limit: 10, orderBy: "postDate DESC") {
title
url
postDate @formatDateTime(format: "F j, Y")
... on post_Entry {
summary
featureImage {
url @transform(width: 800, height: 600)
}
}
}
}
query FilteredProducts {
entries(section: "products", inStock: true, price: ">= 50") {
title
... on product_Entry {
price
sku
}
}
}
mutation SaveRecipe {
save_recipes_recipe_Entry(
title: "New Recipe"
steps: {
entries: [
{
step: {
instructions: "Step 1 content",
id: "new:1"
}
}
],
sortOrder: ["new:1"]
}
) {
id
title
}
}
For the full migration guide with upgrade checklists, load the
craft-5-migrationskill.
Craft 4: Empty relatedTo arrays return all results Craft 5: Empty arrays return NO results
{# WRONG in Craft 5 - returns nothing if categoryIds empty #}
{% set entries = craft.entries()
.relatedTo(categoryIds)
.all() %}
{# CORRECT - check first #}
{% set entries = craft.entries()
.relatedTo(categoryIds|length ? categoryIds : null)
.all() %}
blocks parameter → entries in GraphQL mutationsCraft 4: news_article_Entry
Craft 5: article_Entry (section prefix removed)
.status() - drafts/disabled excluded by default.site() - multi-site queries need explicit site{% dd craft.entries().section('x').getRawSql() %}Symptom: Slow pages, many DB queries in debug toolbar
Solution: Add .with([...]) to eagerly load relations
Check what triggers invalidation:
{% cache using key "posts" tags ["section:blog"] %}
...
{% endcache %}
Field tabs and individual fields can be shown/hidden based on conditions (entry type, status, user group, custom field values). If fields seem missing in the CP, check the field layout conditions before assuming a bug.
{# Generate random strings and UUIDs (5.9) #}
{% set token = randomString(32) %}
{% set id = uuid() %}
{# Hash with specific algorithm (5.9) #}
{{ 'data'|hash('sha256') }}
{# Encode URLs with special characters (5.5) #}
{{ encodeUrl('https://example.com/path with spaces') }}
{# Primary site object (5.6) #}
{{ primarySite.name }}
{# PHP_INT_MAX available (5.6) #}
{% set maxInt = PHP_INT_MAX %}
{# Custom field handles in orderBy (5.7) #}
{% set entries = craft.entries()
.section('products')
.orderBy('price ASC')
.all() %}
{# Custom field handles in where conditions (5.6) #}
{% set entries = craft.entries()
.section('products')
.andWhere(['>', 'price', 100])
.all() %}
{# Nested field values in orderBy (5.6) — e.g. date time zones #}
{% set entries = craft.entries()
.orderBy('myDateField.tz ASC')
.all() %}
{# Only canonical entries, no drafts/revisions (5.7) #}
{% set entries = craft.entries()
.canonicalsOnly(true)
.all() %}
Craft now supports fallback templates for element rendering:
_partials/entry/articleType.twig — specific to entry type_partials/entry.twig — fallback for any entry typeDefine redirects in config/redirects.php instead of the CP:
return [
'old-path' => 'new-path',
'blog/<slug>' => 'articles/<slug>',
];
Specify the current site via X-Craft-Site header (set to site ID or handle) instead of query params.
# Singles get dedicated queries (5.8)
query {
homepageEntry {
... on homepage_Entry {
heroTitle
}
}
}
<handle>Entry queries resolve directly (e.g. homepageEntry)searchTermOptions argument for fine-tuned searchwithProvisionalDrafts argumentX-Craft-Preview-Token header for preview requestsFor detailed patterns, see:
${CLAUDE_SKILL_DIR}/references/query-cookbook.md - 30+ real-world query examples${CLAUDE_SKILL_DIR}/references/graphql-patterns.md - Complete GraphQL reference| Skill | Plugin | When to Load |
|---|---|---|
| content-modeling | craft-developer | Content architecture planning, Matrix strategy |
| craft-5-migration | craft-developer | Breaking changes, upgrade patterns |
| craft-mcp | craft-developer | Direct database/schema inspection via MCP |
| livewires | live-wires | Frontend CSS when building Craft templates |
Official and third-party Claude Code plugins that complement this skill:
| Plugin | Tool | When to Use |
|---|---|---|
| context7 | /context7 | Live documentation for Craft CMS, Twig, Yii2 |
| playwright | Browser tools | Visual testing of rendered Craft templates |
| superpowers | /debug | Debug Craft template and query issues |
| figma | Design context tools | Extract design specs when building Craft templates |
When helping in Discord/forums:
.dd() or {% dd %} for debugging