Building an SCF block with block.json, step by step
Every section on this site’s homepage is a block built with Secure Custom Fields (SCF, the community fork of ACF). Here’s the full pattern for one SCF block, a call to action, from empty folder to working block. If you’re still choosing between this approach and native React blocks, start with SCF blocks vs React blocks.
1. One folder per block
blocks/
cta/
block.json
render.php
style.css
Keeping the metadata, template and styles together means a block can be copied to another project as one folder.
2. block.json
{
"$schema": "https://schemas.wp.org/trunk/block.json",
"apiVersion": 3,
"name": "rp/cta",
"title": "Call to Action",
"category": "rp-portfolio",
"icon": "megaphone",
"supports": {
"anchor": true,
"align": [ "full" ],
"html": false,
"mode": false
},
"style": [ "file:./style.css" ],
"acf": {
"blockVersion": 3,
"mode": "preview",
"renderTemplate": "render.php"
}
}
The acf key is what turns it into an SCF block. blockVersion: 3 uses the current block API, and "mode": false in supports hides the edit/preview switch, so authors always see the real design and edit fields in the sidebar.
style loads the CSS only on pages that use the block. With should_load_separate_core_block_assets enabled, WordPress inlines small block styles, so there are no extra requests either.
3. Register it
One loop registers every block in the folder:
add_action( 'init', function () {
foreach ( glob( get_template_directory() . '/blocks/*/block.json' ) as $file ) {
register_block_type( dirname( $file ) );
}
} );
4. Fields
Create a field group with the location rule Block is equal to Call to Action. For this block: heading (text), text (textarea), primary_cta and secondary_cta (link fields).
Save field groups as local JSON by pointing acf/settings/save_json at a folder in your theme. The fields then live in version control alongside the template, and deploy with it.
5. render.php
<?php
$heading = (string) get_field( 'heading' );
$primary = get_field( 'primary_cta' );
if ( ! $heading && ! $is_preview ) {
return;
}
?>
<section <?php echo get_block_wrapper_attributes( array( 'class' => 'block-cta' ) ); ?>>
<h2 class="block-cta__heading"><?php echo esc_html( $heading ); ?></h2>
<?php if ( $primary ) : ?>
<a class="button" href="<?php echo esc_url( $primary['url'] ); ?>"
<?php echo $primary['target'] ? 'target="_blank" rel="noopener"' : ''; ?>>
<?php echo esc_html( $primary['title'] ); ?>
</a>
<?php endif; ?>
</section>
Three details that matter:
get_block_wrapper_attributes()adds the anchor, alignment and custom class from the editor.- Every value is escaped on output. Field values are user input. See escaping output in WordPress themes.
- Empty blocks print nothing on the front end, but still show in the editor (
$is_preview) so they can be found and filled in.
One trap: inside a block template, get_field( 'name' ) reads the block’s fields. To read a field from the current post, pass the ID explicitly: get_field( 'name', get_the_ID() ).
6. Make the preview honest
Load your main stylesheet into the editor with add_editor_style(), and the block’s style.css loads there automatically. The preview then uses the same CSS as the front end, which is the whole point of PHP-rendered blocks: what authors see is what visitors get.