hubspot-email — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited hubspot-email (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
You are an expert email developer creating HTML email templates for upload to HubSpot. Every template must follow HubSpot's technical constraints, email client compatibility rules, and the brand system.
Scope boundary — pull two `web-design` reference files for email. Email HTML has different constraints than web (table-based layout, 600px max, no JS, limited CSS, no hover on many clients), so don't load all of web-design. But two of its reference files apply directly and should be loaded alongside this skill for any email build or review:
.claude/skills/web-design/references/accessibility.md — semantic HTML, alt text, contrast (brand palette), reading order, reduced-motion. Email a11y matters and this covers the gaps brand-guide doesn't..claude/skills/web-design/references/anti-patterns.md — brand-specific bans (no gradient accent lines, no pill shapes, no "kernel-level" copy, purple-as-default-CTA-for-email justification, no three-color gradient on non-text surfaces). All apply to email.Skip web-design's interaction/motion/responsive/composition/state files — those assume a live web environment. Switch fully to web-design only when the CTA destination or thank-you page needs craft review.
Always read these first:
/brain/brand-guide/brand-guide.md — colors, typography, spacing, brand systemIf the email includes product claims or copy: Read /brain/truth.md and /brain/positioning-and-messaging.md to ensure accuracy. Never invent product facts.
Gather this context (ask if not provided):
When creating templates for HubSpot's "Upload external email template" feature, these elements will not transfer:
| Limitation | Impact | Workaround |
|---|---|---|
| Max width 600px | Container must be 600px or narrower | Always use 600px container |
| No background gradients | CSS linear-gradient on backgrounds is stripped | Use solid background colors. Apply gradients only as accent images or via the gradient bar as an <img> if needed |
| No rounded corners on modules | border-radius on outer module wrappers won't transfer via AI Upload | Remove radius from large containers (cards, banner wrappers). Small inline elements like buttons and tags can keep `border-radius` — these work fine. For full radius support, use a coded template via Content > Design Manager (HTML + HubL, type Email) instead of AI Upload |
| No overlapping elements | Text over images, absolute positioning stripped | Use stacked layout — image block, then text block. No layered compositions |
| No custom fonts | Custom @import or @font-face ignored | Use system-safe font stacks. Inter won't render — fall back to Arial, Helvetica, sans-serif |
| No custom modules | HubSpot-specific modules won't transfer | Use standard HTML tables and content blocks |
| Images must be publicly hosted | Local or private image URLs won't load | Use publicly accessible URLs (e.g., from your website, CDN, or HubSpot File Manager) |
| No JavaScript | All <script> tags stripped | Never include JS — it's stripped by all email clients anyway |
| No CSS positioning | position: absolute/fixed/relative not supported | Use table-based layout exclusively |
| No CSS floats | float property unreliable in email | Use table cells for side-by-side layouts |
| No external stylesheets | <link rel="stylesheet"> won't load | All CSS must be inline or in <style> in <head> |
| `filter` CSS property | Not supported in most email clients | Don't use filter: invert() etc. — use pre-made white icon assets instead |
| Social module icons | Platform-specific social icons (Pinterest, Snapchat) not supported | Use simple image icons with links |
<style> block in <head> (HubSpot inlines it)<style> block (for responsive)When building templates for upload via Design Manager (coded templates), follow these requirements.
Every coded email template file must start with template annotations as an HTML comment before the DOCTYPE:
<!--
templateType: email
isAvailableForNewContent: true
label: [Company] Email - [Template Name]
-->
<!DOCTYPE html>templateType: email — tells Design Manager this is an email templateisAvailableForNewContent: true — makes it selectable when creating new emailslabel — display name in the template pickerUse these HubL tags to create editable fields that appear in the email editor sidebar:
{# Editable text — use for short fields (titles, names, dates, URLs) #}
{% text "field_id" label="Field Label", value="Default value", no_wrapper=True %}
{# Editable rich text — use for paragraphs, formatted content #}
{% rich_text "field_id" label="Field Label", html="<p>Default content</p>", no_wrapper=True %}
{# Boolean toggle — use to show/hide sections #}
{% boolean "field_id" label="Show Section", value=True, export_to_template_context=True %}
{# Module — use for HubSpot-managed email modules #}
{% module "module_id" path="@hubspot/email_cta" %}Important: Always use no_wrapper=True when placing {% text %} or {% rich_text %} inside styled containers. Without it, HubSpot adds wrapper <div> or <span> elements that break inline styling.
Important: For {% boolean %} tags, use export_to_template_context=True instead of no_wrapper=True. Without it, the boolean renders "true" or "false" as visible text in the email body. export_to_template_context=True stores the value silently and makes it available via widget_data.field_id.value.
Conditional sections with boolean:
{% boolean "show_section" label="Show Section", value=True, export_to_template_context=True %}
{% if widget_data.show_section.value %}
<!-- HTML for the conditional section -->
{% endif %}When using a personalization token for greetings (e.g., "Hi {{first_name}},"), always set the fallback value to "there" in HubSpot's token picker UI. This produces:
Do NOT leave the fallback blank or use a space. HubSpot's UI requires a fallback value — an empty field or a space creates awkward rendering ("Hi ," or "Hi ,"). The word "there" is the established pattern for all [Company] emails.
When inserting via the HubSpot rich text editor: type "Hi ", click Insert > Personalization, select Contact > First Name, set fallback to "there", then type "," after the token.
For the coded template equivalent:
Hi {% personalization_token "contact.firstname", "there" %},Email modules are different from web page modules. Use @hubspot/email_cta not @hubspot/cta, @hubspot/email_text not @hubspot/text, etc.
| Module | Path | Use For |
|---|---|---|
| Call-to-Action | @hubspot/email_cta | HubSpot-managed CTA (renders its own markup) |
| Main Email Body | @hubspot/email_body | Primary email content (rich text) |
| Logo | @hubspot/email_logo | Company logo (pulls from account settings) |
| Image | @hubspot/image_email | Standalone image |
| Linked Image | @hubspot/email_linked_image | Image with link |
| Header | @hubspot/email_header | Heading (h1-h6) |
| Text | @hubspot/email_text | Single line of text |
| Section Header | @hubspot/email_section_header | Header + subheader |
| CAN-SPAM Footer | @hubspot/email_can_spam | Auto-populated business address + unsubscribe |
| Social Sharing | @hubspot/email_social_sharing | Social share buttons |
| Raw HTML | @hubspot/raw_html_email | Custom HTML block |
| Video | @hubspot/video_email | Video embed with thumbnail |
| Blog Post Filter | @hubspot/email_post_filter | Blog post filter by tag/author/month |
| Blog Post Listing | @hubspot/email_post_listing | Recent/popular blog posts |
| Subscriptions | @hubspot/email_subscriptions | Subscription preferences page |
| Subscription Confirmation | @hubspot/email_subscriptions_confirmation | Subscription change confirmation |
| Unsubscribe (Backup) | @hubspot/email_simple_subscription | Simple unsubscribe form |
IMPORTANT: `@hubspot/rich_text_email` does NOT exist. There is no email-specific rich text module. For rich text / HTML content in email templates, use @hubspot/email_body (accepts html parameter for default content). The web module @hubspot/rich_text is NOT in HubSpot's "replace for email" list, so it may also work, but @hubspot/email_body is the confirmed email-native option.
IMPORTANT: `{% cta %}` does NOT work in HubSpot marketing emails. The {% cta %} HubL tag is for web/landing pages only. Using it in an email template causes a "There was a problem loading this content" error in the email editor sidebar. This was confirmed by HubSpot support.
#### Recommended Approach: @hubspot/email_cta Module
Use the native CTA module. It renders correctly in the email editor, gives the marketer a proper CTA picker, and tracks clicks in the CTA dashboard.
{% module "cta" path="@hubspot/email_cta", label="CTA Button" %}Do NOT wrap this in a `<div>`. HubSpot's {% module %} generates its own wrapper markup (<div id="hs_cos_wrapper_cta">). Adding a parent div hides the module entirely in the email editor.
Centering and spacing: Target the wrapper ID in the non-inlined style block (the one WITHOUT id="hs-inline-css"). Do NOT put it in the hs-inline-css block — HubSpot inlines those styles before the module wrapper exists, so they have nothing to attach to.
<!-- Non-inlined styles — HubSpot keeps these as <style> in <head> -->
<style type="text/css">
/* Center the CTA module wrapper and its inner table */
#hs_cos_wrapper_cta { text-align: center; padding: 16px 24px 0 24px; }
#hs_cos_wrapper_cta table { margin: 0 auto; }
</style>Why this works: The non-inlined <style> block stays in the <head> as a CSS rule. At render time, the module wrapper exists and the rules apply. text-align: center centers inline content; margin: 0 auto on the inner table centers block-level content. Works in Gmail, Apple Mail, iOS, and most modern clients. Outlook may ignore the <style> block (button still renders, just left-aligned).
Brand colors: Marketer sets #6A2AFF bg, #F9F9F9 text, 12px radius in the CTA editor's Styles tab. Purple is the approved default for email CTAs (teal #50F6E8 renders inconsistently across email clients — Outlook, dark-mode Gmail, and forwarded chains can shift it). Both teal and purple are approved primary button colors per brain/brand-guide/brand-guide.md — use teal on landing pages where contrast is reliable, purple in email where it is not.
What you get: CTA dashboard tracking (views, clicks, conversions), native "Link to: CTA" picker, marketer controls styling via editor UI. What you don't get: Pixel-perfect brand CSS from the template — marketer sets brand colors manually per-email.
#### Comparison
| Approach | Brand Control | CTA Dashboard | Editor UX |
|---|---|---|---|
| `@hubspot/email_cta` module (recommended) | Marketer sets in Styles tab | Yes — views, clicks, conversions | Native CTA picker + style editor |
~~Styled <a> + {% text %}~~ | ~~Full inline CSS~~ | ~~Per-email only~~ | Breaks in Remix editor — HubSpot auto-converts to broken module |
~~{% cta %} tag~~ | ~~N/A~~ | ~~N/A~~ | Does not work in emails — page-only tag |
HubSpot's email editor (Remix) always uses a drag-and-drop interface. Even coded templates get auto-converted to dnd modules — and the conversion often breaks (producing "Unknown Module" or uneditable modules). The fix: use {% dnd_area %} tags to control how HubSpot builds the module tree.
Choose between classic HubL and dnd_area based on who controls the layout.
{% text %}, {% rich_text %}, {% boolean %}): template author locks the structure, marketers only fill in labelled fields. Use this for brand-critical recurring templates (newsletters, promos, announcements) where design consistency matters more than layout flexibility. All current [Company] production templates (email-base-hubspot.html, livestream-email.html, livestream-compact-email.html, on-demand-followup-email.html, newsletter-monthly-email.html) use classic HubL and render correctly in the Remix email editor.For a single-marketer workflow with tight design control ([Company] today), classic HubL is usually the right default. Reach for dnd_area when layout flexibility per send is more valuable than brand-lockdown consistency.
<head> Includes{{ dnd_area_stylesheet }} <!-- REQUIRED for dnd_area rendering -->
{{ email_header_includes }} <!-- Cross-client rendering -->
{{ reset_css_stylesheet }} <!-- Cross-client CSS reset -->dnd_section > dnd_column > dnd_modulednd_area — locked, not editable per-emaildnd_module tagStatic HTML (not editable)
├── Logo header
├── Gradient accent bar
{% dnd_area "main", full_width=False %}
├── dnd_section → dnd_column → dnd_module (content)
├── dnd_section → dnd_column → dnd_module (more content)
├── dnd_section → dnd_column → dnd_module @hubspot/email_cta
{% end_dnd_area %}
Static HTML (not editable)
├── Footer (logo, social, CAN-SPAM, unsubscribe)The bug: In a template mixing a static masthead/footer with a dnd_area, the dnd module content renders ~10px to the right of the static sections — even when both use the same internal padding (e.g. padding: 0 40px). Visible as a misaligned divider/separator line or an off-axis share-row box.
Why it happens: HubSpot's dnd_area_stylesheet (injected via {{ dnd_area_stylesheet }}) adds default horizontal padding to the .dnd-section / .dnd-column / .dnd-module wrappers it generates. Those wrappers sit between your email-container <td> and your module's HTML, so the effective left offset of module content is (module internal padding) + (wrapper padding), while static masthead/footer content is just (td padding).
What does NOT fix it (tried and failed on the Mind the Gap build, 2026-04-22):
padding={'top':0,'bottom':0,'left':0,'right':0} on dnd_section / dnd_columnhs_wrapper_css={'padding':'0'} on every dnd_modulefull_width=False on dnd_area and dnd_section (still a good idea for other reasons, but doesn't fix the offset)<style> block in the template <head> with !important overrides on .dnd-section / .dnd-column / .dnd-module — HubSpot's stylesheet injection wins the specificity war somehow.What DOES fix it — adjust the static sections, not the dnd wrappers. Measure the offset live in devtools (in the company's case: ~10px), then bump the left (and usually right) padding on every static <td> by that amount so it aligns with the module content. The dnd wrapper's offset is fixed — you match it from the static side.
Concrete pattern that worked for Mind the Gap (modules use 40px internal L/R padding, wrapper adds ~10px each side):
<!-- Masthead: left 50 (not 40) to match dnd module content -->
<td style="padding: 48px 40px 32px 50px;">...masthead...</td>
<!-- Share row: 50 L and R so the box aligns with module content both sides -->
<td style="padding: 48px 50px 0 50px;"><div>...share box...</div></td>
<!-- Footer divider: 50 L and R so the line matches module dividers -->
<td style="padding: 32px 50px 0 50px;"><div style="border-top: 1px solid #E5E7EB;"> </div></td>Rule of thumb: don't fight HubSpot's dnd wrapper. Measure the offset once, then pad the static sections to match. This keeps all the module padding logic untouched and doesn't rely on CSS override tricks that don't actually work.
Only use modules confirmed to work in dnd email context:
| Module | Path | Works in dnd email |
|---|---|---|
| CTA Button | @hubspot/email_cta | Confirmed |
| Image | @hubspot/image_email | Confirmed (in official docs) |
| Email Body (rich text) | @hubspot/email_body | Testing |
| Raw HTML | @hubspot/raw_html_email | Testing |
| One line of text | @hubspot/email_text | Testing |
Do NOT use `@hubspot/rich_text_email` — this path does not exist and produces "Unknown Module" errors.
Rich text content with default HTML:
{% dnd_section
background_color={'color':'#0F0F0F'},
padding={'top':'0','bottom':'0','left':'24','right':'24'}
%}
{% dnd_column width=12 %}
{% dnd_module
path='@hubspot/email_body',
html='<div style="font-size: 16px; color: #E2E2E2;">Default content here</div>'
%}
{% end_dnd_module %}
{% end_dnd_column %}
{% end_dnd_section %}CTA button (native editor):
{% dnd_section
background_color={'color':'#0F0F0F'},
padding={'top':'36','bottom':'0','left':'24','right':'24'}
%}
{% dnd_column width=12, horizontal_alignment='CENTER' %}
{% dnd_module path='@hubspot/email_cta' %}
{% end_dnd_module %}
{% end_dnd_column %}
{% end_dnd_section %}Marketer sets brand colors manually in Button module style tab: #6A2AFF bg, #F9F9F9 text, 12px radius.
Image module:
{% dnd_module path='@hubspot/image_email',
img={'alt':'Logo', 'height':32, 'src':'https://example.com/logo.svg', 'width':143},
alignment='center'
%}
{% end_dnd_module %}{% dnd_section
background_color={'color':'#131313'},
padding={'top':'0','bottom':'0','left':'24','right':'24'},
max_width=600
%}{{ site_settings.company_name }}
{{ site_settings.company_street_address_1 }}
{{ site_settings.company_street_address_2 }}
{{ site_settings.company_city }}
{{ site_settings.company_state }}
{{ site_settings.company_zip }}
{{ site_settings.company_country }}<!-- Unsubscribe -->
<a data-unsubscribe="true" href="{{ unsubscribe_link }}">Unsubscribe</a>
<!-- Unsubscribe from all -->
<a data-unsubscribe="true" href="{{ unsubscribe_link_all }}">Unsubscribe from all</a>
<!-- View as webpage -->
<a data-viewaswebpage="true" href="{{ view_as_page_url }}">View as web page</a><div id="preview_text" style="display:none!important;">
{% text "preview_text" label="Preview Text" value="Preview text here" %}
</div>Use id="hs-inline-css" on style blocks that should be compiled to inline CSS:
<style type="text/css" id="hs-inline-css">
/* These styles will be inlined on target elements */
td { font-family: Arial, sans-serif; }
</style>Keep media queries in a separate <style> block (without the inline ID) so they aren't inlined:
<style type="text/css">
@media only screen and (max-width: 620px) {
.mobile-full { width: 100% !important; }
}
</style><table role="presentation"> with cellspacing="0" cellpadding="0" border="0"align="center" on wrapper <td> to center the email container<style> blocks unpredictably<style> block in <head> as progressive enhancement, but don't rely on it<style> blockposition, float, flexbox, grid, filter, clip-path, calc(), CSS variablesborder-radius works in most clients except Outlook (degrades gracefully to square)background-image unreliable in Outlook — always have a solid background-color fallbackArial, Helvetica, sans-serif or Georgia, serif@import Google Fonts as progressive enhancement — they render in Apple Mail, iOS, some Android — but always include fallbackalt text, width, height, and style="display: block; border: 0;"width attribute (not just CSS) for Outlook compatibilityfilter on images — use pre-made assets in the correct color<table role="presentation" cellspacing="0" cellpadding="0" border="0">
<tr>
<td style="background-color: #6A2AFF; border-radius: 12px; text-align: center;">
<a href="[URL]" target="_blank"
style="display: inline-block; padding: 14px 36px; font-family: Arial, Helvetica, sans-serif; font-size: 17px; font-weight: 500; color: #F9F9F9; text-decoration: none;">
Button Text
</a>
</td>
</tr>
</table>Gmail App on iOS is a massive platform and the hardest dark mode problem in email. Read this before building any dark-themed email template.
How Gmail iOS dark mode works:
background-color CSS, bgcolor HTML attributes, color CSS, and <font color=""> tags@media (prefers-color-scheme: dark) is ignored by Gmail iOS (works in Gmail Web only)color-scheme / supported-color-schemes meta tags have no effect on Gmail iOSWhat Gmail iOS does NOT touch:
background-image (including linear-gradient()) — this is the only known way to preserve a background color<img> tags) — these render as-isTechniques we tested and their results (Session 31):
| Technique | Fixes Backgrounds? | Fixes Text? | Verdict |
|---|---|---|---|
color-scheme meta tags | No | No | Useless for Gmail iOS |
bgcolor on <table>/<td> | No | N/A | Gmail inverts these too |
background-image: linear-gradient(#color, #color) | Yes | No | Only reliable background fix |
<font color=""> tags | N/A | No | Gmail overrides them |
@media (prefers-color-scheme: dark) | N/A | N/A | Gmail iOS ignores entirely |
The core problem for dark-themed emails: You can preserve dark backgrounds with the gradient hack, but Gmail will still mute/shift all text colors. The result is dark background + washed-out text — which can look worse than letting Gmail do its full inversion (where at least backgrounds and text have consistent contrast).
Recommendation:
background-image gradient hacks, <font> tags, or color-scheme meta tags to "fix" Gmail dark mode — we tested all of these and none produce an acceptable result for dark emails. They create partial fixes that look worse than Gmail's native full inversion.Full investigation documented at: marketing/plans/gmail-ios-dark-mode-fix.md
@media (prefers-color-scheme: dark) and color-scheme meta — progressive enhancement works here[data-ogsc] (text color) and [data-ogsb] (background color) attributes when forcing dark mode. These are Outlook-specific, not Gmail.color-scheme: light dark; in CSS is fine as progressive enhancement — just know it does nothing for Gmail iOS<!--[if mso]> conditional comments for Outlook-specific fixesmso-table-lspace, mso-padding-alt, etc.) are valid for Outlook onlyborder-radius does not work in Outlook — buttons degrade to rectangularSince email clients have limited CSS support, adapt the brand system:
| Brand Element | Website | Email Adaptation |
|---|---|---|
| Background | #0F0F0F | #0F0F0F — works in email |
| Font | Inter | Arial, Helvetica, sans-serif (with @import Inter as progressive enhancement) |
| Gradient accent bar | CSS linear-gradient | Use a pre-made gradient image (hosted PNG/SVG) or a thin <img> element |
| Gradient text | background-clip: text | Not supported — use white #F9F9F9 text instead |
| Purple CTA button | #6A2AFF solid | Styled <a> with {% text %} fields for URL and copy (see CTA section above) |
| Cyan tag | #50F6E8 bg, #0F0F0F text | Works with inline styles |
| Card surfaces | #131313 with border-radius: 32px | Use #131313 bg. Radius works except Outlook (graceful degradation) |
| Border | 1px solid #454545 | Works inline on <td> |
| Social icons | CSS filter: invert() | Don't use filter. Host white icon PNGs and reference directly |
Since CSS gradients don't work in email backgrounds, create the signature gradient as a hosted image:
<img src="[gradient-bar-url]" width="600" height="4" style="display: block;" />All [Company] HubSpot email templates use a base template pattern. The shared foundation (header, gradient bar, CTA, footer) lives in one file, and each email type is a variant that customizes the content area.
marketing/templates/email-templates/
├── email-base-hubspot.html ← Base (DO NOT upload — starting point only)
├── livestream-email.html ← Full-length livestream promo (classic coded, HubL fields)
├── livestream-compact-email.html ← Two-column compact livestream variant
└── _previews/ ← Static HTML previews (not for upload)
└── livestream-compact-preview.htmlemail-base-hubspot.htmlproduct-announcement-hubspot.html)isAvailableForNewContent: truelabel (e.g., [Company] - Product Announcement)CONTENT START and CONTENT END with your email-specific sections<style> block if needed<a> with {% text %} fields for URL and button copy — marketer edits from sidebar)id="hs-inline-css") and responsive media queries.date-col for the livestream)Every email template should follow this structure:
1. DOCTYPE + HTML wrapper
2. <head>
- Meta tags (charset, viewport, X-UA-Compatible)
- Outlook conditional comments (PixelsPerInch)
- <style> block with inline-css ID (base styles)
- <style> block for media queries (responsive)
3. <body> (background color on body + wrapper table)
4. Full-width wrapper table (100% width, centered)
5. Email container table (600px, centered)
a. Logo header (linked to website)
b. Content sections
c. CTA button(s)
d. Footer (social icons, copyright, unsubscribe)
6. Closing tagsOutput location: marketing/email/templates/[template-slug]/ — confirm the project slug with the user before creating files.
1. Complete HTML file
<style> block in head<style> block<!-- EDITABLE: --> comments marking content the user should replace[like this] for variable content2. Upload instructions
For Design Manager (coded templates):
{% text %}, {% rich_text %}, {% boolean %}) will appear in the right sidebar when editing an email3. Testing checklist
/brain/brand-guide/brand-guide.mdtemplateType: email, isAvailableForNewContent: true, label)filter CSS propertyalt, width, height, display: block, border: 0<style> block/brain/truth.mdUse these tools to look up HubSpot documentation and manage CMS assets:
mcp__HubSpotDev__search-docs to find HubSpot developer documentation on email modules, template markup, HubL syntax, or any email-related API. Always search before guessing at module paths or field syntax.mcp__HubSpotDev__fetch-doc immediately after searching to read the full documentation page. Never skip this step — search results are summaries, not complete docs.mcp__HubSpotDev__create-cms-template to scaffold a new email template file. Set templateType to email-template.mcp__HubSpotDev__list-cms-remote-contents to see what templates and assets are already deployed in the HubSpot Design Manager.mcp__HubSpotDev__create-cms-module to scaffold a custom email module if needed.When to use: Before building any email template, search docs to verify module paths (e.g., confirm @hubspot/email_body vs @hubspot/email_text behavior). After building, use list-cms-remote-contents to check for naming conflicts before manual upload.
When the user provides a Figma URL or references an email design mockup:
mcp__figma-remote-mcp__get_design_context to extract layout, colors, typography, and spacing from the Figma mockup. Translate these into email-safe HTML (table-based, inline styles, system fonts).mcp__figma-remote-mcp__get_screenshot to see the visual design before building the template.mcp__figma-remote-mcp__get_metadata to understand the section structure and layer hierarchy.When to use: When the user has an email design in Figma that needs to be converted to a HubSpot-compatible HTML template. Remember: the Figma design will use features email doesn't support (gradients, custom fonts, border-radius) — adapt per the constraints in this skill.
After building an email template, use agent-browser to validate rendering:
agent-browser open <file> to open the HTML email file in the browser.agent-browser set viewport 600 900 to set the viewport to 600px wide (email container width), then run agent-browser screenshot to capture the result.agent-browser set viewport 375 812 and run agent-browser screenshot again to verify responsive behavior.agent-browser screenshot to capture the entire email template for review.agent-browser snapshot -i to verify heading structure, link text, and alt text on images.agent-browser console to catch any broken image references or CSS issues.When to use: After building any email template — always use agent-browser to validate in the browser before delivering. This catches layout issues, broken images, and responsive problems before the template reaches HubSpot.
dnd_area, dnd_section, dnd_column, dnd_module tags for emailUse the analytics skill to pull real email performance data when reviewing or optimizing email templates:
Example: Before building a new email template, run npx tsx .claude/skills/analytics/scripts/hubspot-emails.ts 90d to see which existing emails had the highest open/click rates, then apply those patterns to the new template.
Before producing work, check whether any of these skills apply to the task. If they do, load the skill before writing that portion. Do not replicate a skill's logic from memory — load it and apply it.
| When the task involves... | Skill to Load | Mandatory? |
|---|---|---|
| CTA buttons (placement, centering, tracking, brand CSS) | hubspot-cta | Yes — load before adding any CTA to a template |
| Writing or reviewing email copy/messaging | copywriting or copy-editing | Yes — load before writing or editing copy |
| Email sequence strategy, drip campaign logic | email-sequence | Yes — load when the email is part of a sequence |
| Visual design decisions beyond what's in the brand guide | brand-design | Yes — load for custom visual asset work |
| Landing page templates (different constraints) | hubspot-landing-page | Load if the user asks about landing pages to avoid applying email constraints |
<!-- Updated by /reflect. Promote stable patterns to the main skill body. -->
#50F6E8 fails WCAG AA on white (~1.4:1). Use #0D9488 (teal-600, 4.68:1 AA) for links on white backgrounds. (Session 77, 2026-04-13)unpkg.com/lucide-static@latest/icons/{name}.svg, bake color with sed 's/stroke="currentColor"/stroke="#HEX"/', rasterize to 40×40 PNG with rsvg-convert -w 40 -h 40. Store both SVG and PNG side-by-side in marketing/icons/{category}/. (Session 77, 2026-04-13)brain/brand-guide/brand-guide.md CTA block patterns first — don't default to a bare intro + button. Variant A (Dark Card) is typically the only brand-guide CTA pattern that translates to email; variants B/D/E rely on CSS gradient backgrounds and ambient glows that email clients strip. For light-themed emails, adapt Variant A per the brand's Light Mode Accent Strategy: white card with rgba(0,0,0,0.10) border, Tonik Blue #444AD9 outlined tag kicker, purple #6A2AFF bulletproof button. (Session 93, 2026-04-22)show_section_marker boolean field. Turn ON for the first item in a group, OFF for subsequent items so they stack cleanly under one marker + one leading divider. Prevents duplicate "LATEST POSTS"-style headers when the marketer drops multiple instances. (Session 93, 2026-04-22)dnd_module tags inside the dnd_area with realistic starter content (e.g. Featured + 2 Blog Posts + 1 Event + Dev Corner). Gives the marketer a working starting layout instead of an empty canvas. (Session 93, 2026-04-22)modules/ subfolder next to the template; reference as dnd_module path="./modules/name". Matches repo layout to Design Manager folder structure and makes uploads predictable. (Session 93, 2026-04-22){% dnd_area %} with any name other than "main", class attribute on dnd_area, {% text %} tags anywhere in email templates, module field names body (use body_text) and name (use something descriptive like event_name). When HubSpot blocks a tag type for editable fields, wrap the fields in a micro custom module and include via {% module "label" path="./modules/foo" %}. (Session 94, 2026-04-22)_previews/*.html. HubSpot round-trips are slow and obscure layout cause-and-effect. If a preview file exists but doesn't mirror HubSpot's dnd_area wrapping (<td class="dnd-section"> → <td class="dnd-column"> → <td class="dnd-module">), make it faithful before debugging layout issues. (Session 94, 2026-04-22)<td>s in a zero-whitespace table (<td style="padding:0; line-height:0; font-size:0;">) when they need to hug the left edge. Plain <a><img></a> inside a <td> picks up a leading space from surrounding HTML whitespace in some email clients, offsetting the image right of other left-aligned siblings. (Session 95, 2026-04-22){% dnd_module %} content in an email template is wrapped by HubSpot's editor with different default styles than modules the marketer drags in at runtime. If pre-placed content renders at a different width than dragged content, try hs_wrapper_css={'padding':'0'} on the {% dnd_module %} or check whether the {% dnd_area %} needs full_width=False to stay inside its parent container's max-width. (Session 94, 2026-04-22)Opted out of email: [subscription name] property is null by default (tri-state model: Subscribed / Unsubscribed / Not Specified). Active-list filters for "subscribers" must use is none of Yes AND check the "Include records where property is empty" box, otherwise never-unsubscribed contacts are excluded from the list. (Session 99, 2026-04-23)~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.