Chapter 5 · CMS Theme & Template Development
CMS Hub development——
Themes & Templates
HubSpot theme structure, HubL template language practice, global partials,
to responsive design. Systematically learn the overall picture of CMS development that front-end developers should know.
5-1 HubSpot CMS development model
Organize the rendering structure of the HubSpot CMS and the layers that developers can interact with.
🏗 CMS Components
HubSpot CMS isThemeBased onTemplate・
Partial・ModuleIt consists of three layers.
Page content can be edited using a drag-and-drop editor, and developers can use HTML, CSS, HubL, and JavaScript to
Build that foundation.
| component | role | file extension | editor |
| theme |
Root container that manages the design and settings of the entire site |
theme.json |
developer |
| template |
Page layout skeleton (header, footer, main area) |
.html(HubL) |
developer |
| global partial |
Parts common to all pages such as headers and footers |
.html(HubL) |
Developer/Marketer |
| module |
Reusable components that can be placed using drag and drop |
.module/(directory) |
developer |
| CSS / JS |
style interaction |
.css / .js |
developer |
Design Manager vs CLI:
You can also develop using HubSpot's Design Manager (file editor on your browser).
Local editor (VS Code etc.) + CLI development is overwhelmingly efficient.
We recommend CLI development, which allows version control using Git, team development, and CI/CD automatic deployment.
5-2 Theme directory structure
Understand the standard theme structure and the role of each file.
my-theme/
theme.json← Theme settings/field definitions
templates/← Page template
home.html← For top page
page.html← For general purpose pages
blog-listing.html← Blog list
blog-post.html← Blog article details
landing-page.html← LP (form etc.)
error-page.html← 404 etc.
partials/← Common parts
header.html
footer.html
navigation.html
modules/← Custom module (detailed in Chapter 6)
hero-banner.module/
cta-section.module/
css/
main.css← Main style sheet
theme-overrides.css
js/
main.js
images/
logo.svg
fonts/
Design manager path:
When uploading with the CLI, the path on the HubSpot side is
@hubspot/my-theme/templates/page.html like
@hubspot/ Prefix + folder name.
hs cms upload ./my-theme my-theme The second argument will be the root path on the HubSpot side.
5-3 theme.json settings
Define theme-wide settings, editable fields, and color palettes.
theme.json
{
"label":
"My Company Theme",
"preview_path":
"./images/preview.png",
"author": {
"name":
"My Company",
"url":
"https://example.com" },
"version":
"1.0.0",
"isAvailableForNewContent":
true,
"fields": [
{
"type":
"font",
"label":
"Heading font",
"name":
"heading_font",
"default": {
"font":
"Noto Sans JP",
"font_set":
"GOOGLE",
"size":
36,
"size_unit":
"px",
"bold":
true
}
},
{
"type":
"color",
"label":
"Brand color (primary)",
"name":
"primary_color",
"default": {
"color":
"#10b981" }
},
{
"type":
"color",
"label":
"Brand color (secondary)",
"name":
"secondary_color",
"default": {
"color":
"#059669" }
},
{
"type":
"image",
"label":
"Site logo",
"name":
"site_logo",
"default": {
"src":
"",
"alt":
"Company Logo" }
},
{
"type":
"group",
"label":
"SNS Link",
"name":
"social_links",
"children": [
{
"type":
"text",
"label":
"Twitter / X",
"name":
"twitter",
"default":
"" },
{
"type":
"text",
"label":
"LinkedIn",
"name":
"linkedin",
"default":
"" }
]
}
]
}
🎨 theme.json field type list
color — Color Picker |
font — Font/Size/Style |
text — Text input |
number — Numerical input |
boolean — On/Off |
image — Image selection |
url — URL input |
choice — Dropdown |
group — Field grouping
5-4 Fundamentals of HubL template language
HubL (HubSpot Markup Language) is a template engine based on Jinja2. Learn syntax and major tags.
| syntax | Purpose | example |
{{ ... }} |
Output of variables/expressions |
{{ content.title }} |
{% ... %} |
Tags (control syntax/functions) |
{% if ... %}{% endif %} |
{# ... #} |
Comment (not output) |
{# TODO: 後で修正 #} |
{{ theme.フィールド名 }} |
Get theme.json field value |
{{ theme.primary_color.color }} |
{% module %} |
Place module |
{% module "hero" path="./modules/hero-banner" %} |
{% dnd_area %} |
Define drag and drop area |
Defining the content area |
{% include %} |
Load partial |
{% include "./partials/header.html" %} |
{% global_partial %} |
Place global partial |
header/footer common to all pages |
HubL — Basic syntax
<h1>{{ content.title }}</h1>
<meta name=
"description" content=
"{{ content.meta_description }}">
{% if content.featured_image %}
<img src=
"{{ content.featured_image }}" alt=
"{{ content.featured_image_alt_text }}">
{% endif %}
{% for tag in content.tag_list %}
<span class=
"tag">{{ tag.name }}</span>
{% endfor %}
{{ content.title | truncate(60) }}
{{ post.publish_date | datetimeformat('%Y year %m month %d day') }}
{{ price | number_format(0, ',', '.') }}
<style>
:root {
--primary-color:
{{ theme.primary_color.color }};
--secondary-color:
{{ theme.secondary_color.color }};
--heading-font:
"{{ theme.heading_font.font }}", sans-serif;
}
</style>
5-5 Implementation of template file
Implement general-purpose page templates and blog post templates.
templates/page.html — Generic page template
<!DOCTYPE html>
<html lang=
"ja">
<head>
<meta charset=
"UTF-8">
<meta name=
"viewport" content=
"width=device-width, initial-scale=1.0">
<title>{{ content.html_title }}</title>
<meta name=
"description" content=
"{{ content.meta_description }}">
<meta property=
"og:title" content=
"{{ content.html_title }}">
<meta property=
"og:description" content=
"{{ content.meta_description }}">
{% if content.featured_image %}
<meta property=
"og:image" content=
"{{ content.featured_image }}">
{% endif %}
{{ require_css("{{ get_asset_url('./css/main.css') }}") }}
<style>
:root {
--color-primary:
{{ theme.primary_color.color }};
--color-secondary:
{{ theme.secondary_color.color }};
}
</style>
</head>
<body>
{% global_partial path="./partials/header.html" %}
<main id=
"main-content">
{% dnd_area "main_content"
label="Main content",
class="page-main-content"
%}
{% dnd_section %}
{% dnd_column %}
{% dnd_row %}
{% dnd_module "rich_text"
path="@hubspot/rich_text",
label="rich text"
%}
{% end_dnd_row %}
{% end_dnd_column %}
{% end_dnd_section %}
{% end_dnd_area %}
</main>
{% global_partial path="./partials/footer.html" %}
{{ require_js("{{ get_asset_url('./js/main.js') }}", "footer") }}
</body>
</html>
templates/blog-post.html — Blog article template (excerpt)
<article class=
"blog-post">
<header class=
"post-header">
{% if content.tag_list %}
<div class=
"post-tags">
{% for tag in content.tag_list %}
<a href=
"{{ tag.url }}" class=
"tag">{{ tag.name }}</a>
{% endfor %}
</div>
{% endif %}
<h1>{{ content.name }}</h1>
<div class=
"post-meta">
<time>{{ content.publish_date | datetimeformat('%Y year % m month % d day') }}</time>
{% if content.blog_post_author %}
<span class=
"author">{{ content.blog_post_author.display_name }}</span>
{% endif %}
</div>
{% if content.featured_image %}
<img
src=
"{{ content.featured_image }}"
alt=
"{{ content.featured_image_alt_text }}"
class=
"post-featured-image"
loading=
"lazy"
>
{% endif %}
</header>
<div class=
"post-body">
{{ content.post_body }}
</div>
{% set related_posts = blog_recent_tag_posts(
group.id, content.tag_list[0].id, 3
) %}
{% if related_posts %}
<section class=
"related-posts">
<h2>Related articles
</h2>
{% for post in related_posts %}
<a href=
"{{ post.absolute_url }}">{{ post.name }}</a>
{% endfor %}
</section>
{% endif %}
</article>
5-6 Implementation of global partial
Implement parts that are common to all pages, such as headers and footers, in a way that marketers can edit them from the management screen.
🌍 Features of global partials
Global partial ({% global_partial %}), if you update one locationImmediately reflected on all pageswill be done.
Marketers can change navigation links and footer text from the HubSpot edit screen without touching any code.
Developers place editable modules in a partial in advance.
partials/header.html — global header
<header class=
"site-header">
<div class=
"header-inner">
<a href=
"/" class=
"site-logo">
{% if theme.site_logo.src %}
<img
src=
"{{ theme.site_logo.src }}"
alt=
"{{ theme.site_logo.alt }}"
width=
"160" height=
"40"
>
{% else %}
<span class=
"logo-text">{{ site_settings.company_name }}</span>
{% endif %}
</a>
{% module "navigation"
path="@hubspot/simple_menu",
label="Main navigation",
overrideable=true
%}
{% module "header_cta"
path="@hubspot/cta",
label="Header CTA",
overrideable=true
%}
</div>
</header>
partials/footer.html — global footer (excerpt)
<footer class=
"site-footer">
<div class=
"footer-inner">
<a href=
"/">
<img src=
"{{ theme.site_logo.src }}" alt=
"{{ theme.site_logo.alt }}">
</a>
<div class=
"social-links">
{% if theme.social_links.twitter %}
<a href=
"{{ theme.social_links.twitter }}" target=
"_blank" rel=
"noopener">X
</a>
{% endif %}
{% if theme.social_links.linkedin %}
<a href=
"{{ theme.social_links.linkedin }}" target=
"_blank" rel=
"noopener">LinkedIn
</a>
{% endif %}
</div>
<p class=
"copyright">
©
{{ "now"|datetimeformat("%Y") }} {{ site_settings.company_name }}
</p>
</div>
</footer>
5-7 Practical HubL techniques
Understand frequently used HubL functions, filters, and variables at once.
| category | HubL expression | explanation |
| string filter |
{{ text | truncate(100) }} |
Truncate to 100 characters (at the end...) |
{{ text | striptags }} |
Remove HTML tags |
{{ text | escape }} |
HTML escape |
{{ text | lower }} |
convert to lower case |
| date filter |
{{ date | datetimeformat('%Y年%m月%d日') }} |
date format |
{{ "now" | datetimeformat('%Y') }} |
Get current year |
{{ date | timeago }} |
Display in "3 days ago" format |
| Page information |
{{ content.absolute_url }} |
Absolute URL of the current page |
{{ content.html_title }} |
page title |
{{ request.path }} |
Current path (e.g. /about) |
| assets |
{{ get_asset_url('./css/main.css') }} |
Get URL of in-theme asset |
{{ require_css("URL") }} |
Load CSS into head |
{{ require_js("URL", "footer") }} |
Load JS at the end of the body |
| blog function |
{% blog_recent_posts "blog" 5 %} |
Get the latest 5 articles |
{% blog_recent_tag_posts group.id tag.id 3 %} |
Get 3 articles with the same tag |
HubL — Display HubDB data in templates
{% set store_table = hubdb_table_rows("store_locations", "is_open=true&orderBy=store_name") %}
<div class=
"store-list">
{% for store in store_table %}
<div class=
"store-card">
<h3>{{ store.store_name }}</h3>
<p>{{ store.address }}</p>
<p>{{ store.phone }}</p>
{% if store.latitude and store.longitude %}
<a
href=
"https://maps.google.com/?q={{ store.latitude }},{{ store.longitude }}"
target=
"_blank"
>See map
</a>
{% endif %}
</div>
{% endfor %}
</div>
5-8 Responsive design and performance optimization
Learn mobile-first CSS design and image optimization patterns in HubSpot CMS.
CSS — Mobile-first breakpoint design
:root {
--breakpoint-sm:
576px;
--breakpoint-md:
768px;
--breakpoint-lg:
992px;
--breakpoint-xl:
1200px;
}
.hero-section {
padding:
40px
16px;
flex-direction: column;
}
@media (min-width:
768px) {
.hero-section {
padding:
80px
40px;
flex-direction: row;
gap:
48px;
}
}
@media (min-width:
1200px) {
.hero-section {
padding:
120px
80px;
}
}
.dnd-section { width:
100%; }
.dnd-column { min-width:
0; }
HubL — Responsive images and lazy loading
<picture>
<source
media=
"(min-width: 768px)"
srcset=
"{{ image.src | resize_image_url(1200, 630) }}"
>
<source
media=
"(min-width: 375px)"
srcset=
"{{ image.src | resize_image_url(768, 400) }}"
>
<img
src=
"{{ image.src | resize_image_url(375, 200) }}"
alt=
"{{ image.alt }}"
loading=
"lazy"
decoding=
"async"
width=
"1200"
height=
"630"
>
</picture>
| Performance measures | Implementation method |
| Lazy loading of images |
loading="lazy" Always give attributes |
| Automatic image resizing |
resize_image_url(w, h) Deliver only the required size with filters |
| Asynchronous loading of CSS |
Non-critical CSS is require_css at the end of the body |
| JS defer |
require_js("URL", "footer") and place it at the end of body |
| Leverage HubSpot CDN |
Theme assets are automatically served from the HubSpot CDN |
| Font optimization |
font-display: swap + Prevent FOUT by specifying a subset |
5-9 Practicing local development flow
Based on the CLI knowledge from Chapter 1, organize the specific flow of theme development.
-
1
Download themes in Sandbox
hs cms fetch "my-theme" ./themes/my-theme --portal dev-sandbox retrieved locally.
-
2
Start developing in Watch mode
hs cms watch ./themes/my-theme my-theme --portal dev-sandbox Automatically upload when saving a file.
-
3
Check preview in browser
Check out the actual render with HubSpot's Preview feature. Also check responsiveness using developer tools.
-
4
Commit to Git
Commit & push changes. Create a PR and receive team review.
-
5
Deploy to production with GitHub Actions
automatically on main merge hs cms upload is executed and reflected in the actual production.
⚠ Do not specify the production portal during watch:
hs cms watch During execution, it will be uploaded every time you save.
surely --portal dev-sandbox Specify the development portal like this,
Avoid accidentally uploading to the production portal.
5-10 Summary of this chapter
Please confirm before proceeding to the next chapter (CMS Hub Development—Module Design).
✅ Chapter 5 Checklist
- Can explain the components of HubSpot CMS (themes, templates, partials, modules)
- Understand the standard theme directory structure
- Font, color, and image fields can be defined in theme.json
- Now able to use HubL's basic syntax (variables, conditions, loops, filters)
- Can implement a general-purpose page template (page.html)
- Can implement blog article template (blog-post.html)
{% dnd_area %} You can define drag and drop areas with
- Headers and footers can be implemented with global partials
hubdb_table_rows() You can display HubDB data in templates with
resize_image_url You can implement responsive images with filters.
- You can practice local development flow using Watch mode
About the next chapter (Chapter 6):
Learn about the modular design of CMS Hub.fields.json Field definition by
module.html HubL implementation inmeta.json Settings/
A practical explanation of best practices for custom modules.