Headless & Composable CMS · 6 min read

HubSpot CMS Development: HubL, Modules and CRM-Driven Content

HubL templating, custom module fields, HubDB, serverless functions and personalising content from CRM data without breaking caching.

By Praful Patel · Last updated
HubSpot CMS Development: HubL, Modules and CRM-Driven Content - article cover
HubSpot CMS Development: HubL, Modules and CRM-Driven Content - article cover

Short answer

HubSpot CMS is a marketing platform that happens to render pages, and building on it well means designing for marketers rather than for engineers. The technical surface — HubL templating, custom modules, HubDB, serverless functions — is straightforward. The judgement is in which fields to expose so a campaign page can be assembled without a developer.

The real advantage is not the templating language. It is that content sits next to the CRM, so a page can genuinely reflect what is known about the person viewing it. That is also the thing most likely to be implemented badly.

HubL, and where server-side rendering stops

HubL is a Jinja-like templating language executed on HubSpot’s servers before the page is served. Loops, conditionals, filters, macros and includes all work as you would expect.

{% for item in module.cards %}
  <article class="card">
    <h3>{{ item.heading }}</h3>
    {# HubL escapes by default; |safe opts out, so only use it on trusted rich text #}
    {{ item.body|safe }}
    {% if item.link.url.href %}
      <a href="{{ item.link.url.href }}">{{ item.link.text|default("Read more") }}</a>
    {% endif %}
  </article>
{% endfor %}

What HubL cannot do is anything requiring a request-time decision beyond the data available to it, or anything expensive. There is a rendering budget; templates that loop over large HubDB tables with nested queries will hit it.

Custom modules: the field schema is the product

A module is a reusable component with a field schema, and the schema is the interface a marketer uses. Get it wrong in either direction and the module fails.

Too permissive — arbitrary HTML fields, free-text CSS classes, unconstrained colour pickers — and every page drifts from the design system. Too rigid, and someone files a ticket for every campaign.

The balance that works:

  • Choice fields for variants, not free text. A dropdown of three defined styles cannot be misspelled.
  • Repeaters with minimum and maximum counts, so a three-column grid receives three to six items rather than one or twenty.
  • Rich text only where prose belongs. Headings should be text fields; a rich-text heading eventually contains a nested list.
  • Sensible defaults on every field, so a freshly dropped module looks correct immediately.
  • Field groups and clear labels. The field label is documentation, and it is the only documentation most users will read.

HubDB for structured content

HubDB is a table store with a HubL query API, and dynamic pages can be generated from it — one row becoming one URL. It suits location finders, product catalogues, event listings and resource libraries.

Its limits matter: no joins, shallow filtering, and query cost that shows up as render time. Denormalise the columns you filter on, paginate, and avoid nested queries inside loops — the equivalent of an N+1 problem, and it degrades the same way.

Serverless functions

Serverless functions provide a Node runtime for the things HubL cannot do: calling an external API with a secret, handling a webhook, processing a custom form submission.

exports.main = async (context, sendResponse) => {
  // Secrets are configured in HubSpot, never in the template or the bundle.
  const key = process.env.PARTNER_API_KEY;

  // Validate before trusting anything from the caller - this URL is public.
  const email = String(context.body?.email ?? '').trim().toLowerCase();
  if (!/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(email)) {
    return sendResponse({ statusCode: 400, body: { error: 'Invalid email' } });
  }

  const result = await lookup(key, email);
  sendResponse({ statusCode: 200, body: { tier: result.tier } });
};

They are rate limited and have execution time limits, so they are not the place for heavy processing. Treat them as a small integration layer, and verify signatures on anything receiving a webhook.

CRM-driven content, without breaking caching

This is the platform’s genuine differentiator: smart content that varies by lifecycle stage, list membership, country or device. It is also where two mistakes recur.

First, personalisation defeats caching. A page that varies per visitor cannot be served from a shared cache, so it is slower for everyone. Personalise the sections that change conversion, not the whole page.

Second, the fallback is the real experience. Most first-time visitors are unknown contacts. If the default variant is a placeholder, that is what most of your audience sees. Design the anonymous case first and treat personalisation as an enhancement.

Local development and deployment

The HubSpot CLI is what makes this a real engineering workflow rather than editing in a browser. It gives you local files, a watch-and-upload loop, and — importantly — the ability to keep templates and modules in Git with normal review.

hs init                      # authenticate a portal
hs watch src design-manager  # upload on save while developing
hs upload src design-manager # explicit deploy

Use a sandbox portal for development and promote deliberately. Editing production templates directly is possible and is how a live landing page breaks during a campaign.

When HubSpot CMS is the right choice

It fits when marketing already runs on HubSpot, when campaign pages need to ship without engineering involvement, and when CRM-aware content is genuinely part of the strategy. The integration is the value, and rebuilding it elsewhere is expensive.

It fits poorly when the site is the product, when performance is a competitive requirement, or when the content model needs relational depth. In those cases a headless CMS with your own front end gives control that HubSpot deliberately does not — the trade-offs are in Contentstack Content Modelling.

Questions people actually ask

Can I use React on HubSpot CMS?
You can ship JavaScript, including React, inside a module for interactive components. You cannot replace HubSpot’s rendering layer with a React application – HubL renders the page. For islands of interactivity it works; as an application framework it does not.

Is HubDB a real database?
It is a table store with a query API and no joins. Good for structured lists that drive dynamic pages; wrong for relational data or anything write-heavy. If the model needs joins across three tables, put the data elsewhere and call it from a serverless function.

How do I keep marketers from breaking the design?
Constrain the module fields. Choice fields instead of free-text classes, repeaters with count limits, and no arbitrary HTML or CSS fields. Governance through documentation does not hold; governance through the field schema does.

Does personalisation hurt performance?
Yes, because per-visitor variation cannot be served from a shared cache. Limit it to the sections where it changes behaviour and let the rest of the page stay cacheable. A fully personalised page is slower for every visitor including the ones it was meant to help.

Can I version control HubSpot templates?
Yes – use the CLI to keep templates, modules and functions as local files in Git, then upload deliberately. This is the main thing separating a maintainable HubSpot build from one edited live in the design manager, and it is worth setting up on day one.

← Back to all insights