How to build custom Carbon Fields custom wrappers extensions utilizing modern Block Patterns API schemas
Leveraging Carbon Fields for Advanced E-commerce UI with Block Patterns
For e-commerce platforms built on WordPress, delivering a seamless and intuitive content management experience is paramount. Carbon Fields, a robust framework for custom fields, offers extensive capabilities. When combined with WordPress’s modern Block Patterns API, we can construct highly sophisticated and reusable UI components. This post details how to build custom Carbon Fields wrapper extensions that integrate directly with Block Patterns, enabling dynamic content generation and management for complex e-commerce layouts.
Understanding the Synergy: Carbon Fields and Block Patterns
Block Patterns are pre-designed arrangements of blocks that users can insert into their posts or pages. They are defined using a JSON schema that describes the structure and content of the blocks. Carbon Fields, on the other hand, provides a PHP-based API for creating custom meta boxes, options pages, and taxonomies. The challenge and opportunity lie in bridging these two systems: using Carbon Fields to define the *data structure* and *input controls* for dynamic content, and then exposing this data through Block Patterns that can be easily inserted and managed by content creators.
Our goal is to create a system where a content editor can select a pre-defined “product showcase” pattern, which then dynamically renders product information managed via a custom Carbon Fields meta box attached to a custom post type (e.g., ‘Products’).
Defining the Custom Post Type and Carbon Fields Meta Box
First, we need a custom post type to hold our product data. We’ll then attach a Carbon Fields meta box to this CPT. This meta box will house the fields for product name, price, description, and an image.
Registering the ‘Products’ Custom Post Type
This code snippet should be placed in your theme’s `functions.php` file or within a custom plugin.
add_action( 'init', function() {
$labels = array(
'name' => _x( 'Products', 'Post type general name', 'your-text-domain' ),
'singular_name' => _x( 'Product', 'Post type singular name', 'your-text-domain' ),
'menu_name' => _x( 'Products', 'Admin Menu text', 'your-text-domain' ),
'name_admin_bar' => _x( 'Product', 'Add New on Toolbar', 'your-text-domain' ),
'add_new' => __( 'Add New', 'your-text-domain' ),
'add_new_item' => __( 'Add New Product', 'your-text-domain' ),
'edit_item' => __( 'Edit Product', 'your-text-domain' ),
'new_item' => __( 'New Product', 'your-text-domain' ),
'view_item' => __( 'View Product', 'your-text-domain' ),
'all_items' => __( 'All Products', 'your-text-domain' ),
'search_items' => __( 'Search Products', 'your-text-domain' ),
'parent_item_colon' => __( 'Parent Products:', 'your-text-domain' ),
'not_found' => __( 'No products found.', 'your-text-domain' ),
'not_found_in_trash' => __( 'No products found in Trash.', 'your-text-domain' ),
'featured_image' => _x( 'Product Cover Image', 'Overrides the "Featured Image" phrase for this post type. Added in 4.3', 'your-text-domain' ),
'set_featured_image' => _x( 'Set cover image', 'Overrides the "Set featured image" phrase for this post type. Added in 4.3', 'your-text-domain' ),
'remove_featured_image' => _x( 'Remove cover image', 'Overrides the "Remove featured image" phrase for this post type. Added in 4.3', 'your-text-domain' ),
'use_featured_image' => _x( 'Use as cover image', 'Overrides the "Use as featured image" phrase for this post type. Added in 4.3', 'your-text-domain' ),
'archives' => _x( 'Product archives', 'The post type archive label used in nav menus. Default is the post type name.', 'your-text-domain' ),
'insert_into_item' => _x( 'Insert into product', 'Used when inserting a post into a post. Similar to "Insert into item", but more specific to the post type.', 'your-text-domain' ),
'uploaded_to_this_item' => _x( 'Uploaded to this product', 'Used when attaching a media file to this post type. e.g. "Uploaded to this document".', 'your-text-domain' ),
'filter_items_list' => _x( 'Filter products list', 'Screen reader text for the filter links heading on the post type listing screen.', 'your-text-domain' ),
'items_list_navigation' => _x( 'Products list navigation', 'Screen reader text for the pagination of the post type listing screen.', 'your-text-domain' ),
'items_list' => _x( 'Products list', 'Screen reader text for the items list of the post type.', 'your-text-domain' ),
);
$args = array(
'labels' => $labels,
'public' => true,
'publicly_queryable' => true,
'show_ui' => true,
'show_in_menu' => true,
'query_var' => true,
'rewrite' => array( 'slug' => 'product' ),
'capability_type' => 'post',
'has_archive' => true,
'hierarchical' => false,
'menu_position' => null,
'menu_icon' => 'dashicons-cart',
'supports' => array( 'title', 'editor', 'thumbnail' ),
'show_in_rest' => true, // Important for Gutenberg integration
);
register_post_type( 'product', $args );
} );
Setting up the Carbon Fields Meta Box
This code defines the meta box and its fields. Ensure Carbon Fields is installed and activated.
use Carbon_Fields\Container;
use Carbon_Fields\Field;
add_action( 'carbon_fields_register_fields', function() {
Container::make( 'post_meta', __( 'Product Details', 'your-text-domain' ) )
->where( 'post_type', '=', 'product' )
->add_fields( array(
Field::make( 'text', 'crb_product_price', __( 'Price', 'your-text-domain' ) )
->set_attribute( 'type', 'number' )
->set_attribute( 'step', '0.01' )
->set_attribute( 'min', '0' ),
Field::make( 'textarea', 'crb_product_short_description', __( 'Short Description', 'your-text-domain' ) )
->set_rows( 3 ),
Field::make( 'image', 'crb_product_image', __( 'Product Image', 'your-text-domain' ) )
->set_storage_key( 'crb_product_image_id' ) // Store attachment ID
->set_width( 50 ),
) );
} );
Creating the Block Pattern Schema
Now, we define the Block Pattern. This JSON describes the structure of the pattern, including the blocks it contains and their attributes. Crucially, we’ll use a custom block (or a core block with specific attributes) that can dynamically pull data from our Carbon Fields meta box. For simplicity, we’ll assume a custom block `your-namespace/product-display` exists or we’ll simulate it using core blocks and custom attributes.
The Block Pattern API allows us to define patterns that can include dynamic blocks. A dynamic block is rendered server-side, allowing it to fetch and display data that might not be directly embedded in the pattern itself. However, for patterns that *initiate* the display of content managed by meta fields, we often define the pattern with placeholder content or specific attributes that a custom block or theme template can interpret.
A more direct approach for integrating Carbon Fields with Block Patterns involves creating a custom block that *references* the post ID and then fetches the Carbon Fields data within its `render_callback`. The pattern then simply includes this dynamic block.
Example Block Pattern JSON
This JSON would typically be registered via `register_block_pattern` or placed in a `block-patterns.php` file within your theme or plugin.
{
"title": "Featured Product Showcase",
"description": "Displays a featured product with its details.",
"categories": ["ecommerce", "products"],
"content": "<!-- wp:your-namespace/product-display {"postId": 0} /-->"
}
In this schema:
titleanddescriptionare for UI display in the editor.categorieshelp organize patterns.contentis the core. We’re using a placeholder forpostId(which will be dynamically set when the pattern is inserted into a post, or we can hardcode it if the pattern is meant for a specific product page template). Theyour-namespace/product-displayis our custom dynamic block.
Developing the Custom Dynamic Block
This custom block will be responsible for fetching the product data from the Carbon Fields meta box associated with a given post ID and rendering it.
Registering the Dynamic Block
This PHP code registers the dynamic block. It should be part of your plugin or theme’s `functions.php`.
add_action( 'init', function() {
register_block_type( 'your-namespace/product-display', array(
'render_callback' => 'render_product_display_block',
'attributes' => array(
'postId' => array(
'type' => 'number',
'default' => 0,
),
),
) );
} );
function render_product_display_block( $attributes ) {
$post_id = isset( $attributes['postId'] ) ? (int) $attributes['postId'] : get_the_ID();
if ( $post_id === 0 || get_post_type( $post_id ) !== 'product' ) {
return '<p>' . __( 'No product selected or invalid post ID.', 'your-text-domain' ) . '</p>';
}
// Ensure Carbon Fields data is available
if ( ! function_exists( 'carbon_get_post_meta' ) ) {
return '<p>' . __( 'Carbon Fields plugin is not active.', 'your-text-domain' ) . '</p>';
}
$product_price = carbon_get_post_meta( $post_id, 'crb_product_price' );
$short_desc = carbon_get_post_meta( $post_id, 'crb_product_short_description' );
$image_id = carbon_get_post_meta( $post_id, 'crb_product_image_id' ); // This is the attachment ID
$output = '<div class="product-display-wrapper">';
// Product Image
if ( $image_id ) {
$image_url = wp_get_attachment_image_url( $image_id, 'medium' ); // Or 'large', 'full', or a custom size
if ( $image_url ) {
$output .= '<img src="' . esc_url( $image_url ) . '" alt="' . esc_attr( get_the_title( $post_id ) ) . '" class="product-image" />';
}
}
// Product Title
$output .= '<h3 class="product-title">' . esc_html( get_the_title( $post_id ) ) . '</h3>';
// Product Price
if ( $product_price ) {
$output .= '<p class="product-price">' . wc_price( $product_price ) . '</p>'; // Using WooCommerce price formatting if available
}
// Short Description
if ( $short_desc ) {
$output .= '<div class="product-short-description">' . wp_kses_post( $short_desc ) . '</div>';
}
$output .= '</div>';
return $output;
}
Explanation:
register_block_typeregisters our dynamic block.render_callbackpoints to the PHP function that will generate the HTML.attributesdefine the block’s configurable properties. We includepostIdto specify which product to display.- The
render_product_display_blockfunction:- Retrieves the
postIdfrom attributes or defaults to the current post ID. - Checks if the post type is ‘product’.
- Uses
carbon_get_post_metato fetch data from our Carbon Fields. - Constructs the HTML output, including image, title, price, and description.
- Uses
wc_price()for formatted currency display (requires WooCommerce active, otherwise fallback needed). - Uses
wp_get_attachment_image_urlto get the image URL from the attachment ID stored by Carbon Fields.
- Retrieves the
Integrating with the Block Editor
Once the block pattern and dynamic block are registered, content creators can:
- Navigate to the WordPress editor (for a post, page, or even a product page template).
- Click the ‘+’ icon to add a block or pattern.
- Search for “Featured Product Showcase” (or whatever title you gave your pattern).
- Insert the pattern.
- The
your-namespace/product-displayblock will be added. If the pattern was inserted into a context wherepostIdisn’t automatically inferred (e.g., a generic page), you might need a way to select the product. This could involve:- A custom inspector control for the block to select a product via a dropdown or search.
- If the pattern is used within a specific template (e.g., a product archive template), the
postIdcould be dynamically set to the current product being viewed.
For the postId attribute in the pattern’s content, if the pattern is intended to be used on a page *about* a specific product, you would manually edit the block’s attributes in the editor to point to that product’s ID. If the pattern is part of a template that *loops* through products, the postId would be the ID of the product in the current loop iteration.
Advanced Considerations and Enhancements
Dynamic Post ID Selection in the Editor
To make the block more user-friendly within the editor, you can add an inspector control to allow users to select a product directly. This requires registering the block’s JavaScript components.
In your block’s block.json (or registered via PHP):
{
"name": "your-namespace/product-display",
"title": "Product Display",
"category": "widgets",
"icon": "cart",
"attributes": {
"postId": {
"type": "number",
"default": 0
}
},
"editor_script": "file:./build/index.js",
"editor_style": "file:./build/index.css",
"render": "file:./render.php" // Points to the PHP render callback
}
And in your build/index.js (using React and the Block Editor API):
import { registerBlockType } from '@wordpress/blocks';
import { InspectorControls } from '@wordpress/block-editor';
import { PanelBody, SelectControl } from '@wordpress/components';
import apiFetch from '@wordpress/api-fetch';
registerBlockType( 'your-namespace/product-display', {
edit: ( { attributes, setAttributes } ) => {
const { postId } = attributes;
// Fetch products for the dropdown
const [ products, setProducts ] = React.useState( [] );
React.useEffect( () => {
apiFetch( { path: '/wp/v2/product?per_page=100' } ).then( ( fetchedProducts ) => {
const productOptions = fetchedProducts.map( ( product ) => ( {
label: product.title.rendered,
value: product.id,
} ) );
setProducts( [ { label: 'Select a Product', value: 0 }, ...productOptions ] );
} );
}, [] );
return (
<>
<InspectorControls>
<PanelBody title="Product Selection">
{ products.length > 0 ? (
<SelectControl
label="Choose Product"
value={ postId.toString() }
options={ products }
onChange={ ( newPostId ) => setAttributes( { postId: parseInt( newPostId, 10 ) } ) }
/>
) : (
<p>Loading products...</p>
) }
</PanelBody>
</InspectorControls>
<div className="editor-product-preview">
{ postId > 0 ? `Preview for Product ID: ${postId}` : 'Select a product to see a preview.' }
{/* Optionally render a server-side preview using a placeholder */}
</div>
</>
);
},
save: () => null, // Dynamic blocks return null in save()
} );
Styling the Output
The rendered HTML includes classes like .product-display-wrapper, .product-image, .product-title, etc. You can style these using CSS in your theme’s stylesheet or a dedicated block stylesheet.
.product-display-wrapper {
border: 1px solid #eee;
padding: 20px;
margin-bottom: 20px;
text-align: center;
background-color: #f9f9f9;
}
.product-image {
max-width: 100%;
height: auto;
margin-bottom: 15px;
}
.product-title {
font-size: 1.8em;
margin-bottom: 10px;
color: #333;
}
.product-price {
font-size: 1.4em;
font-weight: bold;
color: #0073aa;
margin-bottom: 15px;
}
.product-short-description {
font-size: 1.1em;
color: #555;
}
Handling WooCommerce Integration
If you are using WooCommerce, you can leverage its functions for price formatting (like wc_price()) and potentially integrate with WooCommerce product data more deeply. Ensure your Carbon Fields setup aligns with WooCommerce’s product structure if you intend to replace or augment its functionality.
Conclusion
By combining Carbon Fields for robust data management and the Block Patterns API with custom dynamic blocks for flexible rendering, you can create powerful, reusable e-commerce UI components within WordPress. This approach empowers content editors to easily manage product details and integrate them seamlessly into their site’s design, leading to a more efficient and visually appealing online store.