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.
1. Module parameters are not expressions
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 }}".
2. Lists passed to modules vanish
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.
3. "and not" is not what you think
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.
4. Global module defaults stop mattering after the first render
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.
5. The portal path has no .theme suffix
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.
6. Two full width modules in one row
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.
7. The form is gone when HubSpot says it was submitted
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.
How we catch them now
| 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.