Skip to content

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.

  • A portfolio custom post type
  • An ACF field group with an image, title, and description
  • A Twig template that renders those fields

  1. Register the post type

    Add PORTFOLIO to PostTypeConstant:

    app/Constant/PostTypeConstant.php
    final class PostTypeConstant
    {
    const PORTFOLIO = 'portfolio';
    }

    Then open config/custom-post-types.php and add a portfolio entry:

    config/custom-post-types.php
    <?php
    use 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::PORTFOLIO everywhere you reference the slug.

  2. 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
  3. Define the fields

    Open acf-php/post-types/portfolio/header.php and define an image, title, and description field:

    acf-php/post-types/portfolio/header.php
    <?php
    use 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);
    });
  4. Register the field group location

    Open acf-php/post-types/portfolio/index.php and attach the field group to the portfolio post type:

    acf-php/post-types/portfolio/index.php
    <?php
    use 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.php autoloads every nested index.php, so post-types/portfolio/index.php is picked up automatically.

  5. Create the PHP template

    Create single-portfolio.php in the theme root to handle single portfolio posts:

    single-portfolio.php
    <?php
    use 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);
  6. 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 %}
    <img
    src="{{ 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 %}
  7. 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
  • 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