Build Your First Custom Post Type
In this tutorial you will register a “Portfolio” custom post type, attach ACF fields to it, fetch the data in PHP, and display it in a Twig template.
By the end you will have a working single post template for the portfolio post type.
What you will build
Section titled “What you will build”- A
portfoliocustom post type - An ACF field group with an image, title, and description
- A Twig template that renders those fields
Prerequisites
Section titled “Prerequisites”- Lumbermill installed and running locally (Installation guide)
- ACF Pro active (ACF guide)
-
Register the post type
Add
PORTFOLIOtoPostTypeConstant:app/Constant/PostTypeConstant.php final class PostTypeConstant{const PORTFOLIO = 'portfolio';}Then open
config/custom-post-types.phpand add aportfolioentry:config/custom-post-types.php <?phpuse App\Constant\PostTypeConstant;use App\Lib\WordPress\PostTypeBuilder;return [PostTypeBuilder::make(PostTypeConstant::PORTFOLIO)->named('Portfolio'),];This registers the post type with sensible defaults. Use
PostTypeConstant::PORTFOLIOeverywhere you reference the slug. -
Create the ACF field group file structure
Create the following files inside
acf-php/post-types/:Directorylumbermill
Directoryacf-php
Directorypost-types
Directoryportfolio
- index.php
- header.php
-
Define the fields
Open
acf-php/post-types/portfolio/header.phpand define an image, title, and description field:acf-php/post-types/portfolio/header.php <?phpuse JorenRothman\ACFBuilder\FieldGroup;use JorenRothman\ACFBuilder\Fields\Basic\Text;use JorenRothman\ACFBuilder\Fields\Basic\Textarea;use JorenRothman\ACFBuilder\Fields\Content\Image;return createFieldGroup('Portfolio - Header', function (FieldGroup $fieldGroup) {$image = Image::make('Image');$title = Text::make('Title');$description = Textarea::make('Description');$fieldGroup->addField($image);$fieldGroup->addField($title);$fieldGroup->addField($description);}); -
Register the field group location
Open
acf-php/post-types/portfolio/index.phpand attach the field group to theportfoliopost type:acf-php/post-types/portfolio/index.php <?phpuse App\Constant\PostTypeConstant;use JorenRothman\ACFBuilder\FieldGroupLocations;$header = loadFieldGroup(__DIR__ . '/header.php');$location = new FieldGroupLocations;$location->and('post_type', '==', PostTypeConstant::PORTFOLIO);$header->register($location, 10);No wiring step needed —
acf-php/index.phpautoloads every nestedindex.php, sopost-types/portfolio/index.phpis picked up automatically. -
Create the PHP template
Create
single-portfolio.phpin the theme root to handle single portfolio posts:single-portfolio.php <?phpuse Timber\Timber;$context = Timber::context();$post = Timber::get_post();$context['post'] = $post;$context['header'] = ['image' => $post->meta('portfolio_header_image'),'title' => $post->meta('portfolio_header_title'),'description' => $post->meta('portfolio_header_description'),];Timber::render('post-types/portfolio/single.twig', $context); -
Create the Twig template
Create
views/post-types/portfolio/single.twig:{# views/post-types/portfolio/single.twig #}{% extends 'layouts/base.twig' %}{% block content %}<article class="portfolio-single">{% if header.image %}<imgsrc="{{ header.image.src }}"alt="{{ header.image.alt }}"class="portfolio-single__image"/>{% endif %}<h1 class="portfolio-single__title">{{ header.title }}</h1><p class="portfolio-single__description">{{ header.description }}</p></article>{% endblock %} -
Verify in WordPress admin
- Go to wp-admin → Portfolio → Add New
- You should see an “Image”, “Title”, and “Description” field group
- Fill in the fields, publish, and visit the post URL to confirm the template renders correctly
What’s next
Section titled “What’s next”- Create a page and assign a page template to list all portfolio posts
- Add more field types — see ACF Builder Fields
- Create a Timber class map to extend the Post object — see Class Maps

