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
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 buildConfiguration
Copy the example config and edit it for your Cynosure installation:
Config file structure
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
| Field | Purpose |
|---|---|
apiUrl | Base URL of the running Handoff site (local or production) |
output | Directory where generated block folders are written |
themeDir | WordPress theme directory; used for template part generation |
import.element | Whether to compile element-level components (usually false) |
import.block.{name} | Per-block import configuration (see below) |
groups | Merge 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)
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)
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
Property type mapping
Handoff property types compile to WordPress block attribute types:
| Handoff type | Gutenberg equivalent | Editor UI |
|---|---|---|
text | string attribute | Plain text input |
richtext | string attribute | RichText component |
image | object (url, alt, width, height) | MediaUpload panel |
link | object (href, label) | URLInput + text field |
boolean | boolean attribute | ToggleControl |
select | string with enum | SelectControl |
array | Repeater or query block | Configurable (see above) |
object | Grouped attributes | Panel with sub-controls |
number | number attribute | NumberControl |
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:
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:
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
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 validateLocal development with wp-env
The handoff-wordpress package includes a local WordPress environment using @wordpress/env:
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:logsThe 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.):
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
- AI-assisted WordPress workflow
- JavaScript guidelines — understanding what each block's
script.jsimports - CSS guidelines — token usage in WordPress themes
On This Page