Chapter 6 · CMS Module Design
CMS Hub development——
modular design
Field definition with fields.json, HubL implementation in module.html, meta.json settings, and best practices for custom modules. Learn techniques for designing and implementing reusable components.
6-1 What is a module?
Understand the role of HubSpot modules and how they differ from templates.
🧩 Module definition
The module isReusable components that can be placed with a drag-and-drop editoris. Developers define code and edit fields, and marketers can change content with no code. All UI parts such as hero banners, CTA sections, card lists, testimonials, etc. can be modularized.
📄 Differences from templates
Templates are the skeleton of the entire page (one per page). Modules are parts that can be placed multiple times within a template. If the template is a "floor plan", the module is an image of "furniture".
🔁 Reusability
Create one module and share it with multiple templates. When you update a module, it will be reflected on all pages where it is placed (for global modules).
6-2 Module directory structure
The module is .module Consists of extension directories.
hero-banner.module/
fields.json← Editable field definition (required)
module.html← HubL template (required)
module.css← Module-specific styles (optional)
module.js← Module-specific JS (optional)
meta.json← Module meta information (required)
File naming convention:
directory name .module is an identifier. for example hero-banner.module/ , HubSpot automatically recognizes it as a module. The file name inside is fixed (fields.json・module.html・meta.json cannot be changed).
6-3 fields.json — Complete guide to field definitions
Define all fields that marketers can edit in fields.json.
| type | Purpose | return value |
text | 1 line text | string |
richtext | rich text editor | HTML string |
image | Image selection | Object (src, alt, width, height) |
url | URL/link destination | Object (url, type, open_in_new_tab) |
choice | dropdown selection | Selected value (string) |
boolean | on/off toggle | true / false |
number | Numerical input | numerical value |
color | color picker | Object (color, opacity) |
font | Font settings | Object (font, size, bold, italic…) |
group | Grouping of fields (can be listed) | object array |
hubdbrow | Select HubDB rows | row id |
cta | Choose a HubSpot CTA | CTA ID |
form | HubSpot form selection | form information |
blog | Blog selection | Blog ID |
fields.json — Hero banner module example
[
{
"id":
"heading",
"name":
"heading",
"label":
"Heading",
"type":
"text",
"default":
"Accelerate your business",
"required":
true
},
{
"id":
"subheading",
"name":
"subheading",
"label":
"Subheading",
"type":
"text",
"default":
"Unify your customer management with HubSpot"
},
{
"id":
"background_image",
"name":
"background_image",
"label":
"Background image",
"type":
"image",
"default": {
"src":
"",
"alt":
"" }
},
{
"id":
"cta_button",
"name":
"cta_button",
"label":
"CTA button",
"type":
"group",
"children": [
{
"id":
"text",
"name":
"text",
"label":
"Button text",
"type":
"text",
"default":
"Start for free" },
{
"id":
"url",
"name":
"url",
"label":
"Link destination",
"type":
"url",
"default": {
"url":
"",
"open_in_new_tab":
false } },
{
"id":
"style",
"name":
"style",
"label":
"style",
"type":
"choice",
"choices": [[
"primary",
"Primary"],[
"secondary",
"Secondary"],[
"outline",
"outline"]],
"default":
"primary" }
]
},
{
"id":
"layout",
"name":
"layout",
"label":
"Layout",
"type":
"choice",
"choices": [[
"center",
"centered"],[
"left",
"Left alignment"],[
"right",
"Right alignment"]],
"default":
"center"
},
{
"id":
"overlay_opacity",
"name":
"overlay_opacity",
"label":
"Overlay opacity (%)",
"type":
"number",
"default":
50,
"min":
0,
"max":
100
}
]
6-4 module.html — HubL template implementation
Use the fields defined in fields.json as HubL in module.html.
module.html — Hero Banner
<section
class=
"hero-banner hero-layout-{{ module.layout }}"
style=
"
{% if module.background_image.src %}
background-image: url(
{{ module.background_image.src | resize_image_url(1440, 800) }});
{% endif %}
"
>
<div
class=
"hero-overlay"
style=
"opacity: {{ module.overlay_opacity / 100 }}"
></div>
<div class=
"hero-content">
{% if module.heading %}
<h1 class=
"hero-heading">{{ module.heading }}</h1>
{% endif %}
{% if module.subheading %}
<p class=
"hero-subheading">{{ module.subheading }}</p>
{% endif %}
{% if module.cta_button.url.url %}
<a
href=
"{{ module.cta_button.url.url }}"
class=
"btn btn-{{ module.cta_button.style }}"
{% if module.cta_button.url.open_in_new_tab %}target=
"_blank" rel=
"noopener"{% endif %}
>
{{ module.cta_button.text }}
</a>
{% endif %}
</div>
</section>
module.html — List display of group field (card list)
<div class=
"card-grid card-cols-{{ module.columns }}">
{% for card in module.cards %}
<div class=
"card">
{% if card.icon %}
<div class=
"card-icon">{{ card.icon }}</div>
{% endif %}
<h3 class=
"card-title">{{ card.title }}</h3>
<p class=
"card-body">{{ card.body }}</p>
{% if card.link.url %}
<a href=
"{{ card.link.url }}" class=
"card-link">{{ card.link_text | default('Learn more') }} →
</a>
{% endif %}
</div>
{% endfor %}
</div>
6-5 meta.json — Module meta information
Set the module's display name, available contexts, icons, etc.
meta.json
{
"label":
"Hero Banner",
"css_assets": [{
"path":
"module.css" }],
"js_assets": [],
"other_assets": [],
"smart_type":
"NOT_SMART",
"icon":
"heroicons/solid/photograph",
"is_available_for_new_content":
true,
"content_types": [
"SITE_PAGE",
"LANDING_PAGE"],
"host_template_types": [
"PAGE",
"BLOG_POST"]
}
| content_types value | explanation |
SITE_PAGE | site page |
LANDING_PAGE | landing page |
BLOG_POST | Blog article |
BLOG_LISTING | Blog list |
EMAIL | marketing email |
6-6 Best practices for module.css / module.js
Properly scope module-specific styles and JS.
module.css — scoped styles
.hero-banner {
position: relative;
min-height:
560px;
display: flex;
align-items: center;
justify-content: center;
background-size: cover;
background-position: center;
overflow: hidden;
}
.hero-overlay {
position: absolute; inset:
0;
background: #000;
}
.hero-content {
position: relative; z-index:
1;
text-align: center; max-width:
720px;
padding:
40px
24px;
color: #fff;
}
.hero-layout-left .hero-content { text-align: left; }
.hero-layout-right .hero-content { text-align: right; }
@media (max-width:
768px) {
.hero-banner { min-height:
320px; }
}
module.js — Initialization pattern
(
function() {
function initHeroBanner() {
const banners = document.querySelectorAll(
'.hero-banner');
banners.forEach(
function(banner) {
if (banner.dataset.initialized)
return;
banner.dataset.initialized =
'true';
});
}
if (document.readyState ===
'loading') {
document.addEventListener(
'DOMContentLoaded', initHeroBanner);
}
else {
initHeroBanner();
}
})();
⚠ Be careful about polluting global variables:
Wrap module.js in an immediate function (IIFE) to avoid polluting the global scope. When multiple modules are placed on the same page, variable names can conflict.
6-7 Summary of this chapter
✅ Chapter 6 Checklist
- Understood the module's 4-file structure (fields.json/module.html/module.css/meta.json)
- Be able to use the main field types of fields.json (text / image / url / choice / group)
- You can design repeatable components using the group field.
- in module.html
module.フィールド名 can be viewed on HubL
- You can appropriately limit the available contexts in meta.json
- Scope JS with IIFE pattern to prevent global pollution
About the next chapter (Chapter 7):Learn workflow extensions and custom code actions. We will explain Custom Code Action, Serverless Functions, and Run Agent steps using Node.js.