Skip to content

Block Patterns

Føhn provides #[AsBlockPattern] for registering block patterns with Twig template support, along with Twig helpers to generate WordPress block markup comments.

Block Markup Helpers

Writing block comments manually is tedious. Føhn provides Twig functions to generate them:

twig
{# Instead of writing this: #}
<!-- wp:heading {"level":2} -->
<h2 class="wp-block-heading">Title</h2>
<!-- /wp:heading -->

{# You can write: #}
{{ wp_block_start('heading', { level: 2 }) }}
<h2 class="wp-block-heading">Title</h2>
{{ wp_block_end('heading') }}

{# Or use the shorthand: #}
{{ wp_block('heading', { level: 2 }, '<h2 class="wp-block-heading">Title</h2>') }}

Available Functions

FunctionDescription
wp_block_startOpening block comment with optional attrs
wp_block_endClosing block comment
wp_blockComplete block (open + content + close)

Examples

twig
{# Simple block #}
{{ wp_block_start('paragraph') }}
<p>Hello world</p>
{{ wp_block_end('paragraph') }}

{# Block with attributes #}
{{ wp_block_start('group', { layout: { type: 'constrained' } }) }}
<div class="wp-block-group">
    {{ wp_block('heading', {}, '<h2>Section Title</h2>') }}
</div>
{{ wp_block_end('group') }}

{# Namespaced blocks #}
{{ wp_block_start('theme/hero', { fullWidth: true }) }}
<div class="hero">...</div>
{{ wp_block_end('theme/hero') }}

Basic Block Pattern

php
<?php
// app/Patterns/HeroWithCta.php

namespace App\Patterns;

use Studiometa\Foehn\Attributes\AsBlockPattern;

#[AsBlockPattern(
    name: 'theme/hero-with-cta',
    title: 'Hero with CTA',
    categories: ['featured'],
)]
final class HeroWithCta {}

With the corresponding Twig template:

twig
{# patterns/hero-with-cta.twig #}
<!-- wp:cover {"dimRatio":50} -->
<div class="wp-block-cover">
    <div class="wp-block-cover__inner-container">
        <!-- wp:heading {"level":1} -->
        <h1 class="wp-block-heading">Welcome to our site</h1>
        <!-- /wp:heading -->

        <!-- wp:paragraph -->
        <p>Discover amazing content and services.</p>
        <!-- /wp:paragraph -->

        <!-- wp:buttons -->
        <div class="wp-block-buttons">
            <!-- wp:button -->
            <div class="wp-block-button">
                <a class="wp-block-button__link wp-element-button">Get Started</a>
            </div>
            <!-- /wp:button -->
        </div>
        <!-- /wp:buttons -->
    </div>
</div>
<!-- /wp:cover -->

Full Configuration

php
#[AsBlockPattern(
    name: 'theme/pricing-table',
    title: 'Pricing Table',
    categories: ['featured', 'pricing'],
    keywords: ['price', 'plans', 'subscription'],
    blockTypes: ['core/group'],
    description: 'A three-column pricing comparison table',
    viewportWidth: 1400,
    inserter: true,
)]
final class PricingTable {}

Dynamic Patterns

For patterns with dynamic content, implement BlockPatternInterface:

php
<?php
// app/Patterns/LatestPosts.php

namespace App\Patterns;

use Studiometa\Foehn\Attributes\AsBlockPattern;
use Studiometa\Foehn\Contracts\BlockPatternInterface;

#[AsBlockPattern(
    name: 'theme/latest-posts',
    title: 'Latest Posts Grid',
    categories: ['posts'],
)]
final class LatestPosts implements BlockPatternInterface
{
    public function compose(): array
    {
        $posts = \Timber\Timber::get_posts([
            'post_type' => 'post',
            'posts_per_page' => 3,
        ]);

        return [
            'posts' => $posts,
        ];
    }
}
twig
{# patterns/latest-posts.twig #}
<!-- wp:group {"layout":{"type":"constrained"}} -->
<div class="wp-block-group">
    <!-- wp:heading -->
    <h2 class="wp-block-heading">Latest Posts</h2>
    <!-- /wp:heading -->

    <!-- wp:columns -->
    <div class="wp-block-columns">
        {% for post in posts %}
        <!-- wp:column -->
        <div class="wp-block-column">
            <!-- wp:heading {"level":3} -->
            <h3 class="wp-block-heading">{{ post.title }}</h3>
            <!-- /wp:heading -->

            <!-- wp:paragraph -->
            <p>{{ post.excerpt }}</p>
            <!-- /wp:paragraph -->

            <!-- wp:buttons -->
            <div class="wp-block-buttons">
                <!-- wp:button {"className":"is-style-outline"} -->
                <div class="wp-block-button is-style-outline">
                    <a class="wp-block-button__link wp-element-button" href="{{ post.link }}">Read More</a>
                </div>
                <!-- /wp:button -->
            </div>
            <!-- /wp:buttons -->
        </div>
        <!-- /wp:column -->
        {% endfor %}
    </div>
    <!-- /wp:columns -->
</div>
<!-- /wp:group -->

Pattern Categories

Patterns use standard WordPress categories or custom ones:

Standard Categories

  • featured - Featured patterns
  • posts - Post-related patterns
  • text - Text patterns
  • gallery - Gallery patterns
  • call-to-action - CTA patterns
  • banner - Banner patterns
  • header - Header patterns
  • footer - Footer patterns
  • columns - Column layouts

Custom Categories

Register custom categories using filters:

php
<?php

namespace App\Hooks;

use Studiometa\Foehn\Attributes\AsFilter;

final class PatternHooks
{
    #[AsFilter('block_pattern_categories_all')]
    public function registerCategories(array $categories): array
    {
        $categories['theme-layouts'] = [
            'label' => 'Theme Layouts',
            'description' => 'Custom layouts for this theme',
        ];

        return $categories;
    }
}

Template Path Resolution

By default, patterns look for templates in patterns/:

  • theme/hero-with-ctapatterns/hero-with-cta.twig
  • theme/pricing-tablepatterns/pricing-table.twig

Custom path:

php
#[AsBlockPattern(
    name: 'theme/hero',
    title: 'Hero',
    template: 'my-patterns/hero-banner',
)]

Examples

Two Column Text

twig
{# patterns/two-column-text.twig #}
<!-- wp:columns -->
<div class="wp-block-columns">
    <!-- wp:column -->
    <div class="wp-block-column">
        <!-- wp:heading -->
        <h2 class="wp-block-heading">Column One</h2>
        <!-- /wp:heading -->

        <!-- wp:paragraph -->
        <p>Add your content here. This pattern creates a two-column layout.</p>
        <!-- /wp:paragraph -->
    </div>
    <!-- /wp:column -->

    <!-- wp:column -->
    <div class="wp-block-column">
        <!-- wp:heading -->
        <h2 class="wp-block-heading">Column Two</h2>
        <!-- /wp:heading -->

        <!-- wp:paragraph -->
        <p>Add your content here. Edit this pattern to match your needs.</p>
        <!-- /wp:paragraph -->
    </div>
    <!-- /wp:column -->
</div>
<!-- /wp:columns -->

Call to Action

twig
{# patterns/call-to-action.twig #}
<!-- wp:group {"backgroundColor":"primary","textColor":"white","layout":{"type":"constrained"}} -->
<div class="wp-block-group has-white-color has-primary-background-color has-text-color has-background">
    <!-- wp:heading {"textAlign":"center"} -->
    <h2 class="wp-block-heading has-text-align-center">Ready to get started?</h2>
    <!-- /wp:heading -->

    <!-- wp:paragraph {"align":"center"} -->
    <p class="has-text-align-center">Join thousands of satisfied customers today.</p>
    <!-- /wp:paragraph -->

    <!-- wp:buttons {"layout":{"type":"flex","justifyContent":"center"}} -->
    <div class="wp-block-buttons">
        <!-- wp:button {"backgroundColor":"white","textColor":"primary"} -->
        <div class="wp-block-button">
            <a class="wp-block-button__link has-primary-color has-white-background-color has-text-color has-background wp-element-button">Sign Up Now</a>
        </div>
        <!-- /wp:button -->
    </div>
    <!-- /wp:buttons -->
</div>
<!-- /wp:group -->

File Structure

app/Patterns/
├── HeroWithCta.php
├── PricingTable.php
├── LatestPosts.php
└── CallToAction.php

patterns/
├── hero-with-cta.twig
├── pricing-table.twig
├── latest-posts.twig
└── call-to-action.twig

Attribute Parameters

ParameterTypeDefaultDescription
namestringrequiredPattern name with namespace
titlestringrequiredDisplay title
categoriesstring[][]Pattern categories
keywordsstring[][]Search keywords
blockTypesstring[][]Associated block types
description?stringnullPattern description
template?stringAuto-resolvedTemplate path
viewportWidthint1200Preview viewport width
inserterbooltrueShow in inserter

See Also

Released under the MIT License.