All docs

DocsHubSpotHubL cheat sheet

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.

At a glance
  • 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

SyntaxWhat 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:

VariableHolds
contentThe page or blog post: content.absolute_url, content.name, content.publish_date …
page_meta.html_title, page_meta.meta_descriptionThe title and meta description from the page settings.
requestThe request: request.path, request.domain, request.query_dict (the query string as a dict).
local_dtThe current date and time, in the account's time zone.
themeThe theme's settings from its fields.json: theme.colors.primary.color and so on, as your theme names them.
moduleInside a module: its field values, module.heading, module.image.src …
contactThe known visitor's contact properties, for personalization: contact.firstname.
is_in_editorTrue while the page is open in the HubSpot editor.
Keep pages fast. 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 of if.
  • 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.

FilterExampleResult
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_urlalt="{{ 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

FunctionWhat 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

TagWhat 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 typeHubL
Text, rich text{{ module.heading }}, {{ module.text }}
Imagemodule.image.src, .alt, .width, .height
Linkmodule.link.url.href, module.link.open_in_new_tab
Colourmodule.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_text and inline_rich_text make a field editable right on the page in the editor.
  • module.css supports almost no HubL (only helpers such as module_asset_url) and can't read field values; module.js supports none. For a value from a field, put a {% require_css %} block in module.html.
  • A colour picked in a field and printed in an inline style="" wins over the module's CSS: check module.html first 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=true to 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.

Design your website today, launch tomorrow.

At transjt, every second counts. That’s why our mission is simple: save you time and remove friction from your workflow.