🟢 HubSpot Developer Practical Textbook — 2026 Edition Developer Edition
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.

HubL template language
CLI v8 watch mode
Time required: Approximately 120 minutes
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 onTemplatePartialModuleIt 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.

componentrolefile extensioneditor
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, // Define fields that marketers can edit "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.

syntaxPurposeexample
{{ ... }} 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
{# Variable output #} <h1>{{ content.title }}</h1> <meta name="description" content="{{ content.meta_description }}"> {# Conditional branch #} {% if content.featured_image %} <img src="{{ content.featured_image }}" alt="{{ content.featured_image_alt_text }}"> {% endif %} {# loop #} {% for tag in content.tag_list %} <span class="tag">{{ tag.name }}</span> {% endfor %} {# Filter #} {{ content.title | truncate(60) }} {{ post.publish_date | datetimeformat('%Y year %m month %d day') }} {{ price | number_format(0, ',', '.') }} {# Using theme.json fields for CSS #} <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
{# Template meta settings #} <!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 }}"> {# OGP tag #} <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 %} {# Load theme CSS #} {{ require_css("{{ get_asset_url('./css/main.css') }}") }} {# Inject color from theme.json as CSS variable #} <style> :root { --color-primary: {{ theme.primary_color.color }}; --color-secondary: {{ theme.secondary_color.color }}; } </style> </head> <body> {# Global header (common to all pages) #} {% global_partial path="./partials/header.html" %} <main id="main-content"> {# Drag and Drop Area #} {% dnd_area "main_content" label="Main content", class="page-main-content" %} {# Module placed by default #} {% 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 footer #} {% 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)
{# templateType: blog_post #} <article class="blog-post"> <header class="post-header"> {# Category Tag #} {% 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> {# Body #} <div class="post-body"> {{ content.post_body }} </div> {# Related articles (Display 3 articles with the same tag) #} {% 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
{# Declare isGlobalPartial: true with a comment #} {# templateType: global_partial label: global header isAvailableForNewContent: false #} <header class="site-header"> <div class="header-inner"> {# Logo (taken from theme.json) #} <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> {# Navigation (editable by marketers) #} {% module "navigation" path="@hubspot/simple_menu", label="Main navigation", overrideable=true %} {# CTA button (editable by marketers) #} {% 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"> {# Footer logo #} <a href="/"> <img src="{{ theme.site_logo.src }}" alt="{{ theme.site_logo.alt }}"> </a> {# SNS links (taken from social_links group in theme.json) #} <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> {# Copyright (Dynamically display current year) #} <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.

categoryHubL expressionexplanation
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
{# Get and list store data from HubDB table #} {% 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
/* HubSpot theme recommended breakpoints */ :root { --breakpoint-sm: 576px; --breakpoint-md: 768px; --breakpoint-lg: 992px; --breakpoint-xl: 1200px; } /* Mobile first (override with min-width) */ .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; } } /* Coexistence with HubSpot DnD grid */ .dnd-section { width: 100%; } .dnd-column { min-width: 0; }
HubL — Responsive images and lazy loading
{# Automatically resize with HubSpot's resize_image_url filter #} <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 measuresImplementation 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. 1
    Download themes in Sandbox hs cms fetch "my-theme" ./themes/my-theme --portal dev-sandbox retrieved locally.
  2. 2
    Start developing in Watch mode hs cms watch ./themes/my-theme my-theme --portal dev-sandbox Automatically upload when saving a file.
  3. 3
    Check preview in browser Check out the actual render with HubSpot's Preview feature. Also check responsiveness using developer tools.
  4. 4
    Commit to Git Commit & push changes. Create a PR and receive team review.
  5. 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.