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

fields.json complete guide
HubL module.html
Time required: Approximately 90 minutes
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.jsonmodule.htmlmeta.json cannot be changed).
6-3 fields.json — Complete guide to field definitions

Define all fields that marketers can edit in fields.json.

typePurposereturn value
text1 line textstring
richtextrich text editorHTML string
imageImage selectionObject (src, alt, width, height)
urlURL/link destinationObject (url, type, open_in_new_tab)
choicedropdown selectionSelected value (string)
booleanon/off toggletrue / false
numberNumerical inputnumerical value
colorcolor pickerObject (color, opacity)
fontFont settingsObject (font, size, bold, italic…)
groupGrouping of fields (can be listed)object array
hubdbrowSelect HubDB rowsrow id
ctaChoose a HubSpot CTACTA ID
formHubSpot form selectionform information
blogBlog selectionBlog 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
{# Get field value with module_attribute #} <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 %} " > {# Background Overlay #} <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)
{# Repeat group with type: group, occurrence: {min:1, max:6} set in fields.json #} <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, // Limit the contexts in which this module can be used "content_types": ["SITE_PAGE", "LANDING_PAGE"], // Only available in DnD templates "host_template_types": ["PAGE", "BLOG_POST"] }
content_types valueexplanation
SITE_PAGEsite page
LANDING_PAGElanding page
BLOG_POSTBlog article
BLOG_LISTINGBlog list
EMAILmarketing email
6-6 Best practices for module.css / module.js

Properly scope module-specific styles and JS.

module.css — scoped styles
/* Module-specific styles are scoped in the .hero-banner class */ .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
// Execute immediately without waiting for DOMContentLoaded (compatible with HubSpot's asynchronous loading) (function() { function initHeroBanner() { const banners = document.querySelectorAll('.hero-banner'); banners.forEach(function(banner) { // Skip if already initialized if (banner.dataset.initialized) return; banner.dataset.initialized = 'true'; // Initialization process... }); } 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.