A HubSpot theme bug that throws an error is a good bug. You see it, you fix it. The expensive ones upload with a green tick, render a page, and quietly leave something out.
These seven cost us real hours across two Content Hub theme builds this year. Each one is now a check in our validator, so it cannot reach a client portal again.
Passing a variable into a module from a coded template looks obvious and does not work:
{% module "hero" path="../modules/hero" headline=page.headline %}
That renders the literal text page.headline. Parameters only evaluate inside interpolation, so write headline="{{ page.headline }}".
Worse than the first: pass a list to a repeater field and the section renders with its heading and no items. No error anywhere. Our fix is a hidden text field on every repeater module that accepts JSON, which the module parses back into a list:
data_json='{{ cards|tojson }}'
Use single quotes on the parameter, because tojson emits double quotes, and never put a straight apostrophe in the data.
A condition like {% if a and not b %} can coerce a string operand to false and skip the block silently. Nest the conditions or compare explicitly. It reads worse and works every time.
A global module saves its content into the portal the first time it renders. After that, changing the defaults in fields.json uploads cleanly and changes nothing on the site. We render headers and footers from a generated literal inside module.html instead, synced from fields.json by a script that runs before every deploy.
If you change a nav in code and the site does not change, this is why.
The theme folder on disk is my-theme.theme. In the portal it is my-theme. Create a page over the API pointing at the on disk path and you get HTTP 200 and a blank document. The only clue is an HTML comment saying the template is missing.
Two width=12 modules inside the same dnd_row passes a local lint and fails at upload with an error that names the wrong line. One full width module per row.
Code that waits for the submitted event and then reads values from the form finds nothing, because HubSpot has already swapped the form for its thank you message. Capture the form ID and values on input and submit, then treat the submitted message appearing as the signal. Our audit gate sat on "Connecting you" for a day because of this.
| Trap | Check |
|---|---|
| Bare variables in module tags | Validator flags any parameter value that is not a literal or interpolated |
| Lists into repeaters | Every repeater module ships a JSON field; lists passed directly fail the build |
| Global module defaults | Nav and footer rendered from synced literals; sync runs in the deploy script |
| Theme path | API scripts resolve the portal path, never the disk path |
| Two full width modules | Row width totals checked before upload |
| Form submit timing | Shared handoff script used by every gated form |
The pattern across all seven is the same: HubSpot is forgiving by design, so the platform will not tell you when you are wrong. A validator chained to every upload does. If your theme has a bug nobody can explain, send us the repo.