If you build HubSpot CMS themes, you have probably copied the same block of HubL into three templates: a card, a button, a list of tags. A copy-pasted snippet becomes a maintenance problem the moment the markup changes. HubL macros solve this: you define a small function once and call it wherever you need it.
What is a HubL macro?
HubL is built on Jinja, and it supports Jinja-style macros. A macro takes arguments, outputs markup, and can be called like a function. According to the HubSpot developer documentation, macros are defined with the macro tag and closed with endmacro.
{% macro cta_button(label, url, style="primary") %}
<a class="btn btn--{{ style }}" href="{{ url }}">{{ label }}</a>
{% endmacro %}
{{ cta_button("Book a demo", "/demo") }}
{{ cta_button("Read more", "/blog", "secondary") }}
The third argument has a default value ("primary"), so callers can leave it out. Keyword arguments also work: {{ cta_button(label="Pricing", url="/pricing") }}.
Put macros in a shared file
Defining a macro in the same file where you use it is fine for one-offs, but the real value comes from a shared file. Create something like macros/components.html in your theme and place your macros there:
{# macros/components.html #}
{% macro tag_list(tags) %}
<ul class="tags">
{% for tag in tags %}
<li>{{ tag }}</li>
{% endfor %}
</ul>
{% endmacro %}
Then import the file in any template or module and call the macro through the alias you choose:
{% import "../macros/components.html" as components %}
{{ components.tag_list(["HubSpot", "HubL", "CMS"]) }}
You can also import a single macro by name:
{% from "../macros/components.html" import tag_list %}
{{ tag_list(["HubSpot", "HubL"]) }}
Relative paths are resolved from the file that contains the import, so check the path if the macro is not found.
Macros vs. includes vs. modules
- Macro: best for small pieces of markup that take arguments and are used many times, often inside loops or inside other modules.
- Include:
{% include "path/to/partial.html" %}inserts a file in place. It is good for static chunks such as a header or footer partial, but it has no argument list. - Module: best when content editors need to configure fields in the page editor. Macros do not create editor fields.
A useful pattern is to let a module read its field values and then pass them to a macro that renders the markup. The module handles editing; the macro handles consistent output across modules.
Practical tips
- Pass what the macro needs as arguments. Do not rely on variables defined elsewhere in the page; explicit arguments keep macros predictable and easy to test.
- Keep macros small. One macro, one job. A macro that renders a whole page section is usually better as a module.
- Use defaults for optional values so existing calls keep working when you add a new argument.
- Debug with pprint. If a macro prints nothing, output its arguments with
{{ label|pprint }}while you work, and remove it afterwards. - Handle empty data. Wrap output in
{% if tags %}...{% endif %}so the macro does not emit empty wrapper elements. - Test in the design tools. After changing a shared macro file, preview every template and module that imports it, since one edit affects them all.
A realistic example: a card macro for a HubDB listing
{% macro card(title, text, url, image_url="") %}
<article class="card">
{% if image_url %}<img src="{{ image_url }}" alt="" loading="lazy">{% endif %}
<h3><a href="{{ url }}">{{ title }}</a></h3>
<p>{{ text }}</p>
</article>
{% endmacro %}
{% for row in hubdb_table_rows(123456) %}
{{ card(row.name, row.summary, row.hs_path) }}
{% endfor %}
Replace 123456 with your own HubDB table ID. If the card design changes, you edit one macro and every listing updates.
Conclusion
Macros are one of the simplest ways to keep a HubSpot theme DRY. Start by finding the markup you have pasted more than twice, move it into a shared macros file, and import it where needed. Your templates get shorter, and design changes become one-file edits.
Sources
- HubSpot Developers: HubL variables, macros and statements (developers.hubspot.com/docs/cms/reference/hubl)
- HubSpot Developers: HubL supported tags, including
macro,import,fromandinclude - Jinja template documentation: macros and import
