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

Bash
1npm install -g handoff-hubspot

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:

Bash
1handoff-hubspot config

Configuration reference

KeyTypeDefaultDescription
urlstringhttps://localhost:3000/api/Base URL of the Handoff API
modulesPathstringmodulesDirectory where .module folders are written
cssPathstringcss/uds.cssOutput path for the shared CSS bundle
jsPathstringjs/uds.jsOutput path for the shared JS bundle
modulePrefixstringUDS:Prefix prepended to each module label in HubSpot
moduleCSSbooleantrueWrite per-module CSS; false writes a blank stub
moduleJSbooleanfalseWrite per-module JS; false writes a blank stub
usernamestring""HTTP Basic Auth username (if Handoff instance is protected)
passwordstring""HTTP Basic Auth password
importobjectabsentPer-type / per-component import rules (see below)

CLI commands

Bash
1handoff-hubspot list

Lists all components available from the Handoff API.

Bash
1handoff-hubspot docs [component-id]

Opens the Handoff documentation page for a component in the browser.

Bash
1handoff-hubspot validate [component-id] 2handoff-hubspot validate:all

Runs validation against every component (or just one). No files are written. Exits with a non-zero code if any component has errors.

Bash
1handoff-hubspot fetch [component-id] [--force] 2handoff-hubspot fetch:all [--force]

Validates, transpiles, and writes the .module folder(s). Without --force, the entire run aborts on the first validation error.

Bash
1handoff-hubspot styles

Fetches the shared main.css bundle from the Handoff API and writes it to cssPath.

Bash
1handoff-hubspot scripts

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.

JSON
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 valueEffect
import.{type}: falseSkip 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, and properties must all be present and non-empty
  • categories values 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, a name, and rules.required set to a boolean
  • text and number fields must declare rules.content (with min and max)
  • image fields must declare rules.dimensions.min (with width and height)
  • link defaults must include href and text; button defaults must include url and label
  • select fields must provide an options array
  • array fields must declare rules.content, items, and items.type
  • object fields must declare a properties map

Warnings (non-blocking)

  • Missing description or default on any field
  • Missing rules block 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.*:

Handlebars
1{{properties.headline}}{{ module.headline }}

Inside {{#each}} loops, this becomes the loop variable:

Handlebars
1{{#each properties.items}} 2 {{this.label}} 3{{/each}}

becomes:

hubl
1{% for item_i in module.items %} 2 {{ item_i.label }} 3{% endfor %}

Loop metadata variables:

HandlebarsHubL
@indexloop.index (1-based)
@firstloop.first
@lastloop.last

Conditionals

Handlebars
1{{#if properties.show_cta}} 2 <a href="...">Click</a> 3{{else}} 4 <span>No CTA</span> 5{{/if}}

becomes:

hubl
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-pathHubL output
properties.cta.href or .urlmodule.cta_url.href|escape_attr
properties.cta.label or .textmodule.cta_text
properties.cta.targetmodule.cta_url.type == 'EXTERNAL'

For image properties:

Handlebars
1<img src="{{properties.hero_img.src}}" alt="{{properties.hero_img.alt}}">

becomes:

hubl
1<img src="{{ module.hero_img.src }}" alt="{{ module.hero_img.alt }}">

Menu fields

{{#field properties.nav}} injects the HubL menu lookup and redirects subsequent {{#each}} to iterate .children:

Handlebars
1{{#field properties.nav}} 2 {{#each properties.nav}} 3 <li>{{this.label}}</li> 4 {{/each}} 5{{/field}}

becomes:

hubl
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 typeHubSpot field(s)
texttext
richtextrichtext
numbernumber (with min, max, step: 1)
booleanboolean (checkbox display)
selectchoice (with choices array)
imageimage (responsive, lazy-loaded, with dimension constraints)
icontext
link{key}_url (url type) + {key}_text (text type)
button{key}_url (url type) + {key}_text labeled "Label"
urlurl (all link types supported)
video_file{key} (file) + {key}_title (text)
video_embed{key} (embed URL text) + {key}_title + {key}_poster (image)
menumenu
arraygroup with occurrence (min/max); children built recursively
objectgroup; children built recursively

Module output structure

Each component produces a folder at {modulesPath}/{id}.module/:

Code
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 definitions

module.html opens with a metadata comment block:

hubl
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:

JSON
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_typeUse 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:

  1. Adds a Data Source choice field (Query Builder / Manual Data)
  2. Injects a Query Config field group (table selector, column mappings, sort, limit) visible only when Query Builder is selected
  3. Gates the original data array field to appear only when Manual Data is selected