skills/craftcms-ops/SKILL.md
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.
npx skillsauth add 0xDarkMatter/claude-mods craftcms-opsInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
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.
| Concept | What it is | Craft 5 change |
|---------|-----------|----------------|
| Section | Container exposing entry types + URL rules | Three kinds: Single, Channel, Structure |
| Entry Type | Atomic unit of content (fields, title, slug) | Now global + reusable across sections, with per-section aliases |
| Entry | An instance of an entry type | Can be top-level or nested (inside Matrix/CKEditor) |
| Field | Reusable input attached via field layouts | Globally reusable — no per-field-instance duplication |
| Matrix field | Repeatable nested content | Now stores entries (entry types), not "blocks". Nesting supported natively |
| Project Config | Version-controlled schema (config/project/) | Source of truth for sections/fields/settings |
| Type | Use for | Has URLs? | Hierarchy? | |------|---------|-----------|-----------| | Single | One-off pages (home, about) | Optional fixed URI | No | | Channel | Streams (blog, news, products) | Yes, per-entry-type URI format | No | | Structure | Nested/ordered content (docs, nav) | Yes | Yes (drag-to-order, levels) |
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() %}
| Need | Method |
|------|--------|
| 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.
| Pattern | Rule |
|---------|------|
| Private templates | Prefix 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 templates | Push business logic to a module/plugin service, not Twig |
| Caching | {% cache %} — only after queries are optimized, never to mask N+1 |
Craft ships a GraphQL API for decoupled frontends (Next.js, Nuxt, Astro, etc.).
| Concern | Approach | |---------|----------| | Schema | Define GraphQL schemas + scopes in Control Panel; generate a token per schema | | Auth | Bearer token per schema; public schema for anonymous reads | | Alternative | Element API plugin for custom JSON endpoints when GraphQL is overkill | | CORS | Configure allowed origins for the headless frontend | | Eager loading | GraphQL 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.
| Symptom | Fix |
|---------|-----|
| Slow listing pages | Eager-load with .with([...]) — the #1 Craft perf bug is N+1 inside loops |
| Repeated identical render | {% cache %} tag (after query optimization) |
| Whole-site cache needed | Blitz plugin (static page caching, granular invalidation) |
| Slow orderBy on custom field | Ensure the underlying column/field is indexed |
| Heavy asset transforms | Pre-generate transforms; use Imgix/CDN |
config/project/*.yaml) is the version-controlled source of truth for sections, fields, entry types, settings. Commit it.php craft up (runs migrations + applies project config)..env and config/general.php (use App::env() / getenv()).| Area | What changed | Action |
|------|--------------|--------|
| Matrix | Blocks → entries with entry types | Templates iterating .type.handle mostly survive; re-check block-type field handles |
| Fields | Now globally reusable | Expect field/entry-type proliferation post-upgrade — consolidate duplicates |
| Content storage | Reworked internal storage | Run php craft up; test queries on staging |
| PHP/DB | Craft 5 needs PHP 8.2+ | Verify host before upgrading |
| Plugins | Many need a Craft 5-compatible release | Audit plugin compatibility first |
Full upgrade guidance: https://craftcms.com/docs/5.x/upgrade.html
| Gotcha | Why | Fix |
|--------|-----|-----|
| N+1 queries in loops | Element relations lazy-load | Always .with([...]) before iterating |
| {% cache %} masking slow queries | Cache hides, doesn't fix | Optimize queries first, cache second |
| Business logic in Twig | Hard to test/reuse | Move to a module/plugin service |
| Project Config drift in teams | Out-of-band CP edits | Treat config/project/ as source of truth; php craft up on deploy |
| Untested migrations to prod | Data loss risk | Test on staging clone first |
| Over-using Matrix | Complexity + perf cost | Use simpler structures when nesting isn't needed |
| Calling old "Matrix block" APIs | Removed in Craft 5 | Use entry/entry-type APIs |
| File | Use |
|------|-----|
| assets/entry-type-field-layout.md | Annotated content-modeling starter: section + entry type + field layout + Matrix-as-entries shape, mapped to Project Config |
laravel-ops — shared PHP/Composer/Twig-adjacent tooling, Eloquent patterns for comparisonsql-ops — index strategy behind slow orderBy/relation queriesnginx-ops — serving Craft, caching headers, reverse proxy for headlesstools
yt-dlp operations - the media ACQUISITION layer that feeds ffmpeg-ops: format selection (-S sort vs -f filters) that avoids post-download transcodes, --download-sections clip-at-download, audio-only extraction for STT pipelines (-x --audio-format opus), playlists + --download-archive incremental channel syncs, cookies/auth (--cookies-from-browser), rate limiting and politeness, SponsorBlock mark/remove, output templates (-o), subtitle download (--write-subs/--write-auto-subs), remux-vs-recode doctrine, and failure triage (403s, throttling, geo blocks, the nsig-extraction class that means yt-dlp is outdated). Triggers on: yt-dlp, ytdlp, youtube-dl, download video, download youtube, download from youtube, download playlist, download channel, archive channel, channel sync, rip audio, youtube to mp3, youtube to mp4, save video, grab video, video downloader, download subtitles, download transcript, clip from youtube, download section, sponsorblock, cookies-from-browser, download-archive, nsig, requested format is not available, sign in to confirm, download livestream, record stream, live-from-start, premiere, impersonate.
tools
Comprehensive ffmpeg/ffprobe operations - probe-first media processing: transcode and compress (H.264/H.265/AV1/Opus), frame-accurate cut/trim/concat, EDL-driven editing, color grading and .cube LUTs, audio loudnorm and mixing, STT/Whisper audio prep, subtitles, GIF and thumbnails, HLS packaging, hardware encoding (NVENC/QSV/AMF/VideoToolbox), restoration, scene and silence detection, VMAF quality gates, screen capture, yt-dlp interop. Triggers on: ffmpeg, ffprobe, transcode, convert video, compress video, encode video, extract audio, trim video, cut video, concat videos, video to gif, thumbnail, contact sheet, burn subtitles, watermark, resize video, crop video, change fps, slow motion, timelapse, loudnorm, normalize audio, audio for whisper, transcription prep, scene detection, silence detection, remove silence, color grade, LUT, tonemap HDR, vmaf, nvenc, hardware encode, hls, remux, faststart, deinterlace, stabilize video, denoise video, screen record, EDL, keyframes.
development
Payload CMS 3 (Next.js-native) architecture - collections, globals, fields, access control, hooks, Local API, storage adapters, and database (Postgres/MongoDB/SQLite). Use for: payload, payloadcms, payload cms, payload 3, collection config, access control, payload hooks, local api, payload fields, multi-tenant payload, payload nextjs, payload s3, payload r2, payloadcms architecture, headless cms typescript.
testing
Cypress end-to-end and component testing operations - selector/retry-ability strategy, cy.intercept network stubbing, cy.session auth, component vs e2e, flake diagnosis, CI, Test Replay. Use for: cypress, e2e test, component test, cy.get, cy.intercept, cy.session, data-cy, data-test, retry-ability, flake, flaky test, cypress.config, cy.mount, Test Replay, custom commands, fixtures.