HubL cheat sheet
The HubSpot Markup Language on one page: the syntax, the filters, functions and tags you reach for most, and how module fields work, each with an example.
- HubL is the HubSpot Markup Language: the template language of HubSpot CMS themes, modules, partials and emails.
- It is based on Jinjava, HubSpot's Java version of Jinja, so most Jinja habits carry over.
- HubSpot renders it on the server; visitors only ever see the finished HTML.
- transjt writes the HubL for you: every exported module comes with its fields wired up. This sheet is for the moments you want to adjust it.
Delimiters
| Syntax | What it does |
|---|---|
{{ ... }} | Prints an expression: a variable, a field, the result of a function. |
{% ... %} | A statement or tag: if, for, set, module, include … |
{# ... #} | A comment. It never reaches the page. |
{%- ... -%} | Whitespace control: the dash trims the whitespace before or after the tag. |
{# A module's heading field #}
<h1>{{ module.heading }}</h1>
Variables
Set your own with set. Strings, numbers, booleans, lists and dicts work as in Jinja:
{% set title = "Pricing" %}
{% set sizes = [480, 960, 1440] %}
{% set button = {"label": "Start free", "url": "/start/"} %}
{{ title ~ " | transjt" }} {# ~ joins strings #}
{{ sizes[0] }} {# 480 #}
{{ button.label }} {# or button["label"] #}
{% do sizes.append(1920) %} {# change a list in place #}
HubSpot provides these on every page:
| Variable | Holds |
|---|---|
content | The page or blog post: content.absolute_url, content.name, content.publish_date … |
page_meta.html_title, page_meta.meta_description | The title and meta description from the page settings. |
request | The request: request.path, request.domain, request.query_dict (the query string as a dict). |
local_dt | The current date and time, in the account's time zone. |
theme | The theme's settings from its fields.json: theme.colors.primary.color and so on, as your theme names them. |
module | Inside a module: its field values, module.heading, module.image.src … |
contact | The known visitor's contact properties, for personalization: contact.firstname. |
is_in_editor | True while the page is open in the HubSpot editor. |
contact, local_dt, request.query_dict and today() change with every visit, so HubSpot can't serve a page that uses them fully from its prerendered cache. Use them only where you need them.If and for
{% if module.items %}
<ul>
{% for item in module.items %}
<li class="{{ 'is-first' if loop.first else '' }}">{{ loop.index }}. {{ item.title }}</li>
{% endfor %}
</ul>
{% elif is_in_editor %}
<p>Add an item in the sidebar.</p>
{% else %}
{# Nothing to show on the live page #}
{% endif %}
- In a loop:
loop.index(from 1),loop.index0(from 0),loop.first,loop.last,loop.length. {% unless condition %} … {% endunless %}is the opposite ofif.- Tests read like English:
{% if module.subtitle is defined %},is string,is number,is even,is divisibleby 3. - Operators:
==,!=,and,or,not,in.
Filters
A filter changes a value after a pipe, {{ value|filter }}, and filters chain from left to right.
| Filter | Example | Result |
|---|---|---|
default | {{ module.label|default("Read more", true) }} | A fallback when the value is missing; with true, also when it is empty |
upper, lower, capitalize, title | {{ "hubl"|upper }} | HUBL |
truncate | {{ text|truncate(120) }} | Cut to 120 characters at a word boundary, then “...” |
truncatehtml | {{ module.text|truncatehtml(200) }} | Shortens rich text and keeps its tags closed |
striptags | {{ module.text|striptags }} | Plain text without the HTML |
escape_html, escape_attr, escape_url | alt="{{ module.alt|escape_attr }}" | Safe output in HTML text, attributes and URLs |
replace | {{ "a-b"|replace("-", " ") }} | a b |
split, join | {{ "a,b"|split(",")|join(" · ") }} | a · b |
length | {{ module.items|length }} | The number of items |
sort | {{ posts|sort(False, False, "name") }} | Sorted by an attribute (reverse, case-sensitive, attribute) |
selectattr, map | {{ items|selectattr("featured")|map("title")|join(", ") }} | The titles of the featured items |
format_datetime | {{ content.publish_date|format_datetime("medium", "Europe/Berlin") }} | A formatted date and time in that time zone (UTC if you name none) |
int, float, round | {{ "3.7"|float|round }} | 4 |
tojson | {{ module|tojson }} | The value as JSON, for a script |
pprint | {{ module|pprint }} | Prints a value with its type, for debugging |
Functions
| Function | What it does |
|---|---|
get_asset_url("../css/main.css") | The public URL of a file in your theme. |
require_css(url), require_js(url) | Loads a stylesheet in the head or a script before the closing body tag, once per page however often it is called. |
resize_image_url(url, 800) | A resized copy of an image from the File Manager, 800 pixels wide. |
menu("main") | A navigation menu by name or ID, with its children, for your own markup. |
blog_recent_posts("default", 3) | The three newest posts of the default blog. |
blog_popular_posts("default", 5) | The five most read posts of the past year, refreshed every six hours. |
hubdb_table_rows(table, "orderBy=name&limit=10") | Rows from a HubDB table, filtered and sorted by a query string (Content Hub Professional or Enterprise). |
color_variant("#FF00F4", -40) | A colour made lighter (positive) or darker (negative). |
today(), unixtimestamp(local_dt) | Today's date; a date as a Unix timestamp in milliseconds. |
{% for post in blog_recent_posts("default", 3) %}
<a href="{{ post.absolute_url }}">{{ post.name }}</a>
{% endfor %}
Tags
| Tag | What it does |
|---|---|
{% module "hero" path="../modules/hero" %} | Places a module with a fixed position in a template. |
{% dnd_area "main" %} … {% end_dnd_area %} | A drag-and-drop area editors fill in the page editor, with dnd_section, dnd_row, dnd_column and dnd_module inside. |
{% include "../partials/footer.html" %} | Inserts another file. |
{% global_partial path="../partials/header.html" %} | A partial that is edited once and shared by every page. |
{% extends "./layouts/base.html" %} with {% block body %} | Template inheritance: a page fills the blocks of its layout. |
{% macro button(label, url) %} | A reusable snippet with parameters, shared with {% from "/my-theme/macros.html" import button %} (the file's path in the Design Manager). |
{% require_css %} … {% end_require_css %} | A <style> block moved into the head, where it may use HubL. Unlike the function, it is added every time. |
{% raw %} … {% endraw %} | Prints its content as written, without running HubL in it. |
Every page template needs {{ standard_header_includes }} before the closing </head> and {{ standard_footer_includes }} before the closing </body>: they bring in HubSpot's own scripts, styles and tracking.
<!--
templateType: page
isAvailableForNewContent: true
label: Landing page
-->
{% extends "./layouts/base.html" %}
{% block body %}
{% dnd_area "main" label="Main" %}
{% dnd_section %}
{% dnd_column %}
{% dnd_row %}
{% dnd_module path="@hubspot/rich_text" %}{% end_dnd_module %}
{% end_dnd_row %}
{% end_dnd_column %}
{% end_dnd_section %}
{% end_dnd_area %}
{% endblock %}
Modules and fields
A module is a folder ending in .module with module.html (HubL), module.css, module.js, fields.json and meta.json. In module.html, each field is module. plus its name:
| Field type | HubL |
|---|---|
| Text, rich text | {{ module.heading }}, {{ module.text }} |
| Image | module.image.src, .alt, .width, .height |
| Link | module.link.url.href, module.link.open_in_new_tab |
| Colour | module.color.color, module.color.opacity |
| Choice, boolean | {{ module.style }}, {% if module.show_button %} |
Repeater (a field with occurrence) | {% for item in module.items %} |
{% set img = module.image %}
<section class="hero hero--{{ name }}">
{% inline_text field="heading" value="{{ module.heading }}" %}
{% if img.src %}
<img src="{{ img.src }}" alt="{{ img.alt|escape_attr }}" width="{{ img.width }}" height="{{ img.height }}" loading="lazy">
{% endif %}
</section>
{{ name }}is the module instance's unique name: use it to scope CSS to one instance.inline_textandinline_rich_textmake a field editable right on the page in the editor.module.csssupports almost no HubL (only helpers such asmodule_asset_url) and can't read field values;module.jssupports none. For a value from a field, put a{% require_css %}block inmodule.html.- A colour picked in a field and printed in an inline
style=""wins over the module's CSS: checkmodule.htmlfirst when a style won't change.
Debugging
{{ module|pprint }}prints a value with its type, right on the page. Remove it before you publish.- Add
?hsDebug=trueto a live page's URL: its CSS and JavaScript load unminified and uncombined, nothing comes from the cache, and the end of the page's source lists HubL errors and warnings, the render time and whether the page can be prerendered. - The Design Manager marks HubL errors in the file while you edit. With the HubSpot CLI,
hs cms lint <path>checks files, and uploads report fatal errors in the terminal. - HubSpot re-renders the pages that use an updated module or template by itself, which can take a few minutes. If a live page still shows the old version, publish it again.
HubL in transjt themes
When you export a design or an AI build, transjt writes the theme for you: templates with drag-and-drop areas, and one module per section with its texts, images, links and colours as fields. Your marketing team edits those fields in the page editor; nobody has to write HubL.
To adjust a theme yourself, keep your own CSS and JavaScript in custom.css and custom.js: they stay in place when you export again. See For HubSpot developers and Using your theme in HubSpot.
The full HubL reference is in HubSpot's developer documentation.