Skip to content

Twig for WordPress Developers

Twig is a templating language for PHP. In Lumbermill, all HTML lives in .twig files instead of PHP files. PHP handles data — Twig handles output.

If you have written WordPress themes with the_title(), the_content(), and get_field() directly in PHP template files, this guide will map what you already know onto how Twig works.


Standard WordPress mixes PHP logic and HTML in the same file. This works but gets messy fast. Twig enforces a clean split:

  • PHP file — fetches data, builds a $context array, calls Timber::render()
  • Twig file — receives the context and renders HTML, no PHP logic

This means templates are easier to read, and data flow is explicit — if a variable isn’t in $context, it won’t appear in the template.


Twig uses three types of tags:

TagPurposeExample
{{ }}Output a value{{ post.title }}
{% %}Logic (if, for, block, etc.){% if post.title %}
{# #}Comment (not rendered){# TODO: fix this #}

WordPress (PHP)Twig
the_title(){{ post.title }}
the_content(){{ post.content }}
the_permalink(){{ post.link }}
the_excerpt(){{ post.excerpt }}
get_the_ID(){{ post.id }}
the_post_thumbnail_url(){{ post.thumbnail.src }}
bloginfo('name'){{ site.name }}
bloginfo('url'){{ site.url }}
get_template_directory_uri(){{ theme.link }}
get_field('my_field')passed via $context in PHP, then {{ my_field }}

{% if post.title %}
<h1>{{ post.title }}</h1>
{% endif %}
{% if user %}
<p>Welcome, {{ user.name }}</p>
{% else %}
<p>You are not logged in.</p>
{% endif %}

{% for post in posts %}
<article>
<h2>{{ post.title }}</h2>
<p>{{ post.excerpt }}</p>
</article>
{% endfor %}

Use loop.index for the current iteration number (1-based):

{% for item in items %}
<p>{{ loop.index }}. {{ item.title }}</p>
{% endfor %}

This is the biggest difference from standard WordPress themes. Instead of including a header and footer at the top and bottom of every file, Twig uses inheritance.

views/layouts/base.twig defines the full HTML document with named blocks:

{# views/layouts/base.twig #}
<!doctype html>
<html>
<head>
<title>{{ site.name }}</title>
</head>
<body>
{% block content %}{% endblock %}
</body>
</html>

Every page template extends this layout and fills in the blocks:

{# views/templates/services.twig #}
{% extends 'layouts/base.twig' %}
{% block content %}
<main>
<h1>{{ post.title }}</h1>
{{ post.content }}
</main>
{% endblock %}

You never call get_header() or get_footer() — the base layout handles that.


To include a reusable partial:

{% include 'partials/card.twig' %}

Pass variables to the partial:

{% include 'partials/card.twig' with { title: post.title, image: post.thumbnail } %}

Variable not showing up in Twig

The variable was not added to $context in the PHP file.

// ❌ this won't be available in Twig
$my_posts = Timber::get_posts(['post_type' => 'news']);
// ✅ add it to context
$context['my_posts'] = Timber::get_posts(['post_type' => 'news']);

Trying to call PHP functions directly

Twig is not PHP. You cannot call arbitrary PHP functions inside {{ }}.

{# ❌ won't work #}
{{ get_field('my_field') }}
{# ✅ fetch in PHP, pass through context #}
{{ my_field }}

Printing HTML safely

By default, Twig escapes output. To render HTML stored in a variable, use the raw filter:

{{ post.content | raw }}

Only use raw on content you trust (e.g. WordPress post content, not user input).