WordPress Integration

Overview

The handoff-wordpress CLI reads the Cynosure Handoff API and compiles components into WordPress Gutenberg blocks. Each Handoff component becomes a registered Gutenberg block with:

  • A block editor UI driven by the component's property schema
  • A PHP render template compiled from the Handlebars template
  • Scoped component CSS
  • Enqueued JavaScript from the shared Handoff bundle

This lets WordPress site developers drop design-system components directly into the block editor without writing any template code.

Installation

Bash
1# Clone or install handoff-wordpress into your WordPress project 2npm install handoff-wordpress 3 4# Or run directly from the design system repo 5cd examples/handoff-wordpress 6npm install 7npm run build

Configuration

Copy the example config and edit it for your Cynosure installation:

Bash
1cp handoff-wp.config.example.json handoff-wp.config.json

Config file structure

JSON
1{ 2 "apiUrl": "https://your-handoff-site.com", 3 "output": "./plugin/blocks", 4 "themeDir": "./theme", 5 "username": "", 6 "password": "", 7 8 "import": { 9 "element": false, 10 11 "block": { 12 "posts-latest": { 13 "posts": { 14 "postTypes": ["post"], 15 "selectionMode": "query", 16 "maxItems": 6, 17 "renderMode": "mapped", 18 "fieldMapping": { 19 "image": "featured_image", 20 "title": "post_title", 21 "excerpt": "post_excerpt", 22 "date.day": "post_date:day_numeric", 23 "date.month": "post_date:month_short", 24 "date.year": "post_date:year", 25 "category": { "type": "taxonomy", "taxonomy": "category", "format": "first" }, 26 "link.url": "permalink", 27 "link.text": { "type": "static", "value": "Read More" } 28 }, 29 "defaultQueryArgs": { 30 "posts_per_page": 6, 31 "orderby": "date", 32 "order": "DESC" 33 } 34 } 35 } 36 } 37 }, 38 39 "groups": { 40 "heroes": "merged", 41 "ctas": "merged" 42 } 43}

Config fields

FieldPurpose
apiUrlBase URL of the running Handoff site (local or production)
outputDirectory where generated block folders are written
themeDirWordPress theme directory; used for template part generation
import.elementWhether to compile element-level components (usually false)
import.block.{name}Per-block import configuration (see below)
groupsMerge grouped blocks into a single registered block ("merged" or "separate")

Array property configuration

When a block's property schema contains an array type that should be driven by WordPress content, configure it in the import.block section:

Query-driven arrays (post lists)

JSON
1"posts-latest": { 2 "posts": { 3 "postTypes": ["post", "page"], 4 "selectionMode": "query", 5 "maxItems": 20, 6 "renderMode": "mapped", 7 "fieldMapping": { 8 "image": "featured_image", 9 "title": "post_title", 10 "excerpt": "post_excerpt", 11 "link.url": "permalink" 12 } 13 } 14}

Manual selection arrays (curated content)

JSON
1"hero-carousel": { 2 "slides": { 3 "postTypes": ["post", "page"], 4 "selectionMode": "manual", 5 "maxItems": 10, 6 "renderMode": "mapped", 7 "fieldMapping": { 8 "image": "featured_image", 9 "title": "post_title", 10 "link.url": "permalink", 11 "link.label": { "type": "static", "value": "Learn More" } 12 } 13 } 14}

Inner blocks arrays

JSON
1"content": { 2 "body": { "innerBlocks": true } 3}

Property type mapping

Handoff property types compile to WordPress block attribute types:

Handoff typeGutenberg equivalentEditor UI
textstring attributePlain text input
richtextstring attributeRichText component
imageobject (url, alt, width, height)MediaUpload panel
linkobject (href, label)URLInput + text field
booleanboolean attributeToggleControl
selectstring with enumSelectControl
arrayRepeater or query blockConfigurable (see above)
objectGrouped attributesPanel with sub-controls
numbernumber attributeNumberControl

Avoid the icon and video_file property types if the component will be used in WordPress — these have limited mapping support.

Generated file anatomy

After running handoff-wordpress fetch, each block produces:

Code
plugin/blocks/{block-name}/ ├── block.json — Block registration: name, title, attributes, editorScript ├── edit.js — React editor component using @10up/block-components ├── render.php — PHP render template (Handlebars compiled to PHP) ├── module.css — Compiled component SCSS └── index.js — Block entry (registers block with registerBlockType)

render.php example

The Handlebars {{properties.headline}} binding compiles to PHP get_field() or attribute access:

php
1<?php 2$headline = $attributes['headline'] ?? ''; 3$items = $attributes['items'] ?? []; 4?> 5<section class="c-feature" data-component="feature"> 6 <?php if ($headline): ?> 7 <h2 class="c-feature__headline"><?php echo esc_html($headline); ?></h2> 8 <?php endif; ?> 9 10 <div class="c-feature__grid row"> 11 <?php foreach ($items as $item): ?> 12 <div class="col-12 col-md-4"> 13 <h3><?php echo esc_html($item['title'] ?? ''); ?></h3> 14 <p><?php echo esc_html($item['body'] ?? ''); ?></p> 15 </div> 16 <?php endforeach; ?> 17 </div> 18</section>

CLI commands

Bash
1# Fetch all components from Handoff API and generate block files 2handoff-wordpress fetch 3 4# Development: watch for changes and regenerate 5handoff-wordpress dev 6 7# Interactive setup wizard 8handoff-wordpress wizard 9 10# Generate for a specific theme template 11handoff-wordpress fetch --theme my-theme-name 12 13# Validate components before generating 14handoff-wordpress validate

Local development with wp-env

The handoff-wordpress package includes a local WordPress environment using @wordpress/env:

Bash
1# Start local WordPress (requires Docker) 2npm run wp:start 3 4# Stop 5npm run wp:stop 6 7# Open WP CLI 8npm run wp:cli 9 10# View logs 11npm run wp:logs

The local environment mounts the demo/theme/ and demo/plugin/ directories automatically. After running handoff-wordpress fetch, the generated blocks appear in the WordPress block editor immediately.

Enqueuing the Handoff JS bundle

The compiled main.js from the Handoff design system must be enqueued in WordPress to enable the shared JavaScript utilities (Select2, Slick Carousel, Magnific Popup, etc.):

php
1// functions.php 2function cynosure_enqueue_assets() { 3 wp_enqueue_script( 4 'cynosure-handoff', 5 'https://cynosure.handoff.com/api/component/main.js', 6 ['jquery'], 7 null, 8 true 9 ); 10 11 // Inject localized data required by selects.js and popup.js 12 wp_localize_script('cynosure-handoff', 'CYNO_DATA', [ 13 'ajax_url' => admin_url('admin-ajax.php'), 14 'filter_labels' => [ 15 'any' => __('Any', 'cynosure'), 16 'selected' => __('selected', 'cynosure'), 17 'select_treatment_any' => __('Select treatment (any)', 'cynosure'), 18 'select_product_any' => __('Select product (any)', 'cynosure'), 19 ], 20 ]); 21} 22add_action('wp_enqueue_scripts', 'cynosure_enqueue_assets');

Further reading