Introduction
With version 5.0, WordPress introduced a new post editing experience called Gutenberg. Instead of a rich
text writing flow,
the new editor prioritizes blocks to design and create content.
Creating custom blocks is not a trivial task, as Javascript is now needed on all steps to do so. As of now, there is no
native way to create custom blocks using PHP, the programming language that WordPress is built on.
Advanced Custom
Fields, WordPress' most popular custom field solution, launched their own wrapper to create
custom blocks using PHP with version 5.8. Here at Fabrikat,
we were so blown away by the possibilities of this solution that we decided to build an internal
framework that makes composing ACF blocks easier and quicker. The result is Blockstudio. (BS)
Under the hood
BS is making heavy use of the acf_register_block_type function. This means that every block made
with BS is an ACF block under the hood, exposed to the same block attributes and settings that are
available to ACF blocks not made with BS.
Motivation
Recent
discussions on WordPress vs Jamstack show that developers are increasingly favouring new
ways of working. Components have made a big impact on the way sites and apps are built in the
current day, with React, Vue and Angular
revolutionising a jQuery past.
While the bare functionality of BS is not forcing any of those methodologies on it's users,
the accompanying library is trying to find a middle ground between the old and the new and is thus
making use of one framework: Alpine
JS.
It's declarative and reactive nature brings the benefits of the libraries mentioned above to any DOM
and is thus perfect for a new WordPress world consisting of blocks.
Why ACF blocks
You might wonder at this stage, why bother with Advanced Custom Fields for block creation when you
could just use WordPress core? While we have been toying with React based blocks for a bit,
constructing new elements with ACF has one big advantage over the "WordPress way":
Dynamic blocks don't need to duplicate the rendering logic in Javascript and
PHP for each block. This is huge, especially when the list of blocks is constantly growing. There is
a healthy
discussion on this topic in the Gutenberg GitHub if you want to learn more about the issue.
Since the single file component approach is so central to BS, using ACF blocks is the best
option out there right now. Of course, we will keep our ears open to new developments and extend BS
to use new APIs where we see fit!
Acknowledgements
The blocks creation process is heavily inspired by the work of Maciej
Palmowski and his Timber
WP blocks plugin.
Creating blocks
Composing your own blocks with BS is extremely easy. The plugin will look for a blockstudio
folder within your currently activated theme. Inside of it, every .php file will create a new block
by making use of the file header information. Let's take a look at one of the blocks that is being
used on the landing page:
<?php
// cta.php
/*
Title: Call to Action
Description: Call to action element.
Category: blockstudio
Icon: lightbulb
Mode: preview
SupportsAlign: false
SupportsMode: false
*/
The above is the equivalent of this code:
add_action( 'acf/init', 'my_register_blocks' );
function register_cta_block() {
if( function_exists( 'acf_register_block_type' ) ) {
register_cta_block(array(
'name' => 'cta',
'title' => __('Call to Action'),
'description' => __('Call to action element.'),
'category' => 'blockstudio',
'icon' => 'lightbulb',
'mode' => 'preview',
'supports' => array(
'align' => false,
'mode' => false,
),
));
}
}
While both of these examples will register a block, nothing is being rendered yet. Instead of relying
on callbacks or templates, markup in BS is being defined in the same file like so:
<?php
// cta.php
/*
Title: Call to Action
Description: Call to action element.
Category: blockstudio
Icon: lightbulb
Mode: preview
SupportsMode: false
SupportsAlign: false
*/
?>
<p>Buy my product!</p>
And that's it, we made our first block! Just like ACF itself, every filename can only contain
lowercase alphanumeric characters and dashes, and must begin with a letter. We are working hard to
support all parameters that ACF is providing, at the moment following settings are
allowed/working:
<?php
/*
Title: Testimonial
Description: Content block for customer testimonials.
Category: marketing
Icon: smiley
Keywords: testimonial customer content
Mode: preview
Align: full
PostTypes: page
SupportsMode: false
SupportsAlign: false
SupportsMultiple: false
SupportsJSX: false
*/
?>
If you need to access more advanced features with PHP, it is also possible to register blocks using
the $register variable:
<?php
$register = array(
'title' => 'First Block',
'title' => __( 'First Block' ),
'category' => 'formatting',
'icon' => 'design',
);
?>
<p>My Markup</p>
Alternatively, it is possible to add settings via PHP while maintaining settings set with the file
header using the $settings variable:
<?php
// cta.php
/*
Title: Call to Action
Description: Call to action element.
Category: blockstudio
Icon: lightbulb
Mode: preview
SupportsMode: false
SupportsAlign: false
*/
$settings = array(
'text-align' => 'left'
);
?>
<p>Buy my product!</p>
Subfolders
Since BS is built on ACF, every block will automatically be registered under the acf
namespace. In our example above, the block name would have been acf/cta.
It is also possible to create blocks using folders within the Blockstudio folder. To avoid
duplicate block names and errors, every block within a subfolder will be prefixed with the folders
name. For example, a file called testimonial.php inside a marketing subfolder will
have acf/marketing-testimonial as it's block name.
Unlimited nested folders are supported.
Prefix
It may make sense to prefix all blocks if you already have an existing ACF blocks library. The blockstudio/prefix
filter does exactly that:
<?php
add_filter( 'blockstudio/prefix', function () {
return 'bs';
} );
The code above would convert the block name from acf/my-block to
acf/bsx-my-block. The filter is used internally for the block library with a
blockstudio prefix, so avoid this particular prefix.
Custom paths
Changing the location of the folder that is going to be searched for blocks might be necessary, for
example if you are using a page builder like Oxygen, which disables the theme altogether. Use the
blockstudio/path filter to do so:
<?php
// Custom path within your theme.
add_filter( 'blockstudio/path' , function () {
return get_template_directory() . '/blocks';
} );
// Custom path within a plugin.
add_filter( 'blockstudio/path' , function () {
return plugins_url . '/my-custom-plugin/blocks';
} );
Multiple instances
If the above options are not enough, it is possible to initiate the Blockstudio\Build class on
various folders of your choice:
<?php
add_action( 'acf/init', function () {
// Blockstudio\Build::init( $folder, $prefix );
Blockstudio\Build::init( 'client-blocks', 'client' );
Blockstudio\Build::init( 'documentation-blocks', 'documentation' );
} );
Using different prefixes is highly advisable in this case, as it'll make sure that no two block names
will be the same.
Defining fields
Of course, the real usefulness of blocks come from connecting custom fields to them. This way, blocks
can be infinitely reused with different settings or texts.
While defining fields via the ACF interface is completely fine, BS includes a handy way to
register fields in the block files like so:
<?php
// cta.php
/*
Title: Call to Action
Description: Call to action element.
Category: design
Icon: lightbulb
Mode: preview
SupportsAlign: false
SupportsMode: false
*/
$fields = array(
array(
'key' => 'cta_title_key',
'label' => 'Title',
'name' => 'cta_title',
'type' => 'text',
),
array(
'key' => 'cta_text_key',
'label' => 'Text',
'name' => 'cta_text',
'type' => 'textarea',
),
);
?>
<h1><?php echo get_field( 'cta_title' ) ?: 'Title'; ?></h1>
<p><?php echo get_field( 'cta_text' ) ?: 'Text'; ?></p>
Blockstudio is automatically going to look for a $fields variable inside your files and if
available, will register those and connect them to the block. All of the settings that ACF
offers when registering fields via PHP are available here.
Previews
Previews make it possible to show the block in the inserter panel on hover. This feature behaves
similar to the field registration and has to be defined in an $example variable inside your
block file. All field data is called with $block['data'] or get_field() is
available. Below is an example of the heading block used on the landing page.
<?php
/*
Title: Heading
Description: Simple heading.
Category: blockstudio
Icon: editor-paragraph
Mode: preview
SupportsAlign: false
SupportsMode: false
*/
$fields = array(
array(
'key' => 'heading_heading_key',
'label' => 'Heading',
'name' => 'heading_heading',
'type' => 'text',
),
array(
'key' => 'heading_text_key',
'label' => 'Text',
'name' => 'heading_text',
'type' => 'text',
),
);
$example = array(
'attributes' => array(
'mode' => 'preview',
'data' => array(
'heading_heading' => "Title",
'heading_text' => "Text"
)
)
);
$heading = get_field( 'heading_heading' );
$text = get_field( 'heading_text' );
?>
<div class="<?php echo $block['blockstudio_center']; ?>">
<?php if ( $heading ): ?>
<h1 class="<?php echo $block['blockstudio_text_2xl']; ?>">
<?php echo $heading; ?>
</h1>
<?php endif; ?>
<?php if ( $text ): ?>
<p class="<?php echo $block['blockstudio_text_md']; ?> bsx-text-gray-600 2xl:bsx-max-w-none inter">
<?php echo $text; ?>
</p>
<?php endif; ?>
</div>
Defaults
It is possible to set global defaults for all blocks registered with Blockstudio using the blockstudio/defaults
filter:
<?php
add_filter( 'blockstudio/defaults', function () {
return array(
'post_types' => array( 'post' ),
);
} );
The above code will limit all BS blocks to posts. Setting the post type on individual blocks will
override the global setting.
Generating classes
The classFilter is an essential part of Blockstudio,
since it is able to automatically generate cohesive CSS classes for your blocks.
<div class="<?php echo Blockstudio\Library::classFilter( $block, $name, $type, $modifier, $post_id ) ?>">Hello</div>
<!-- Follows BEM naming structure.
<!-- Block scoped and global class without prefix. -->
<div class="block-name__element-name--modifier"/>
<div class="block-name__element-name--modifier"/>
<!-- Block scoped and global class with prefix. -->
<div class="prefix-block-name__element-name--modifier"/>
<div class="prefix-block-name__element-name--modifier"/>
The function accepts following parameters:
- $block: this is the ACF block array and required as it will use the
block prefix and name for the generation of the class.
-
$name: the main name for the class and required as well. It makes sense to pick a
descriptive term like 'container' or 'h1' here. This parameter can also be an array and generate
multiple classes.
-
$type: string to indicate what kind of classes should be generated. choice between 'all',
'global' or 'block'. (optional, defaults to 'all')
-
$modifiers: an array of strings that will be added as a modifier to the end of the
class name. (optional, one class per array entry)
-
$post_id: passing this will add the post ID to the filter, making it possible to
reference it in your filter actions. (optional)
Let's illustrate the above with a simple hero block:
<?php
// functions.php
add_filter( 'blockstudio/prefix', function () {
return 'twenty';
} );
// hero.php
/*
Title: Hero
Description: Hero block.
Category: design
Icon: cover-image
Mode: preview
SupportsAlign: false
SupportsMode: false
*/
$fields = array(
array(
'key' => 'layout_field',
'name' => 'layout',
'type' => 'checkbox',
'choices' => array(
'full-height' => 'full-height',
'full-width' => 'full-width',
),
)
);
$modifier = get_field( 'layout' );
?>
<div class="<?php echo Blockstudio\Library::classFilter( $block, 'wrapper', 'block', $modifier ) ?>">
<div class="<?php echo Blockstudio\Library::classFilter( $block, 'text' ) ?>">
<h1 class="<?php echo Blockstudio\Library::classFilter( $block, 'h1' ) ?>">My hero headline</h1>
<p class="<?php echo Blockstudio\Library::classFilter( $block, 'p' ) ?>">My hero text</p>
</div>
</div>
With the full-width checkbox checked in the editor, the following markup would be generated on
the frontend:
<div class="twenty-hero__wrapper--full-width twenty-hero__wrapper">
<div class="twenty__text twenty-hero__text">
<h1 class="twenty__h1 twenty-hero__h1">My hero headline</h1>
<p class="twenty__p twenty-hero__p">My hero text</p>
</div>
</div>
Adding classes
While the CSS class generation might be the perfect solution for some, others might rather apply
utility classes to all elements marked with the classFilter. This is done by hooking into
the acf/register_block_type_args filter, which enables the addition of new
data to the blocks.
Let's add some classes to the hero element from before:
<?php
add_filter( 'acf/register_block_type_args', function ( $args ) {
// Only add arguments for blocks created with Blockstudio.
$blocks = array(
'acf/twenty-hero',
);
if ( in_array( $args['name'], $blocks ) ) {
// List of classes.
$args['blockstudio/user/twenty/class/block/hero/wrapper'] = 'px-8 py-24 mx-auto max-w-screen-sm lg:px-16 lg:max-w-screen-2xl';
$args['blockstudio/user/twenty/class/text'] = 'break-word space-y-6 md:space-y-8';
$args['blockstudio/user/twenty/class/h1'] = 'text-4xl font-semibold tracking-tight md:text-5xl lg:text-6xl';
$args['blockstudio/user/twenty/class/p'] = 'text-sm leading-relaxed md:text-base lg:text-lg lg:leading-relaxed';
return $args;
}
} );
The classes will be applied automatically to the specific blocks or to all elements with the same
filter name.
<div class="twenty-hero__wrapper--full-width twenty-hero__wrapper px-8 py-24 mx-auto max-w-screen-sm lg:px-16 lg:max-w-screen-2xl">
<div class="twenty__text twenty-hero__text break-word space-y-6 md:space-y-8">
<h1 class="twenty__h1 twenty-hero__h1 text-4xl font-semibold tracking-tight md:text-5xl lg:text-6xl">My hero headline</h1>
<p class="twenty__p twenty-hero__p text-sm leading-relaxed md:text-base lg:text-lg lg:leading-relaxed">My hero text</p>
</div>
</div>
<?php
// Creates global and block classes.
Blockstudio\Library::classFilter( $block, 'h1' );
// Works.
$args['blockstudio/user/twenty/class/h1'] = 'text-4xl font-semibold tracking-tight md:text-5xl lg:text-6xl';
$args['blockstudio/user/twenty/class/block/hero/h1'] = 'text-primary-600';
// Creates global classes.
Blockstudio\Library::classFilter( $block, 'wrapper', 'global' );
// Works.
$args['blockstudio/user/twenty/class/wrapper'] = 'bg-primary-50';
// Won't work.
$args['blockstudio/user/twenty/class/block/hero/wrapper'] = 'px-8 py-24 mx-auto max-w-screen-sm lg:px-16 lg:max-w-screen-2xl';
// Creates block type specific classes.
Blockstudio\Library::classFilter( $block, 'wrapper', 'block' );
// Works.
$args['blockstudio/user/twenty/class/block/hero/wrapper'] = 'px-8 py-24 mx-auto max-w-screen-sm lg:px-16 lg:max-w-screen-2xl';
// Won't work.
$args['blockstudio/user/twenty/class/wrapper'] = 'bg-primary-50';
// Creates ID specific classes.
Blockstudio\Library::classFilter( $block, 'wrapper' );
$args['blockstudio/user/twenty/class/id/block_5fad1acc5c9c4/wrapper'] = 'h-screen';
// Creates modifier classes.
Blockstudio\Library::classFilter( $block, 'wrapper', 'block', ['full-screen'] );
$args['blockstudio/user/twenty/class/block/hero/wrapper/full-screen'] = 'bg-primary-600';
Filter classes
As the name of the function suggests, custom filters
are created for every instance of the function. Here, the names correlate to it's $args
counterpart from before. Let's apply a h-screen class to the wrapper element of our
previous hero block:
<?php
add_filter( 'blockstudio/user/twenty/class/block/hero/wrapper', function ( $class ) {
return $class . ' h-screen';
} );
The filter accepts a $class parameter, which represents the current classes. Three
different filters are generated when using the function:
<?php
// Global filter - will add the class to all instances where 'wrapper' name was used.
// Keep in mind that this won't work when the $type parameter is false.
add_filter( 'blockstudio/user/twenty/wrapper', function ( $class ) {
return $class . ' h-screen';
} );
// Block filter - will only add the class to a specific block.
add_filter( 'blockstudio/user/twenty/class/block/hero/wrapper', function ( $class ) {
return $class . ' w-full';
} );
// ID filter - will add the class to a specific block id.
add_filter( 'blockstudio/user/twenty/class/id/block_5fad1acc5c9c4/wrapper', function ( $class ) {
return $class . ' px-20';
} );
// If the $modifier parameter is set, additional filters are generated depending on the modifier names.
add_filter( 'blockstudio/user/twenty/class/block/hero/wrapper/full-width', function ( $class ) {
return $class . ' h-screen';
} );