Skip to main content
← All articles
craft cms

Same Twig, different planet: what a Drupal themer unlearns in Craft

Same Twig, different planet: what a Drupal themer unlearns in Craft

Craft and Drupal both theme with Twig, which lulls a Drupal developer into thinking the front end will feel familiar. It doesn't. Same language, different planet. In Drupal, Twig is the last, dumb layer that renders what a mountain of PHP — preprocess hooks, render arrays, the theme system — has already prepared. In Craft, the template is the layer: it queries content directly, decides access, sets headers, and owns its own caching. Here's what a Drupal themer actually has to unlearn.

Templates are routes now

Craft's template resolution is just the filesystem. Paths are relative to templates/, the .twig extension is optional, and index.twig is addressable by its directory name. A section or an entry points at a template through its settings; a parameterized route can point straight at a template with no controller at all. Underscore-prefixed templates aren't directly routable (that's the privateTemplateTrigger convention), a templates/de/ folder overrides shared templates per site, and a secondary extension like .vcf.twig even sets the response Content-Type. And — the line that sums it up — "PHP code isn't allowed in templates."

So the mental model flips. In Drupal, a route goes to a controller, which returns a render array, which gets preprocessed, which finally hits a template. In Craft, the URL often hits the template directly and the template does the work. Craft's own tags make that explicit: alongside the usual Twig, you get {% requireLogin %}, {% requirePermission %}, {% redirect %}, {% header %}, {% cache %}, {% nav %}, {% paginate %}. A Drupal themer reads "access control and HTTP headers, in a template," and their instinct screams "that belongs in a route or a controller!" In Craft, the template is the controller and the view, fused.

What you unlearn

Concretely, a pile of Drupal muscle memory just… doesn't apply. There's no hook_preprocess_node(), no theme suggestions, no render arrays, no {{ content|without(...) }} to surgically remove a field, no {{ attach_library() }}. (I'm describing the Drupal side from memory rather than fresh docs, so take the specifics as "roughly this.") The closest honest analogy: a Craft template is Drupal Views plus a template, collapsed into one file — it runs the query and renders the result, right there.

What you gain is real: readability and speed of building. You open a template, you see the query, you see the markup, and there's no preprocess indirection to trace. For a lot of day-one work it's genuinely more pleasant than Drupal's layered ceremony.

Drupal's theme layer is ceremony that protects you; Craft's is freedom that trusts you. On day one the freedom feels great. On day ninety, the discipline the ceremony used to handle is now yours.

The discipline that moves to you: queries and caching

Two things in particular become your problem. First, N+1 queries. Because templates query content directly, a loop like {% for post in posts %}{{ post.featureImage.one() }}{% endfor %} fires one query per item — 50 entries, 51 queries. Craft's answer is eager-loading, and it's explicit and manual: .with(['topics.thumbnail']) batches nested relations, and Craft 5 added lazy eager-loading with .eagerly() (post.featureImage.eagerly().one()), which the docs suggest most projects should just reach for by default. Nothing warns you if you forget; you profile with the debug toolbar and fix it yourself. In Drupal, Views mostly handled this behind the UI.

Second, caching, and this is the sharpest contrast with everything I've written about Drupal's cache system. Craft's {% cache %} tag caches output, not logic. It auto-invalidates when an element inside it is saved or deleted, which is lovely. But there's no bubbling, no cache contexts, no per-user variation — if you don't pass using key, "a random key will be generated each time Twig re-parses the template," and you're warned in plain text not to cache csrfInput() or forms inside it. Compare that to Drupal, where cache tags, contexts, and max-age bubble up the render tree automatically, and the system itself stops you from serving one user's cached HTML to another. Craft's model is the opposite trade-off: dead simple and explicit, with none of Drupal's automatic correctness. You get a faster mental model and the responsibility for not caching something per-user that shouldn't be.

The honest verdict

Craft's Twig is a nicer place to build a front end on day one — flat, direct, readable, no preprocess maze. But Drupal's heavier theme layer was doing real work you didn't see: isolating logic in preprocess, and getting cache correctness right so you couldn't leak content between users. In Craft, both of those jobs quietly become yours. The language is the same; the contract is not. Learn the eager-loading reflex early and treat {% cache %} with the respect you'd give a per-user render in Drupal, and Craft's front end is a genuine pleasure. Skip that, and you'll meet the discipline you unlearned — as a slow page, or worse, a cached one showing the wrong thing.

Links

BM
Blue Moose
The moose behind Blue Moose. Full-stack PHP developer — Drupal by day, Symfony by night, tests always.