📘 HubSpot CMS Development Textbook — 2026 Edition
Chapter 2

HubL (HubSpot Markup Language) basics

A thorough explanation of HubL, a language for writing templates and modules. It systematically organizes everything from basic syntax to built-in variables, filters, and frequently used functions, along with example code.

🎯 Target level:Basic to intermediate
⏱ Estimated reading completion:60-80 minutes
🔗 Previous chapter:Chapter 1. Setting up the development environment

Contents of this chapter

  1. What is HubL?
  2. Basic syntax: 3 delimiters
  3. Variables/sets/data types
  4. Control syntax: if / for / unless
  5. Defining and reusing macros
  6. Built-in variables list
  7. Complete guide to filters
  8. Blog-specific HubL function list
  9. How to use HubL and JavaScript
  10. Chapter 2 Summary
Section 2-1

What is HubL?

HubLis a template engine designed by HubSpot. Based on Python's Jinja2, HubSpot-specific variables, filters, and tags have been added. HubL is embedded in an HTML file, processed on the server side, and finally output as HTML.

Positioning of HubL
HTML + HubL
↓ (server processing)
pure HTML
Only the final HTML is delivered to the browser
base technology
Jinja2
(Python template engine)
+ HubSpot proprietary extension
Those who have knowledge of Jinja2 will be able to get used to it quickly.
place to use
・Template.html
・Module module.html
・Partial.html
Cannot be written in CSS/JS files
ℹ️ HubL only works on the server side

HubL does not work in the browser (client side). When a page is requested, HubSpot's servers process it, convert it to HTML, and then serve it. Therefore,JavaScript is responsible for interactive processing that responds to user operations (such as switching the display after clicking)I will. The usage of HubL and JavaScript will be explained in detail in Section 2-9.


Section 2-2

Basic syntax: 3 delimiters

HubL has three types of notation for different purposes. This is the basic unit of HubL.

{{ }} output tag
{{ variable name }}
{{ variable | filter }}
{{ expression }}
Output variable values ​​and filter processing results to HTML
{% %} statement tag
{% if condition %}
{% for x in list %}
{% set variable = value %}
Control syntax, variable definitions, etc. Not output to HTML
{# #} comment
{# This text is
No output #}
Template comments. Not output to HTML source
HubL — Three Delimiters Examples
{# ====== ① Output tag {{ }} ====== #}
{# Output variable value to HTML #}
<h1>{{ content.name }}</h1>
<p>{{ content.meta_description }}</p>{# Process and output using filters #}
<p>{{ content.publish_date|datetimeformat("%Y year %m month %d day") }}</p>{# ====== ② Statement tag {% %} ====== #}
{# Set value to variable (no output) #}
{% set site_name = "Sample Co., Ltd." %}

{# Conditional branch (no output) #}
{% if content.featured_image %}
  <img src="{{ content.featured_image }}" alt="{{ content.featured_image_alt_text }}">{% endif %}

{# ====== ③ Comment {# #} ====== #}
{# This comment is also not displayed in the HTML source #}
{# TODO: Add mobile support #}

Section 2-3

Variables/sets/data types

Defining variables (set)

{% set %} Define variables with tags. Once defined, variables can be reused within the template.

HubL — variable definition with set
{# string #}
{% set company_name = "Sample Co., Ltd." %}

{# number #}
{% set max_posts = 6 %}

{# Boolean value #}
{% set is_published = true %}

{# list (array) #}
{% set nav_items = ["Home", "service", "Company Profile", "inquiry"] %}

{# dict — key-value pairs #}
{% set form_ids = {
  "contact"  : "xxxx-xxxx-contact",
  "seminar"  : "xxxx-xxxx-seminar",
  "download" : "xxxx-xxxx-download"
} %}

{# Get value from dictionary #}
{{ form_ids["seminar"] }}
{{ form_ids.contact }}     {# Also accessible with dot notation #}

Update variables (namespace pattern)

HubL does not allow you to directly rewrite variables in the outer scope within a loop. namespace You can update variables outside the loop from within the loop. This is a pattern often used in practice.

HubL — namespace pattern (variable update in loop)
{# Ordinary set cannot update outer variables within a loop (NG) #}
{% set content_type = "blog" %}
{% for tag in content.tag_list %}
  {% set content_type = tag.slug %}  {# ← This is only valid inside a loop! #}
{% endfor %}

{# ✅ Correct pattern using namespace #}
{% set ns = namespace(content_type="blog") %}
{% for tag in content.tag_list %}
  {% if tag.slug starts_with "type:" %}
    {% set ns.content_type = tag.slug|replace("type:", "") %}
  {% endif %}
{% endfor %}

{# Get the correct value after the loop #}
<div class="post-type-{{ ns.content_type }}">...</div>
⚠️ This is a common point

HubL's scope is different from JavaScript. {% set %} The variables defined in{% for %} Since it has its own scope within the block, Changes made inside the loop are not reflected outside the loop. If you want to update variables across loops, use Be sure to use namespace. We will actually use it as a tag judgment pattern in Chapter 6.


Section 2-4

Control syntax: if / for / unless

① Conditional branching (if / elif / else / endif)

HubL — Basic patterns for if statements
{# Basic if #}
{% if content.featured_image %}
  <img src="{{ content.featured_image }}" alt="{{ content.featured_image_alt_text }}">{% endif %}

{# if / elif / else #}
{% if content_type == "seminar" %}
  <span class="badge badge-seminar">セミナー</span>{% elif content_type == "news" %}
  <span class="badge badge-news">お知らせ</span>{% elif content_type == "case" %}
  <span class="badge badge-case">事例</span>{% else %}
  <span class="badge badge-blog">ブログ</span>{% endif %}

Compound condition using {# not / and / or #}
{% if content.featured_image and content.featured_image_alt_text %}
  {# Display only when there is both image and alt #}
{% endif %}

{% if not content.archived %}
  {# Display only unarchived articles #}
{% endif %}

Frequently used comparison operators/test operators

Operator/Testmeaningexample
==equal{% if tag.slug == "news" %}
!=not equal{% if count != 0 %}
> / < / >= / <=Numerical comparison{% if loop.index <= 3 %}
and / or / notlogical operations{% if a and not b %}
starts_withPrefix match{% if tag.slug starts_with "type:" %}
ends_withSuffix match{% if url ends_with ".pdf" %}
inIs it included in a list/string?{% if "news" in tag.slug %}
is definedIs the variable defined?{% if my_var is defined %}
is not definedIs the variable undefined?{% if my_var is not defined %}
is divisibleby(n)Is it divisible by n?{% if loop.index is divisibleby(3) %}

② Loop (for/endfor)

HubL — Basic for loop pattern
{# List loop #}
{% set nav_items = [
  {"label": "Home",     "url": "/"},
  {"label": "service", "url": "/service"},
  {"label": "Company Profile", "url": "/about"}
] %}

<ul>{% for item in nav_items %}
  <li><a href="{{ item.url }}">{{ item.label }}</a></li>{% endfor %}
</ul>{# loop variable (special variable that can be used inside the loop) #}
{% for post in posts %}
  {% if loop.first %}<div class="first-item">{% endif %}

  <article class="post-item post-{{ loop.index }}">
    <h2>{{ post.name }}</h2>
  </article>

  {% if loop.last %}</div>{% endif %}
{% endfor %}

{# Numeric loop with range() #}
{% for i in range(1, 6) %}
  <span>{{ i }}</span>   {# 1, 2, 3, 4, 5 #}
{% endfor %}

{# else clause: if list is empty #}
{% for post in posts %}
  <article>...</article>{% else %}
  <p>記事がありません</p>{% endfor %}

loop variable list

variableContentexample
loop.indexCurrent index (starting from 1)1, 2, 3 …
loop.index0Current index (starting from 0)0, 1, 2 …
loop.revindexIndex from the end (starting from 1)… 3, 2, 1
loop.firstIs it the first element (true/false)?Special treatment only for the first time
loop.lastIs it the last element? (true/false)Special treatment only at the end
loop.lengthTotal number of lists10
loop.depthNesting depth (starting from 1)used in nested loops

③ unless (negative version of if)

HubL — unless
{# unless is "unless". Synonymous with if using not #}
{% unless content.archived %}
  <div class="post-content">...</div>{% endunless %}

{# The above is the same as below #}
{% if not content.archived %}
  <div class="post-content">...</div>{% endif %}

Section 2-5

Defining and reusing macros

macrois a mechanism for defining HTML patterns that are used repeatedly as functions. Since you can pass arguments, you can efficiently manage parts that "look the same but have different contents."

HubL — Defining and calling macros
{# ====== Macro definition ====== #}

{# Card component macro #}
{% macro render_card(title, description, url, badge="", image="") %}
  <div class="card">
    {% if image %}
      <img src="{{ image }}" alt="{{ title }}">
    {% endif %}
    {% if badge %}
      <span class="badge">{{ badge }}</span>
    {% endif %}
    <h3><a href="{{ url }}">{{ title }}</a></h3>
    <p>{{ description|truncate(80) }}</p>
  </div>{% endmacro %}

{# ====== Macro call ====== #}

{# Basic call #}
{{ render_card(
  title="How to achieve results with HubSpot",
  description="We will explain specific methods of marketing measures using HubSpot.",
  url="/blog/hubspot-tips"
) }}

{# Call with badge/image #}
{{ render_card(
  title="[Seminar] How to use HubSpot",
  description="This is an online seminar introducing the latest HubSpot usage examples in 2024.",
  url="/blog/seminar-2024",
  badge="Seminar",
  image="https://example.com/seminar.jpg"
) }}

{# Combine with loop in blog list #}
{% set posts = blog_recent_posts("default", 6) %}
{% for post in posts %}
  {{ render_card(
    title=post.name,
    description=post.meta_description,
    url=post.absolute_url,
    image=post.featured_image
  ) }}
{% endfor %}

Split macro into files and import

As the number of macros increases, organize them into a dedicated parts file. import or from … import You can read it with .

partials/macros.html — Macro definition file
{% macro render_card(title, description, url, badge="") %}
  ...{% endmacro %}

{% macro render_badge(label, type="default") %}
  <span class="badge badge--{{ type }}">{{ label }}</span>{% endmacro %}
templates/blog-listing.html — Macro import
{# Import and use the entire file #}
{% from "../partials/macros.html" import render_card, render_badge %}

{% set posts = blog_recent_posts("default", 9) %}
{% for post in posts %}
  {{ render_card(post.name, post.meta_description, post.absolute_url) }}
{% endfor %}

Section 2-6

Built-in variables list

HubSpot CMS templates provide many built-in variables that can be used in specific contexts. Pay attention to the scope (where you can use it).

content — content information (most important)

This is an object that contains information about the currently displayed page, article, email, etc. These are the most frequently used variables that can be used throughout the template.

variableContentscope
content.namePage/article titleAll templates
content.absolute_urlAbsolute URL of the pageAll templates
content.meta_descriptionmeta descriptionAll templates
content.featured_imageFeatured image URLAll templates
content.featured_image_alt_textEye-catching alt attributesAll templates
content.publish_datePublication date and time (UNIX timestamp)Blog
content.updatedLast updated date and timeBlog
content.tag_listArray of tags (name/slug/tag_url)Blog
content.blog_authorAuthor information objectBlog
content.blog_author.display_nameAuthor display nameBlog
content.blog_author.avatarAuthor avatar image URLBlog
content.tagTarget tags on the tag archive pagetag archive
content.tag.nametag nametag archive
content.tag.slugTag slug (part of URL)tag archive
content.portal_idHubSpot portal IDAll templates
content.languageContent language code (ja/en etc.)All templates
content.archivedArchived or not (true/false)Blog

request — Request information

variableContent
request.pathCurrent URL path (e.g. /blog/post-slug)
request.query_stringQuery string (e.g. ?page=2)
request.domainDomain (e.g. www.example.com)
request.full_urlFull URL
request.is_hubspot_userPreview for HubSpot users?

Other frequently used variables

variableContentscope
hub_idPortal ID (numeric). Often used for embedding JS in formsAll templates
site_settings.company_nameSite company name (from settings)All templates
site_settings.logo_urlSite logo image URLAll templates
contentsBlog list article array (used in listing / tag template)Blog list
contents.total_countTotal number of articles (used for pagination)Blog list
current_page_numCurrent page number (starting from 1)Blog list
last_page_numLast page numberBlog list
next_page_numNext page numberBlog list
previous_page_numprevious page numberBlog list
💡 How to check if a variable exists

When you want to check whether a specific variable can be used or has a value,{{ variable }} Temporarily write this in the template. The easiest way to check is with your browser.
Also {% if variable is defined %} Check the existence of the variable with {{ variable|default("デフォルト値") }} You can set the default value with .


Section 2-7

Complete guide to filters

A filter is a mechanism for processing and converting variable values. {{ 変数|フィルター名 }} used in the form of|You can chain multiple filters by connecting them with (pipes).

HubL — Basic syntax for filters
{# Single filter #}
{{ content.name|upper }}

{# Filter with arguments #}
{{ content.meta_description|truncate(100) }}

{# Chain (connect multiple filters) #}
{{ content.name|truncate(30)|upper }}

string manipulation filter

truncate(length, end="...")
Truncate string to specified number of characters
{{ "The complete guide to success with HubSpot"|truncate(10) }}

Output: Created with HubSpot...

upper / lower / capitalize
Capitalization / Lowercase / First capitalization
{{ "hubspot"|upper }}
→ HUBSPOT

Used to standardize the style of English content

replace(old, new)
String replacement
{{ "type:seminar"|replace("type:", "") }}
→ seminar

Often used when removing prefix from tag slug

trim
Remove leading and trailing whitespace
{{ " HubSpot "|trim }}
→ HubSpot

Used for processing form input values, etc.

striptags
Remove HTML tags and extract only text
{{ content.post_body|striptags|truncate(80) }}

Used when creating text excerpts from the article body

escape / e
HTML escape processing
{{ user_input|escape }}

Used to securely display user input values

urlencode
URL encoding
{{ "How to use HubSpot"|urlencode }}
→ HubSpot%20%E6%B4%BB...

Used when generating URL parameters

wordcount
count words
{{ content.post_body|striptags|wordcount }}

Used to calculate article reading time, etc.

date and time filter

datetimeformat(format)
Display date and time in specified format
{{ content.publish_date|datetimeformat("%Y year %m month %d day") }}
→ March 15, 2024

Required for displaying the publication date of blog articles

datetimeformat (with time)
Display including time
{{ content.publish_date|datetimeformat("%Y/%m/%d %H:%M") }}
→ 2024/03/15 14:30

If you need the time, such as the date and time of a seminar

Numeric/list filter

abs
return absolute value
{{ -5|abs }}
→ 5

Used in difference calculations, etc.

round(precision)
Round numbers
{{ 3.14159|round(2) }}
→ 3.14

Used to display prices, evaluation points, etc.

length / count
Returns the number of items in the list
{{ content.tag_list|length }}
→ 3

Used to count number of tags, number of articles, etc.

first / last
first/last element of list
{{ content.tag_list|first }}
→ First tag object

Used when obtaining only one representative tag, etc.

join(separator)
Join lists by delimiter
{{ ["A","B","C"]|join(", ") }}
→ A, B, C

Used to convert tag name list into strings, etc.

sort / sort(attribute)
sort list
{{ posts|sort(attribute="publish_date") }}

Sort by any attribute such as date or name

selectattr(attr, "equalto", val)
Filter list by attribute
{{ items|selectattr("active","equalto",true)|list }}

Used when narrowing down to valid items, etc.

map(attribute)
Extract specific attribute values ​​from list
{{ content.tag_list|map(attribute="name")|join(", ") }}

Used when getting a list of tag names as a string, etc.

💜 Filter combinations often used in practice

Article excerpt text generation:
{{ content.post_body|striptags|truncate(120, end="…") }}

Display tag names separated by commas:
{{ content.tag_list|map(attribute="name")|join(" / ") }}

Display release date in Japanese format:
{{ content.publish_date|datetimeformat("%Y年%m月%d日") }}


Section 2-8

Blog-specific HubL function list

We will organize the HubL functions required to implement the blog function. These are the most frequently used functions in blog-related templates.

Article retrieval function

functionargumentexplanation
blog_recent_posts(blog, limit) blog: blog ID or "default"
limit: Number of items retrieved
Get N latest articles. Most frequently used functions.
blog_posts(blog, limit, offset, tag) blog/limit/offset(starting position)/tag(tag slug) Get articles with specified conditions. Tag filtering is possible.
blog_popular_posts(blog, limit) blog / limit Get N popular articles (ordered by number of views).
blog_total_post_count(blog) blog Returns the total number of articles. Used in pagination calculations.
HubL — Practical pattern for retrieving articles
{# Get 6 latest articles #}
{% set recent_posts = blog_recent_posts("default", 6) %}

{# Get articles by specifying tags (seminar articles only) #}
{% set seminar_posts = blog_posts("default", 3, 0, "seminar") %}

{# Pagination using offset (2nd page: 11-20 items) #}
{% set page2_posts = blog_posts("default", 10, 10) %}

{# 5 popular articles #}
{% set popular = blog_popular_posts("default", 5) %}

{# Total number of articles #}
{% set total = blog_total_post_count("default") %}
<p>全{{ total }}件の記事</p>

URL/link generation function

functionargumentexplanation
blog_tag_url(blog, tag_slug) blog / tag_slug Returns the archive page URL for the specified tag
blog_page_link(page_num) page_num: page number Returns the paging URL for the specified page number
blog_author_url(blog, author_slug) blog / author_slug Returns URL of author archive page
HubL — Practical patterns for URL link generation
{# Generate tag archive URL #}
<a href="{{ blog_tag_url("default", "seminar") }}">セミナー一覧へ</a>{# Example output: /blog/tag/seminar #}

{# Dynamically generate tag list link in navigation #}
{% set tag_nav = [
  {"label": "Blog",         "slug": "blog"},
  {"label": "Seminar",       "slug": "seminar"},
  {"label": "notice",       "slug": "news"},
  {"label": "Introduction example",       "slug": "case"}
] %}

<nav class="blog-nav">
  <a href="/blog">すべて</a>
  {% for tag in tag_nav %}
    <a href="{{ blog_tag_url("default", tag.slug) }}"
       {% if content.tag.slug == tag.slug %}
         class="is-active" aria-current="page"
       {% endif %}>
      {{ tag.label }}
    </a>
  {% endfor %}
</nav>{# Generate pagination URL #}
<a href="{{ blog_page_link(current_page_num + 1) }}">次へ</a>

Main properties of article objects

blog_recent_posts() This is a list of properties that can be used with article objects obtained with etc.

propertiesContent
post.nameArticle title
post.absolute_urlAbsolute URL of article
post.featured_imageEye-catching image URL
post.featured_image_alt_textEye-catching alt attributes
post.meta_descriptionMeta description (used as an excerpt)
post.publish_datePublication date and time (UNIX timestamp)
post.tag_listarray of tags
post.blog_author.display_nameAuthor display name
post.blog_author.avatarAuthor avatar image URL
post.post_bodyArticle body HTML (used in conjunction with striptags)
post.read_timeEstimated reading time (minutes)

Section 2-9

How to use HubL and JavaScript

HubL and JavaScript are both used in HubSpot templates, but they have distinctly different roles. A proper understanding of the differences between the two will help you choose the appropriate implementation.

point of viewHubL (server side)JavaScript (client side)
Execution timing When requesting a page (on the server) After page loads in browser
Reaction to user interaction ❌ No (static output only) ✅ Possible (click, scroll, etc.)
Accessing HubSpot data ✅ Directly accessible △ Only through API
SEO (content visibility) ✅ Reliably recognized by search engines △ Not recognized by crawlers
Dynamic UI changes ❌ Not possible ✅ Modals, tabs, accordions, etc.
External API real-time collaboration △ Limited (excluding Enterprise) ✅ Freely collaborate with fetch / axios
HubL × JS — Cooperation pattern
{# Pass server-side data as JS variables in HubL #}
{# → Pattern to use when you want to use HubSpot data from JS #}

<script>
  // Pass HubL variables to JS
  const hubspotConfig = {
    portalId  : {{ hub_id }},
    pageId    : {{ content.id }},
    pageTitle : "{{ content.name|escape }}",
    contentType: "{{ ns.content_type }}",
    formIds   : {
      seminar  : "{{ form_ids.seminar }}",
      download : "{{ form_ids.download }}"
    }
  };

  // After this it can be processed as JS
  console.log(hubspotConfig.pageTitle);

  // Dynamically switch forms depending on content type
  hbspt.forms.create({
    portalId : hubspotConfig.portalId,
    formId   : hubspotConfig.formIds[hubspotConfig.contentType]
               || hubspotConfig.formIds.download,
    target   : '#hs-form-area'
  });
</script>
✅ Principles of proper usage

“Data confirmed at page load” → HubL(Article title, tag, publication date, URL, etc.)
“Things that change after user operation” → JavaScript(modal opening/closing, form switching, search, etc.)
“Passing HubL data to JS” → Pattern for expanding HubL variables within a script tag


Section 2-10

Chapter 2 Summary

📌 Key points to keep in mind in this chapter

three delimiters

{{ }} output / {% %} Control/Definition / {# #} comment. The basic unit of all HubLs.

namespace pattern

To update external variables within a loop namespace is required. A pattern that frequently appears in practice, such as tag judgment.

Understanding built-in variables

content / hub_id / contents / current_page_num etc. Remember it as a set with a scope (where it can be used).

Utilizing filters

datetimeformat / truncate / striptags / replace / map appears most frequently in practice. Combine with a chain.

Blog-specific functions

blog_recent_posts() / blog_posts() / blog_tag_url() / blog_page_link() is the core of blog implementation.

HubL vs JavaScript

HubL is used for data acquisition and output on the server side. User operation/interaction is JS. The linkage pattern that passes HubL variables to JS is also important.

Next chapter: Chapter 3 Template structure design

Types of templates, base templates and inheritance, and global partial design. We will explain the specific implementation patterns of 4 types of blog-related templates.

Go to Chapter 3 →