Don’t hesitate to contact us if you have any feedback.

Universal Processing of WordPress Blocks: ACF & Native (React)

Modern WordPress development empowers you to build custom blocks using either Advanced Custom Fields (ACF) or native React. For maintainable, scalable projects, it’s crucial to adopt a universal approach to structuring, building, and registering your blocksβ€”regardless of their type.

This article provides a unified workflow for processing both ACF and native (React) blocks, covering file structure, build configuration, PHP registration, and best practices.

Important: the ACF blocks example are made by using ACF and Timber, I’m not mentionning It every time because It’s too long.

Why a Universal Approach?

  • Consistency: One structure for all block types simplifies onboarding and maintenance.
  • Portability: Blocks can be moved between projects with minimal changes.
  • Scalability: Unified build and registration processes make it easy to add new blocks.

Block Configuration: block.json

The block.json file is the heart of every block, defining metadata, assets, and supported features.

Example for Native (React) Block:

{
  "$schema": "https://schemas.wp.org/trunk/block.json",
  "apiVersion": 3,
  "name": "namespace/your-block",
  "title": "Your block",
  "icon": "block-default",
  "description": "A custom react block.",
  "category": "widgets",
  "keywords": ["block", "example"],
  "version": "1.0.0",
  "textdomain": "your-textdomain",
  "attributes": {},
  "supports": {},
  "viewScript": "file:./assets/view.js",
  "editorScript": "file:./assets/index.js",
  "editorStyle": "file:./assets/index.css",
  "style": "file:./assets/style-index.css",
  "viewStyle": "file:./assets/view.css"
}

Example for ACF Block:

{
  "$schema": "https://schemas.wp.org/trunk/block.json",
  "apiVersion": 3,
  "name": "namespace/your-block",
  "title": "Your block",
  "icon": "block-default",
  "description": "A custom ACF block.",
  "category": "widgets",
  "keywords": ["block", "example"],
  "version": "1.0.0",
  "textdomain": "your-textdomain",
  "attributes": {},
  "supports": {},
  "acf": {
    "mode": "preview",
    "renderTemplate": "./includes/render.php"
  },
  "viewScript": "file:./assets/view.js",
  "viewStyle": "file:./assets/style-view.css",
  "editorScript": "file:./assets/index.js",
  "editorStyle": "file:./assets/index.css"
}

Build Process: Webpack & Scripts

Use a single build process for all blocks.
Adjust paths as needed for your project.

webpack.config.js

const wordpressConfig = require("@wordpress/scripts/config/webpack.config");
const CopyWebpackPlugin = require("copy-webpack-plugin");

module.exports = {
  ...wordpressConfig,
  plugins: [
    ...wordpressConfig.plugins,
    new CopyWebpackPlugin({
      patterns: [
        { from: "**/*.twig", context: process.env.WP_SOURCE_PATH },
        { from: "**/*.php", context: process.env.WP_SOURCE_PATH }
      ],
    }),
  ],
};

package.json scripts:

{
  "scripts": {
    "watch:blocks": "wp-scripts start --webpack-src-dir=blocks --output-path=build/blocks",
    "build:blocks": "wp-scripts build --webpack-src-dir=blocks --output-path=build/blocks"
  }
}

Registering and Enqueueing Blocks

Automatically register all blocks by scanning for block.json files, this ensures all block assets and metadata are registered and enqueued automatically.

<?php

/**
 * Registers blocks using metadata from `block.json` files.
 *
 * This function registers blocks and their assets so they can be enqueued
 * through the block editor in the corresponding context.
 *
 * @see https://developer.wordpress.org/reference/functions/register_block_type/
 * @see wp-includes\blocks.php
 *
 * @return void
 */
function register_blocks(): void
{
    // Include all files in the block folder
    $dirpath = get_stylesheet_directory() . '/build/blocks/';

    $files = glob($dirpath . '**/block.json');

    if (!empty($files)) {
        foreach ($files as $file) {
            if (file_exists($file)) {

                // Get all the block metadata
                $metadata = wp_json_file_decode($file, ['associative' => true]);
                if (!is_array($metadata) || empty($metadata['name'])) {
                    continue;
                }

                // Register the block
                $metadata['file'] = wp_normalize_path(realpath($file));
                register_block_type($metadata['file']);

                // Handle translation for the block
                $scriptEditorHandle = generate_block_asset_handle($metadata['name'], 'editorScript');
                $scriptHandle = generate_block_asset_handle($metadata['name'], 'viewScript');
                $handles = [];
                if (!empty($scriptEditorHandle)) {
                    $handles[] = $scriptEditorHandle;
                }
                if (!empty($scriptHandle)) {
                    $handles[] = $scriptHandle;
                }

                // Include acf.php if it exists
                $block_dir = dirname($file);
                $acf_file = $block_dir . '/includes/acf.php';
                if (file_exists($acf_file)) {
                    include_once $acf_file;
                }
            }
        }
    }
}
add_action('init', 'register_blocks');

ACF Block Example

Folder Structure:

πŸ“‚ your-block
β”œβ”€β”€ πŸ“‚ assets
β”‚   β”œβ”€β”€ πŸ“‚ styles
β”‚   β”‚    β”œβ”€β”€ style.scss        # block styles
β”‚   β”‚    └── editor.scss       # Editor-specific styles
β”‚   β”œβ”€β”€ index.js         # Editor-specific JavaScript
β”‚   └── view.js           # Front-specific JavaScript
β”œβ”€β”€ πŸ“‚ includes
β”‚   β”œβ”€β”€ acf.php            # ACF field configuration
β”‚   └── render.php         # PHP render callback
β”œβ”€β”€ πŸ“‚ views
β”‚   └── view.twig          # Twig template
└── block.json             # Block configuration

ACF Field Registration (includes/acf.php):

<?php 

if (!function_exists('acf_add_local_field_group')) {
    return;
}
acf_add_local_field_group([
    'key' => 'group_your_block',
    'title' => 'Your Block Fields',
    'fields' => [
        [
            'key' => 'field_your_field',
            'label' => 'Your Field',
            'name' => 'your_field',
            'type' => 'text',
            // ... other field settings
        ],
        // Add more fields as needed
    ],
    'location' => [
        [
            [
                'param' => 'block',
                'operator' => '==',
                'value' => 'acf/your-block',
            ],
        ],
    ],
]);

Render Callback (includes/render.php):

<?php

/**
 * ACF Block Template
 *
 * Available variables:
 * @var array    $block      The block settings and attributes
 * @var string   $content    The block inner HTML (empty)
 * @var boolean  $is_preview True during backend preview render
 * @var integer  $post_id    The Post ID of the current context
 * @var array    $context    The context provided to the block
 */
use Timber\Timber;

// Get ACF fields
$field = get_field('field_your_field');

// Render the Twig template
Timber::render('views/view.twig', [
    'field' => !empty($field) ? $field : 'placeholder'
]);

Twig Template (views/view.twig):

<section class="your-block">
    {{ field }}
</section>

Helper Functions for Block Output

When working with ACF blocks, you may need to convert block attributes and style arrays into valid CSS for inline styles or classes. Here are some useful PHP helpers:

<?php

class BlockHelpers
{
    /**
     * Converts custom property strings to CSS variable format.
     *
     * @param string $value The custom property string to convert.
     * @return string The converted CSS variable string.
     */
    public static function convertCustomProperties(string $value): string
    {
        $prefix = 'var:';
        $prefix_len = mb_strlen($prefix);
        $token_in = '|';
        $token_out = '--';
        if (str_starts_with($value, $prefix)) {
            $unwrapped_name = str_replace(
                $token_in,
                $token_out,
                mb_substr($value, $prefix_len)
            );
            $value = "var(--wp--{$unwrapped_name})";
        }

        return $value;
    }

    /**
     * Returns inline block style properties as a CSS string.
     *
     * @param array $block The block data.
     * @return string The inline CSS style properties.
     */
    public static function getBlockStyleInline(array $block): string
    {
        $attrs = [];

        // Map of preset properties to their CSS variable formatters
        // Each formatter is a closure that generates the appropriate CSS variable string
        // e.g., fontSize: 'large' -> var(--wp--preset--font-size--large)
        $presetMap = [
            'fontSize' => fn ($v) => "var(--wp--preset--font-size--{$v})",
            'textColor' => fn ($v) => "var(--wp--preset--color--{$v})",
            'backgroundColor' => fn ($v) => "var(--wp--preset--color--{$v})",
        ];

        // Process each preset property if it exists in the block
        foreach ($presetMap as $key => $formatter) {
            if (isset($block[$key])) {
                // Convert camelCase property names to kebab-case CSS properties
                $cssKey = match ($key) {
                    'fontSize' => 'font-size',
                    'textColor' => 'color',
                    'backgroundColor' => 'background-color',
                };
                $attrs[$cssKey] = $formatter($block[$key]);
            }
        }

        // Spacing
        if (isset($block['style']['spacing'])) {
            foreach (['margin', 'padding'] as $prop) {
                $spacing = $block['style']['spacing'][$prop] ?? null;
                if (is_array($spacing)) {
                    foreach ($spacing as $dir => $value) {
                        $attrs["{$prop}-{$dir}"] = self::convertCustomProperties($value);
                    }
                }
            }
        }

        // Colors
        if (isset($block['style']['color'])) {
            foreach (['text' => 'color', 'background' => 'background-color'] as $key => $cssKey) {
                $color = $block['style']['color'][$key] ?? null;
                if ($color) {
                    $attrs[$cssKey] = self::convertCustomProperties($color);
                }
            }
        }

        // Convert attributes array to CSS string
        // If no attributes exist, return empty string
        // Otherwise, map each key-value pair to "key:value" format and join with semicolons
        return empty($attrs) ? '' : implode(';', array_map(
            fn ($k, $v) => "{$k}:{$v}",
            array_keys($attrs),
            $attrs
        ));
    }

    /**
     * Retrieves block classes based on back-office settings.
     *
     * @param array $block The block data.
     * @return string The block classes as a space-separated string.
     */
    public static function getBlockClass(array $block): string
    {
        $classes = [];

        if (isset($block['align'])) {
            $align = $block['align'];
            $classes[] = "align{$align}";
        }

        if (isset($block['className'])) {
            $classes[] = $block['className'];
        }

        if (!empty($classes)) {
            $classes = implode(' ', $classes);
        }

        return $classes;
    }

    /**
     * Gets theme color value from a color label.
     *
     * @param string $label The color label to look up.
     * @param string $returnKey The key to return from the color data (default: 'slug').
     * @return string The color value or empty string if not found.
     */
    public static function getThemeColorsFromLabel(string $label, string $returnKey = 'slug'): string
    {
        $theme_json = \WP_Theme_JSON_Resolver::get_merged_data('theme');
        $theme_data = $theme_json->get_raw_data();
        $theme_colors = $theme_data['settings']['color']['palette']['theme'] ?? [];

        if (empty($theme_colors)) {
            return '';
        }

        // Use array_filter to find the color with matching name
        $matching_colors = array_filter($theme_colors, function ($color) use ($label) {
            return $color['name'] === $label;
        });

        // If we found a match, return its color value
        if (!empty($matching_colors)) {
            $first_match = reset($matching_colors);
            return $first_match[$returnKey];
        }

        // Return empty string if no match found
        return '';
    }
}

Using Native Block Features

You can use native WordPress block features (like spacing, color, typography, etc.) in your ACF blocks. To do this, process the values from the `$block` variable in your `render.php` file and pass them to your template for inline styles or classes.

Example: Handling spacing and outputting inline styles

<?php

// In render.php
$style_to_apply = BlockHelpers::getBlockStyleInline($block);
$block_context['style_to_apply'] = $style_to_apply;

Then in your Twig template:

{# In your Twig template #}
<section class="your-block" style="{{ style_to_apply }}">
  {{ field }}
</section>

The same pattern applies to other block features like colors, typography, and alignment. Just access the values from the `$block` variable and process them as needed.

Styles

Front-end Styles (assets/styles/style.scss)

// Style for the block
.block-acf---your-block {
  border: 1px solid #ccc;
  min-height: 200px;
}

Editor Styles (assets/styles/editor.scss)

// Style for the gutenberg editor part of the block
@use 'style.scss';

.block-acf---your-block {}

Scripts

Front-end Scripts (assets/view.js)

// This file is for the front-end part of the block
import './styles/style.scss'

Editor Scripts (assets/index.js)

// This file is for the gutenberg editor part of the block
import './styles/editor.scss'

Native (React) Block Example

Folder Structure:

πŸ“‚ your-block
β”œβ”€β”€ πŸ“‚ assets
β”‚   β”œβ”€β”€ index.jsx          # Main entry point
β”‚   β”œβ”€β”€ view.js            # Frontend view script
β”‚   β”œβ”€β”€ πŸ“‚ scripts
β”‚   β”‚   β”œβ”€β”€ save.jsx       # Save component
β”‚   β”‚   └── edit.jsx       # Edit component
β”‚   └── πŸ“‚ styles
β”‚       β”œβ”€β”€ style.scss     # Frontend styles
β”‚       └── editor.scss    # Editor styles
└── block.json             # Block configuration

Main Entry (assets/index.jsx):

/**
 * Let webpack process CSS, SASS or SCSS files referenced in JavaScript files.
 * All files containing `style` keyword are bundled together. The code used
 * gets applied both to the front of your site and to the editor.
 *
 * @see https://www.npmjs.com/package/@wordpress/scripts#using-css
 */
import "./styles/style.scss";

/**
 * Registers a new block provided a unique name and an object defining its behavior.
 *
 * @see https://developer.wordpress.org/block-editor/reference-guides/block-api/block-registration/
 */
import {registerBlockType} from "@wordpress/blocks";

import metadata from "../block.json";

/**
 * Internal dependencies
 */
import Edit from "./scripts/edit";
import Save from "./scripts/save";

/**
 * Every block starts by registering a new block type definition.
 *
 * @see https://developer.wordpress.org/block-editor/reference-guides/block-api/block-registration/
 */
registerBlockType(metadata.name, {
  /**
   * @see ./edit.js
   */
  edit: Edit,

  /**
   * @see ./save.js
   */
  save: Save,
});

Edit Component (assets/scripts/edit.jsx):

import "../styles/editor.scss";

import {useBlockProps} from "@wordpress/block-editor";
import {__} from "@wordpress/i18n";

export default function Edit({attributes, isSelected, setAttributes}) {
  // ---------- attributes are the states stored by WordPress
  // They are defined in the block.json
  // ----------

  // ---------- BlockProps are the data that will be inserted into the main html tag of the block (style, data-attr, etc...)
  const blockProps = useBlockProps();
  // ----------

  // ---------- States that aren't stored by WordPress
  // They are only useful for the preview
  // ----------

  // ---------- Other variables
  // ----------

  return (
    <section {...blockProps}>
      <h1>{__("this is my custom block", "your-textdomain")}</h1>
    </section>
  );
}

Save Component (assets/scripts/save.jsx):

import {useBlockProps} from "@wordpress/block-editor";
import {__} from "@wordpress/i18n";

export default function Save({attributes}) {
  const blockProps = useBlockProps.save();

  return (
    <section {...blockProps}>
      <h1>{__("this is my custom block", "your-textdomain")}</h1>
    </section>
  );
}

Styles

Frontend Styles (style.scss)

.wp-block-namespace-your-block {
  border: 1px solid #ccc;
  min-height: 200px;
}

Editor Styles (editor.scss)

.wp-block-namespace-your-block {
  // Editor-specific styles
}

Using Timber Inside a React Block

To integrate Timber with your React block, use a PHP render callback (render.php) to process block attributes and render a Twig template.

1. Folder Structure Update

πŸ“‚ your-block
β”œβ”€β”€ πŸ“‚ includes
β”‚   └── render.php         # PHP render callback
└── πŸ“‚ views
    └── view.twig          # Twig template

2. Block Configuration Update (block.json)

Add the render parameter to your block.json to specify the PHP render callback:

{
  "render": "file:./includes/render.php",
}

3. PHP Render Callback (includes/render.php)

<?php

use Timber\Timber;

if (!isset($block) || !isset($attributes)) {
    return '';
}

$inner_blocks = $block->inner_blocks;
$inner_blocks_html = '';
if (!empty($inner_blocks)) {
    foreach ($inner_blocks as $inner_block) {
        $inner_blocks_html .= $inner_block->render();
    }
}

Timber::render('views/view.twig', [
   'inner_blocks' => $inner_blocks_html
]);

4. Twig Template (views/view.twig)

<section class="block-react---your-block">
  {{ inner_blocks|raw }}
</section>

Best Practices

  • Consistent Structure: Use the same folder and file naming conventions for all blocks.
  • block.json First: Always define block metadata and assets in block.json.
  • Unified Build: Use a single build process for all block types.
  • Auto-Registration: Register blocks by scanning for block.json files.
  • Separation of Concerns: Keep templates, scripts, and styles modular and organized.
  • Accessibility & Internationalization: Use semantic HTML and @wordpress/i18n for translations.
  • Performance: Optimize bundle size and use dynamic imports for heavy dependencies.

References

By following this universal approach, you’ll ensure your WordPress blocksβ€”whether ACF or nativeβ€”are robust, maintainable, and ready for any project.

Let me know if you want this tailored for a specific block, or if you need a more concise or expanded version!


Comments

Leave a Reply

Your email address will not be published. Required fields are marked *