sanruiz avatar

acf-block-development

Create ACF flexible content blocks (partials) following SOMA PageBuilder architecture

提供方 sanruiz|开源

ACF Block Development Skill

This skill guides the creation of ACF flexible content blocks (partials) for the SOMA theme's PageBuilder system.

When to Use This Skill

Use this skill when you need to:

  • Create a new ACF flexible content block
  • Add a new page section/component
  • Modify existing block layouts
  • Work with BlockRegistry and BlockRenderer
  • Create reusable content patterns

PageBuilder Architecture

System Overview

ACF Field: soma_blocks (Flexible Content)
    ↓
page-builder.php (calls BlockRenderer)
    ↓
BlockRegistry (53 blocks mapped: layout → field_group + partial)
    ↓
BlockRenderer (validates, sets query vars, includes partial)
    ↓
partials/ComponentName.php (renders HTML)

Key Components

ComponentFilePurpose
BlockRegistryincludes/PageBuilder/BlockRegistry.phpMaps layouts to field groups and partials
BlockRendererincludes/PageBuilder/BlockRenderer.phpRenders blocks with validation and caching
Loaderincludes/PageBuilder/Loader.phpInitializes PageBuilder system

Creating a New ACF Block

Step 1: Create ACF Field Group

Option A: ACF Admin UI

  1. Go to Custom FieldsAdd New
  2. Create field group with fields for your block
  3. Set Location: Flexible Content > soma_blocks > Layout equals ComponentName

Option B: ACF JSON (Recommended)

Create acf-json/group_component_name.json:

{
    "key": "group_component_name",
    "title": "Component Name Block",
    "fields": [
        {
            "key": "field_component_title",
            "label": "Title",
            "name": "title",
            "type": "text"
        },
        {
            "key": "field_component_content",
            "label": "Content",
            "name": "content",
            "type": "wysiwyg"
        }
    ],
    "location": [
        [
            {
                "param": "flexible_content_layout",
                "operator": "==",
                "value": "ComponentName"
            }
        ]
    ]
}

Step 2: Register Block in BlockRegistry

File: includes/PageBuilder/BlockRegistry.php

private function register_default_blocks(): void {
    // ... existing blocks ...
    
    // Add your new block
    $this->register_block(
        'ComponentName',          // ACF layout name
        'component_name_content', // ACF field group key (for validation)
        'ComponentName'           // Partial filename (without .php)
    );
}

Step 3: Create Partial Template

File: partials/ComponentName.php

<?php
/**
 * Component Name Block
 *
 * Renders the Component Name section.
 *
 * @package Soma
 * @since   3.0.0
 */

if ( ! defined( 'ABSPATH' ) ) {
    exit; // Exit if accessed directly.
}

// Access block data via WordPress query vars (v3.0+)
$block_counter = get_query_var( 'soma_block_counter' );
$block_content = get_query_var( 'soma_block_content' );
$block_layout  = get_query_var( 'soma_block_layout' );

// Extract fields with defaults
$title   = $block_content['title'] ?? '';
$content = $block_content['content'] ?? '';
$image   = $block_content['image'] ?? null;

// Generate unique hash for CSS class
$hash = substr( md5( $block_layout . $block_counter ), 0, 6 );
?>

<section class="componentname-partial-<?php echo esc_attr( $hash ); ?>" 
         data-block="<?php echo esc_attr( $block_counter ); ?>">
    
    <?php if ( $title ) : ?>
        <h2 class="section-title"><?php echo esc_html( $title ); ?></h2>
    <?php endif; ?>
    
    <?php if ( $content ) : ?>
        <div class="section-content">
            <?php echo wp_kses_post( $content ); ?>
        </div>
    <?php endif; ?>
    
    <?php if ( $image ) : ?>
        <div class="section-image">
            <?php echo wp_get_attachment_image( $image, 'large' ); ?>
        </div>
    <?php endif; ?>
    
</section>

Step 4: Create SCSS Styles

File: sass/partials/_ComponentName.scss

/**
 * Component Name Partial Styles
 *
 * @package Soma
 * @since   3.0.0
 */

[class^="componentname-partial-"] {
    padding: var(--soma-spacing-2xl) 0;
    background: var(--soma-bg-white);
    
    .section-title {
        font-family: var(--soma-font-family-primary);
        font-size: var(--soma-font-size-h2);
        color: var(--soma-color-text-primary);
        margin-bottom: var(--soma-spacing-lg);
    }
    
    .section-content {
        font-size: var(--soma-font-size-body);
        line-height: var(--soma-line-height-relaxed);
        color: var(--soma-color-text-secondary);
        
        p {
            margin-bottom: var(--soma-spacing-md);
        }
    }
    
    .section-image {
        margin-top: var(--soma-spacing-xl);
        
        img {
            width: 100%;
            height: auto;
            border-radius: var(--soma-border-radius);
        }
    }
    
    // Responsive
    @media (max-width: 991px) {
        padding: var(--soma-spacing-xl) 0;
        
        .section-title {
            font-size: var(--soma-font-size-h3);
        }
    }
    
    @media (max-width: 767px) {
        padding: var(--soma-spacing-lg) 0;
        
        .section-title {
            font-size: var(--soma-font-size-h4);
        }
    }
}

Step 5: Import SCSS in Main File

File: sass/main.scss

// ... existing imports ...

// #DittoPartials
@import 'partials/ComponentName';  // Add under this marker

Step 6: Add JavaScript (if needed)

File: js/components/componentName.js

/**
 * Component Name Handler
 *
 * @package Soma
 * @since   3.0.0
 */

export function componentNameHandler($element) {
    if (!$element || !$element.length) {
        return;
    }
    
    // Your interactive code here
    $element.each(function() {
        const $this = $(this);
        
        // Example: Click handler
        $this.find('.clickable').on('click', function(e) {
            e.preventDefault();
            // Handle click
        });
    });
}

File: js/main.js

import { componentNameHandler } from './components/componentName';

// Initialize conditionally
if ($('[class^="componentname-partial-"]').length > 0) {
    componentNameHandler($('[class^="componentname-partial-"]'));
}

Step 7: Build Assets

npm run prod

Query Vars Reference

The BlockRenderer sets these WordPress query vars for each block:

Query VarTypeDescription
soma_block_counterintBlock index (1-based)
soma_block_contentarrayACF field data for block
soma_block_layoutstringLayout name (e.g., 'ComponentName')
// Access in partial
$counter = get_query_var( 'soma_block_counter' ); // e.g., 1, 2, 3
$content = get_query_var( 'soma_block_content' ); // ACF fields array
$layout  = get_query_var( 'soma_block_layout' );  // 'ComponentName'

Common ACF Field Types

Text Fields

$title = $block_content['title'] ?? '';
echo esc_html( $title );

WYSIWYG/Textarea

$content = $block_content['content'] ?? '';
echo wp_kses_post( $content );

Image

$image_id = $block_content['image'] ?? null;
if ( $image_id ) {
    echo wp_get_attachment_image( $image_id, 'large', false, [
        'class' => 'section-image',
        'alt'   => get_post_meta( $image_id, '_wp_attachment_image_alt', true ),
    ] );
}

Gallery

$gallery = $block_content['gallery'] ?? [];
foreach ( $gallery as $image_id ) {
    echo wp_get_attachment_image( $image_id, 'medium' );
}

Link

$link = $block_content['link'] ?? null;
if ( $link ) {
    printf(
        '<a href="%s" target="%s" class="btn">%s</a>',
        esc_url( $link['url'] ),
        esc_attr( $link['target'] ?: '_self' ),
        esc_html( $link['title'] )
    );
}

Repeater

$items = $block_content['items'] ?? [];
if ( $items ) {
    echo '<ul class="items-list">';
    foreach ( $items as $item ) {
        printf(
            '<li>%s</li>',
            esc_html( $item['title'] )
        );
    }
    echo '</ul>';
}

Select/Radio

$style = $block_content['style'] ?? 'default';
$class = 'section-style-' . sanitize_html_class( $style );

True/False (Switcher)

$show_button = $block_content['show_button'] ?? false;
if ( $show_button ) {
    // Render button
}

Post Object

$selected_post = $block_content['featured_post'] ?? null;
if ( $selected_post ) {
    $post_id    = is_object( $selected_post ) ? $selected_post->ID : $selected_post;
    $post_title = get_the_title( $post_id );
    $post_url   = get_permalink( $post_id );
}

Relationship (Multiple Posts)

$posts = $block_content['related_posts'] ?? [];
foreach ( $posts as $post ) {
    $post_id = is_object( $post ) ? $post->ID : $post;
    // Use get_the_title($post_id), get_permalink($post_id), etc.
}

Internationalized Fields

For bilingual content (EN/ES), use the helper function:

// ACF fields: file (EN), file_es (ES)
$file = soma_get_i18n_field( $block_content, 'file' );

// Returns file_es if Spanish, file if English
if ( $file ) {
    echo '<a href="' . esc_url( $file['url'] ) . '">Download</a>';
}

CSS Variables Reference

// Colors
--soma-primary
--soma-secondary
--soma-color-text-primary
--soma-color-text-secondary
--soma-bg-white
--soma-bg-light
--soma-bg-dark

// Typography
--soma-font-family-primary
--soma-font-size-h1
--soma-font-size-h2
--soma-font-size-h3
--soma-font-size-body
--soma-line-height-tight
--soma-line-height-relaxed

// Spacing
--soma-spacing-xs
--soma-spacing-sm
--soma-spacing-md
--soma-spacing-lg
--soma-spacing-xl
--soma-spacing-2xl

// Layout
--soma-container-max-width
--soma-border-radius

// Transitions
--soma-transition-base
--soma-transition-slow

Dark Mode Support

// Check if previous block had dark background
$previous_is_dark = get_query_var( 'soma_previous_block_dark' ) ?? false;

// Check if this block should be dark
$is_dark = $block_content['dark_style'] ?? false;
$dark_class = $is_dark ? 'dark-style' : '';
?>

<section class="componentname-partial <?php echo esc_attr( $dark_class ); ?>">
[class^="componentname-partial-"] {
    &.dark-style {
        background: var(--soma-bg-dark);
        color: var(--soma-color-text-light);
        
        .section-title {
            color: var(--soma-color-text-light);
        }
    }
}

Block Validation

The BlockRenderer validates blocks before rendering:

  1. Structure Validation - Checks for acf_fc_layout key
  2. Registry Validation - Verifies block is registered in BlockRegistry
  3. File Existence - Checks partial file exists

Errors are logged to wp-content/uploads/soma-logs/soma.log.

Debugging Blocks

// In partial, temporarily add for debugging:
if ( WP_DEBUG ) {
    echo '<!-- Block Data: ';
    print_r( $block_content );
    echo ' -->';
}

Or use the logger:

soma_log_debug( 'Block content', [
    'layout'  => $block_layout,
    'counter' => $block_counter,
    'content' => $block_content,
] );

Checklist Before Committing

  • ACF field group created (JSON or Admin UI)
  • Block registered in BlockRegistry.php
  • Partial created in partials/ directory
  • Query vars used (not global $pageBlock)
  • All output properly escaped
  • SCSS file created and imported in main.scss
  • JavaScript handler added (if interactive)
  • Assets rebuilt (npm run prod)
  • Block renders correctly on frontend
  • Responsive design tested
  • PHPCS passes on partial file