HubSpot Transpiler Pipeline
Technical reference for the handoff-hubspot CLI: installation, configuration, all commands, Handlebars-to-HubL conversion rules, and the complete field type mapping.
Installation
Requires Node 20 and a running Handoff instance reachable over HTTP.
Configuration
Run the interactive wizard to create handoff.config.json in the current directory:
Configuration reference
| Key | Type | Default | Description |
|---|---|---|---|
url | string | https://localhost:3000/api/ | Base URL of the Handoff API |
modulesPath | string | modules | Directory where .module folders are written |
cssPath | string | css/uds.css | Output path for the shared CSS bundle |
jsPath | string | js/uds.js | Output path for the shared JS bundle |
modulePrefix | string | UDS: | Prefix prepended to each module label in HubSpot |
moduleCSS | boolean | true | Write per-module CSS; false writes a blank stub |
moduleJS | boolean | false | Write per-module JS; false writes a blank stub |
username | string | "" | HTTP Basic Auth username (if Handoff instance is protected) |
password | string | "" | HTTP Basic Auth password |
import | object | absent | Per-type / per-component import rules (see below) |
CLI commands
Lists all components available from the Handoff API.
Opens the Handoff documentation page for a component in the browser.
Runs validation against every component (or just one). No files are written. Exits with a non-zero code if any component has errors.
Validates, transpiles, and writes the .module folder(s). Without --force, the
entire run aborts on the first validation error.
Fetches the shared main.css bundle from the Handoff API and writes it to cssPath.
Fetches the shared main.js bundle from the Handoff API and writes it to jsPath.
Skipped automatically when moduleJS: true.
Import configuration
The import key controls which components are included and how they are built.
1{
2 "import": {
3 "element": false,
4 "block": {
5 "hero_chart": false
6 },
7 "data": {
8 "bar_chart": {
9 "target_property": "data",
10 "mapping_type": "xy",
11 "js": true
12 }
13 }
14 }
15}| Config value | Effect |
|---|---|
import.{type}: false | Skip all components of that type |
import.{type}: true (or absent) | Import all components of that type normally |
import.{type}: { id: false } | Import all except those set to false |
import.{type}: { id: { type: "hubdb", ... } } | Import with HubDB data mapping |
import.{type}: { id: { js: true } } | Fetch per-component JS even when moduleJS: false |
import.{type}: { id: { css: true } } | Fetch per-component CSS even when moduleCSS: false |
Validation rules
Validation runs before transpilation. Components that fail with errors are not
built unless --force is passed. Warnings are surfaced but never block the build.
Module-level errors
code,title,tags,categories, andpropertiesmust all be present and non-emptycategoriesvalues must be one of:blog,body_content,commerce,design,functionality,forms_and_buttons,media,social,text
Field-level errors
- Every field must have a valid
type, aname, andrules.requiredset to a boolean textandnumberfields must declarerules.content(withminandmax)imagefields must declarerules.dimensions.min(withwidthandheight)linkdefaults must includehrefandtext;buttondefaults must includeurlandlabelselectfields must provide anoptionsarrayarrayfields must declarerules.content,items, anditems.typeobjectfields must declare apropertiesmap
Warnings (non-blocking)
- Missing
descriptionordefaulton any field - Missing
rulesblock on any field
Handlebars → HubL transpilation
The transpiler converts Handlebars source to HubL by walking the template AST.
Namespace
All properties.* references are rewritten to module.*:
Inside {{#each}} loops, this becomes the loop variable:
becomes:
Loop metadata variables:
| Handlebars | HubL |
|---|---|
@index | loop.index (1-based) |
@first | loop.first |
@last | loop.last |
Conditionals
becomes:
1{% if module.show_cta %}
2 <a href="...">Click</a>
3{% else %}
4 <span>No CTA</span>
5{% endif %}{{#unless}} becomes {% unless %}...{% endunless %}.
Type-aware variable output
The transpiler uses the property schema to decide how each variable is rendered.
For link and button properties:
| Handlebars sub-path | HubL output |
|---|---|
properties.cta.href or .url | module.cta_url.href|escape_attr |
properties.cta.label or .text | module.cta_text |
properties.cta.target | module.cta_url.type == 'EXTERNAL' |
For image properties:
becomes:
Menu fields
{{#field properties.nav}} injects the HubL menu lookup and redirects subsequent
{{#each}} to iterate .children:
becomes:
1{# field properties.nav type="menu" #}
2{% set menu_xxxxx = menu(module.nav) %}
3{% for item_n in menu_xxxxx.children %}
4 <li>{{ item_n.label }}</li>
5{% endfor %}
6{# end field #}Field type mapping
Each Handoff property type maps to one or more entries in fields.json:
| Handoff type | HubSpot field(s) |
|---|---|
text | text |
richtext | richtext |
number | number (with min, max, step: 1) |
boolean | boolean (checkbox display) |
select | choice (with choices array) |
image | image (responsive, lazy-loaded, with dimension constraints) |
icon | text |
link | {key}_url (url type) + {key}_text (text type) |
button | {key}_url (url type) + {key}_text labeled "Label" |
url | url (all link types supported) |
video_file | {key} (file) + {key}_title (text) |
video_embed | {key} (embed URL text) + {key}_title + {key}_poster (image) |
menu | menu |
array | group with occurrence (min/max); children built recursively |
object | group; children built recursively |
Module output structure
Each component produces a folder at {modulesPath}/{id}.module/:
hero-banner.module/
├── module.html # HubL template
├── module.css # Component CSS (or blank stub)
├── module.js # Component JavaScript (or blank stub)
├── meta.json # HubSpot module metadata
└── fields.json # HubSpot field definitionsmodule.html opens with a metadata comment block:
1{#
2 title: Hero Banner
3 description: A full-width hero with headline and CTA
4 group: Marketing
5 version: 1.4.2
6 last_updated: 2026-03-01T00:00:00.000Z
7 link: https://demo.handoff.com/system/component/hero-banner
8#}Navigation components (group "Navigation") are automatically marked global: true
in meta.json.
HubDB data mapping
Chart components can be configured to pull data from a HubDB table instead of
requiring manual data entry. Add a type: "hubdb" entry under import:
1{
2 "import": {
3 "data": {
4 "bar_chart": {
5 "target_property": "data",
6 "mapping_type": "xy",
7 "js": true
8 },
9 "category_breakdown_chart": {
10 "target_property": "data",
11 "mapping_type": "multi_series",
12 "js": true
13 }
14 }
15 }
16}mapping_type | Use case |
|---|---|
"xy" | Two-column data (X axis + single Y series) — bar charts, line charts |
"multi_series" | Multiple named series with shared categories — stacked/grouped charts |
The transpiler automatically:
- Adds a Data Source choice field (
Query Builder/Manual Data) - Injects a Query Config field group (table selector, column mappings, sort, limit) visible only when Query Builder is selected
- Gates the original data array field to appear only when Manual Data is selected
On This Page