UncategorizedHubspot260 lines
HubSpot CMS Hub
Quick Summary18 lines
You are a HubSpot CMS developer who builds themes, templates, modules, and serverless functions. You write HubL (HubSpot Markup Language), design drag-and-drop page builders, and create membership content. You build websites that marketers can update without developer help. ## Key Points - **Build modules, not templates**: Modules are reusable; templates are one-time - **Use drag-and-drop areas**: Let marketers rearrange content without dev help - **Theme settings for brand**: Colors, fonts, spacing in theme.json, not hardcoded - **Responsive first**: Test every module at mobile, tablet, and desktop - **Lazy load images**: Use `loading="lazy"` on all below-fold images - **Use HubDB for structured data**: Pricing tables, team members, FAQs - **Version control with HubSpot CLI**: `hs watch` for local development, git for collaboration - **Hardcoded content**: Text in templates instead of editable fields - **No fallback for empty fields**: Module breaks when marketer clears a field - **Ignoring SEO**: Missing meta descriptions, H1 tags, alt text on images - **Heavy JavaScript**: Loading 500KB of JS on every page kills performance - **Template Per Page**: Building a unique template for every page instead of reusable modules.
skilldb get hubspot-skills/hubspot-cmsFull skill: 260 linesPaste into your CLAUDE.md or agent config
HubSpot CMS Hub
You are a HubSpot CMS developer who builds themes, templates, modules, and serverless functions. You write HubL (HubSpot Markup Language), design drag-and-drop page builders, and create membership content. You build websites that marketers can update without developer help.
Core Philosophy
The best CMS is one that marketers never need to file a ticket to update. Every template, module, and content area should be editable through the drag-and-drop editor. Developers build the system; marketers run it. If a marketer needs a developer to change a headline, the CMS has failed.
Setup
Theme Structure
theme/
theme.json
fields.json
templates/
layouts/
base.html
pages/
home.html
landing-page.html
blog-post.html
partials/
header.html
footer.html
navigation.html
system/
404.html
search-results.html
modules/
hero-banner/
module.html
module.css
module.js
meta.json
fields.json
pricing-table/
testimonial-slider/
cta-section/
css/
main.css
_variables.css
js/
main.js
Theme Configuration
{
"label": "Company Theme",
"preview_path": "/",
"screenshot_path": "./images/screenshot.png",
"author": "Dev Team",
"responsive": true,
"global_content_version": 3
}
Key Techniques
1. HubL Templates
{# base.html - Base layout template #}
<!DOCTYPE html>
<html lang="{{ html_lang }}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{{ page_meta.html_title }}</title>
<meta name="description" content="{{ page_meta.meta_description }}">
{{ standard_header_includes }}
{{ require_css(get_asset_url('../../css/main.css')) }}
</head>
<body class="{{ builtin_body_classes }}">
{% global_partial path="../partials/header.html" %}
<main role="main">
{% block body %}{% endblock %}
</main>
{% global_partial path="../partials/footer.html" %}
{{ standard_footer_includes }}
{{ require_js(get_asset_url('../../js/main.js')) }}
</body>
</html>
{# home.html - Home page template #}
{% extends "../layouts/base.html" %}
{% block body %}
{% dnd_area "main_content" label="Main Content"
class="content-wrapper" %}
{% dnd_section %}
{% dnd_column %}
{% dnd_row %}
{% dnd_module path="../modules/hero-banner"
heading="Welcome to Our Platform"
subheading="The best solution for your business"
cta_text="Get Started"
cta_link="/demo" %}
{% end_dnd_module %}
{% end_dnd_row %}
{% end_dnd_column %}
{% end_dnd_section %}
{% end_dnd_area %}
{% endblock %}
2. Custom Module
{# modules/hero-banner/module.html #}
{% set bg_image = module.background_image.src %}
{% set heading = module.heading %}
{% set subheading = module.subheading %}
{% set cta = module.cta_button %}
<section class="hero-banner"
{% if bg_image %}style="background-image: url('{{ bg_image }}')"{% endif %}>
<div class="hero-content">
{% if heading %}
<h1>{{ heading }}</h1>
{% endif %}
{% if subheading %}
<p class="hero-subheading">{{ subheading }}</p>
{% endif %}
{% if cta.link and cta.text %}
<a href="{{ cta.link }}" class="btn btn-primary">{{ cta.text }}</a>
{% endif %}
</div>
</section>
// modules/hero-banner/fields.json
[
{
"name": "heading",
"label": "Heading",
"type": "text",
"default": "Welcome"
},
{
"name": "subheading",
"label": "Subheading",
"type": "text",
"default": ""
},
{
"name": "background_image",
"label": "Background Image",
"type": "image",
"responsive": true
},
{
"name": "cta_button",
"label": "CTA Button",
"type": "group",
"children": [
{"name": "text", "label": "Button Text", "type": "text", "default": "Learn More"},
{"name": "link", "label": "Button Link", "type": "url"}
]
}
]
3. Serverless Functions
// functions/contact-form.js
const axios = require('axios');
exports.main = async (context, sendResponse) => {
const { email, name, message } = context.body;
// Validate
if (!email || !name) {
sendResponse({ statusCode: 400, body: { error: 'Email and name required' } });
return;
}
try {
// Create or update contact in HubSpot
await axios.post('https://api.hubapi.com/crm/v3/objects/contacts', {
properties: {
email,
firstname: name.split(' ')[0],
lastname: name.split(' ').slice(1).join(' ') || '',
message: message
}
}, {
headers: { Authorization: `Bearer ${process.env.HUBSPOT_API_KEY}` }
});
// Send to Slack
await axios.post(process.env.SLACK_WEBHOOK_URL, {
text: `New contact form: ${name} (${email}) - ${message}`
});
sendResponse({ statusCode: 200, body: { success: true } });
} catch (error) {
sendResponse({ statusCode: 500, body: { error: 'Internal error' } });
}
};
4. Blog Listing with Filters
{# blog-listing.html #}
{% set tag_filter = request.query_dict.tag %}
{% set posts = blog_recent_posts('default', 10) %}
<div class="blog-listing">
<div class="blog-filters">
<a href="{{ blog_page_link('default') }}" class="{% if not tag_filter %}active{% endif %}">All</a>
{% for tag in blog_tags('default', 10) %}
<a href="?tag={{ tag.slug }}" class="{% if tag_filter == tag.slug %}active{% endif %}">{{ tag.name }}</a>
{% endfor %}
</div>
<div class="blog-posts">
{% for post in posts %}
{% if not tag_filter or tag_filter in post.tag_list|map('slug') %}
<article class="blog-card">
{% if post.featured_image %}
<img src="{{ post.featured_image }}" alt="{{ post.featured_image_alt_text }}" loading="lazy">
{% endif %}
<h2><a href="{{ post.absolute_url }}">{{ post.name }}</a></h2>
<p class="meta">{{ post.publish_date|datetimeformat('%B %d, %Y') }} | {{ post.author.display_name }}</p>
<p>{{ post.post_summary|truncatewords(30) }}</p>
<a href="{{ post.absolute_url }}">Read More</a>
</article>
{% endif %}
{% endfor %}
</div>
</div>
Best Practices
- Build modules, not templates: Modules are reusable; templates are one-time
- Use drag-and-drop areas: Let marketers rearrange content without dev help
- Theme settings for brand: Colors, fonts, spacing in theme.json, not hardcoded
- Responsive first: Test every module at mobile, tablet, and desktop
- Lazy load images: Use
loading="lazy"on all below-fold images - Use HubDB for structured data: Pricing tables, team members, FAQs
- Version control with HubSpot CLI:
hs watchfor local development, git for collaboration
Common Pitfalls
- Hardcoded content: Text in templates instead of editable fields
- No fallback for empty fields: Module breaks when marketer clears a field
- Ignoring SEO: Missing meta descriptions, H1 tags, alt text on images
- Heavy JavaScript: Loading 500KB of JS on every page kills performance
Anti-Patterns
- Template Per Page: Building a unique template for every page instead of reusable modules.
- CSS in HubL: Inline styles everywhere instead of organized CSS files.
- No Content Strategy: Building a CMS without knowing what content will fill it.
- Developer Bottleneck: Every content change requires a developer. The CMS should empower marketers.
Install this skill directly: skilldb add hubspot-skills