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.
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.
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.
HubL has three types of notation for different purposes. This is the basic unit of HubL.
{# ====== ① 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 #}
{% set %} Define variables with tags. Once defined, variables can be reused within the template.
{# 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 #}
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.
{# 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>
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.
{# 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 %}
| Operator/Test | meaning | example |
|---|---|---|
| == | equal | {% if tag.slug == "news" %} |
| != | not equal | {% if count != 0 %} |
| > / < / >= / <= | Numerical comparison | {% if loop.index <= 3 %} |
| and / or / not | logical operations | {% if a and not b %} |
| starts_with | Prefix match | {% if tag.slug starts_with "type:" %} |
| ends_with | Suffix match | {% if url ends_with ".pdf" %} |
| in | Is it included in a list/string? | {% if "news" in tag.slug %} |
| is defined | Is the variable defined? | {% if my_var is defined %} |
| is not defined | Is the variable undefined? | {% if my_var is not defined %} |
| is divisibleby(n) | Is it divisible by n? | {% if loop.index is divisibleby(3) %} |
{# 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 %}
| variable | Content | example |
|---|---|---|
| loop.index | Current index (starting from 1) | 1, 2, 3 … |
| loop.index0 | Current index (starting from 0) | 0, 1, 2 … |
| loop.revindex | Index from the end (starting from 1) | … 3, 2, 1 |
| loop.first | Is it the first element (true/false)? | Special treatment only for the first time |
| loop.last | Is it the last element? (true/false) | Special treatment only at the end |
| loop.length | Total number of lists | 10 |
| loop.depth | Nesting depth (starting from 1) | used in nested loops |
{# 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 %}
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."
{# ====== 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 %}
As the number of macros increases, organize them into a dedicated parts file. import or from … import You can read it with .
{% macro render_card(title, description, url, badge="") %} ...{% endmacro %} {% macro render_badge(label, type="default") %} <span class="badge badge--{{ type }}">{{ label }}</span>{% endmacro %}
{# 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 %}
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).
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.
| variable | Content | scope |
|---|---|---|
| content.name | Page/article title | All templates |
| content.absolute_url | Absolute URL of the page | All templates |
| content.meta_description | meta description | All templates |
| content.featured_image | Featured image URL | All templates |
| content.featured_image_alt_text | Eye-catching alt attributes | All templates |
| content.publish_date | Publication date and time (UNIX timestamp) | Blog |
| content.updated | Last updated date and time | Blog |
| content.tag_list | Array of tags (name/slug/tag_url) | Blog |
| content.blog_author | Author information object | Blog |
| content.blog_author.display_name | Author display name | Blog |
| content.blog_author.avatar | Author avatar image URL | Blog |
| content.tag | Target tags on the tag archive page | tag archive |
| content.tag.name | tag name | tag archive |
| content.tag.slug | Tag slug (part of URL) | tag archive |
| content.portal_id | HubSpot portal ID | All templates |
| content.language | Content language code (ja/en etc.) | All templates |
| content.archived | Archived or not (true/false) | Blog |
| variable | Content |
|---|---|
| request.path | Current URL path (e.g. /blog/post-slug) |
| request.query_string | Query string (e.g. ?page=2) |
| request.domain | Domain (e.g. www.example.com) |
| request.full_url | Full URL |
| request.is_hubspot_user | Preview for HubSpot users? |
| variable | Content | scope |
|---|---|---|
| hub_id | Portal ID (numeric). Often used for embedding JS in forms | All templates |
| site_settings.company_name | Site company name (from settings) | All templates |
| site_settings.logo_url | Site logo image URL | All templates |
| contents | Blog list article array (used in listing / tag template) | Blog list |
| contents.total_count | Total number of articles (used for pagination) | Blog list |
| current_page_num | Current page number (starting from 1) | Blog list |
| last_page_num | Last page number | Blog list |
| next_page_num | Next page number | Blog list |
| previous_page_num | previous page number | Blog list |
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 .
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).
{# Single filter #} {{ content.name|upper }} {# Filter with arguments #} {{ content.meta_description|truncate(100) }} {# Chain (connect multiple filters) #} {{ content.name|truncate(30)|upper }}
Output: Created with HubSpot...
Used to standardize the style of English content
Often used when removing prefix from tag slug
Used for processing form input values, etc.
Used when creating text excerpts from the article body
Used to securely display user input values
Used when generating URL parameters
Used to calculate article reading time, etc.
Required for displaying the publication date of blog articles
If you need the time, such as the date and time of a seminar
Used in difference calculations, etc.
Used to display prices, evaluation points, etc.
Used to count number of tags, number of articles, etc.
Used when obtaining only one representative tag, etc.
Used to convert tag name list into strings, etc.
Sort by any attribute such as date or name
Used when narrowing down to valid items, etc.
Used when getting a list of tag names as a string, etc.
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日") }}
We will organize the HubL functions required to implement the blog function. These are the most frequently used functions in blog-related templates.
| function | argument | explanation |
|---|---|---|
| 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. |
{# 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>
| function | argument | explanation |
|---|---|---|
| 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 |
{# 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>
blog_recent_posts() This is a list of properties that can be used with article objects obtained with etc.
| properties | Content |
|---|---|
| post.name | Article title |
| post.absolute_url | Absolute URL of article |
| post.featured_image | Eye-catching image URL |
| post.featured_image_alt_text | Eye-catching alt attributes |
| post.meta_description | Meta description (used as an excerpt) |
| post.publish_date | Publication date and time (UNIX timestamp) |
| post.tag_list | array of tags |
| post.blog_author.display_name | Author display name |
| post.blog_author.avatar | Author avatar image URL |
| post.post_body | Article body HTML (used in conjunction with striptags) |
| post.read_time | Estimated reading time (minutes) |
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 view | HubL (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 |
{# 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>
“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
{{ }} output / {% %} Control/Definition / {# #} comment. The basic unit of all HubLs.
To update external variables within a loop namespace is required. A pattern that frequently appears in practice, such as tag judgment.
content / hub_id / contents / current_page_num etc. Remember it as a set with a scope (where it can be used).
datetimeformat / truncate / striptags / replace / map appears most frequently in practice. Combine with a chain.
blog_recent_posts() / blog_posts() / blog_tag_url() / blog_page_link() is the core of blog implementation.
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.