Build Your First Page
In this tutorial you will create an “About” page template, attach a custom ACF field to it, and display it using Twig.
By the end you will understand how data flows from PHP into your Twig template — the core pattern behind every page in Lumbermill.
What you will build
Section titled “What you will build”- An
aboutpage template selectable from the WordPress admin - An ACF textarea field for an intro paragraph
- A Twig template that renders the page title and intro text
Prerequisites
Section titled “Prerequisites”- Lumbermill installed and running locally (Installation guide)
How it works
Section titled “How it works”Before creating any files, it helps to understand the pattern you are about to follow.
In a standard WordPress theme, a single PHP file handles both the data and the HTML. In Lumbermill these two concerns are split across two files:
| File | Role |
|---|---|
templates/about.php | Fetches data, builds a $context array, calls Timber::render() |
views/templates/about.twig | Receives the context and renders HTML — no PHP logic |
Every key you add to $context in PHP becomes a variable you can use in Twig. That handoff is the core pattern you will use on every page.
-
Create the PHP template
Create
templates/about.php. TheTemplate name:comment is what WordPress shows in the page editor’s template dropdown.templates/about.php <?phpuse Timber\Timber;/** Template name: About */$context = Timber::context();$context['post'] = Timber::get_post();Timber::render('templates/about.twig', $context);Timber::context()loads global data (site name, menus, etc.).Timber::get_post()fetches the current WordPress page. Storing it under the keypostmakes it available in Twig as{{ post }}. -
Create the Twig template
Create
views/templates/about.twig. Every template extendslayouts/base.twig— which handles the full HTML document, header, and footer — and fills in thecontentblock.{# views/templates/about.twig #}{% extends 'layouts/base.twig' %}{% block content %}<main class="about-page"><div class="container"><h1>{{ post.title }}</h1></div></main>{% endblock %}{{ post.title }}comes from$context['post']set in the previous step. You never callget_header()orget_footer()—layouts/base.twighandles that. -
Assign the template in WordPress
- Create a new page in wp-admin → Pages → Add New
- In the right sidebar under Page Attributes, open the Template dropdown
- Select About
- Publish the page and visit its URL — your template renders
-
Define an ACF field group
A field group is a collection of fields that ACF renders in the WordPress admin. You define which fields it contains in one file, and where in WordPress it should appear in a separate file.
Create the following files:
Directorylumbermill/
Directoryacf-php/
Directorytemplates/
Directoryabout/
- content.php
- index.php
Define the field in
acf-php/templates/about/content.php.createFieldGroup()sets up the group — everything you add to$fieldGroupwill appear as an input in the editor:acf-php/templates/about/content.php <?phpuse JorenRothman\ACFBuilder\FieldGroup;use JorenRothman\ACFBuilder\Fields\Basic\Textarea;return createFieldGroup('About - Content', function (FieldGroup $fieldGroup) {$introText = Textarea::make('Intro Text');$fieldGroup->addField($introText);}); -
Register the field group location
Register where this field group should appear in
acf-php/templates/about/index.php. The->and()call sets the location rule — here it targets pages using the About template:acf-php/templates/about/index.php <?phpuse JorenRothman\ACFBuilder\FieldGroupLocations;$content = loadFieldGroup(__DIR__ . '/content.php');$location = new FieldGroupLocations;$location->and('page_template', '==', 'templates/about.php');$content->register($location, 10);The first argument to
->and()is the location type. Common values arepage_template,post_type,page, andoptions_page. See the ACF guide for a full overview.ACF picks up the new folder automatically —
acf-php/index.phpautoloads every nestedindex.php, so there’s no wiring step. -
Fetch the field and pass it to context
Open
templates/about.phpand fetch the ACF field. The meta key is the field group name and field name joined with underscores and lowercased:about_content_intro_text.templates/about.php <?phpuse Timber\Timber;/** Template name: About */$context = Timber::context();$post = Timber::get_post();$context['post'] = $post;$context['intro_text'] = $post->meta('about_content_intro_text');Timber::render('templates/about.twig', $context);$context['intro_text']becomes{{ intro_text }}in Twig. Every key in$contextmaps directly to a Twig variable — this is always how data moves from PHP to your template. -
Render the field in Twig
Open
views/templates/about.twigand add the intro text:{# views/templates/about.twig #}{% extends 'layouts/base.twig' %}{% block content %}<main class="about-page"><div class="container"><h1>{{ post.title }}</h1>{% if intro_text %}<p class="about-page__intro">{{ intro_text }}</p>{% endif %}</div></main>{% endblock %}$post->meta()returnsnullwhen the field is empty, so{% if intro_text %}evaluates to false and skips the element — no empty<p>tags in the markup. -
Verify in WordPress admin
- Open the About page in wp-admin → Pages
- You should see an Intro Text field below the content editor
- Fill it in, update the page, and visit it — your intro text appears on the page
What’s next
Section titled “What’s next”- Add more field types — see the ACF guide
- Build a Custom Post Type — Build your first CPT
- Learn about the full context API — Context guide

