Leveraging well-built macro files will help you keep modules and module elements consistent and DRY across your entire codebase.

Prerequisites

  • Working knowledge of HubSpot modules (Overview found here)
  • Understanding of HubSpot macros (Overview found here)
  • Working knowledge of importing HubSpot macros (Overview found here)

Overview

A common group of issues that seems to be plaguing many HubSpot developers, whether they know it or not, is creating modules that are:

  1. Simple in design (not neccessrily simple in function)
  2. Easy to edit (perhaps synonymous with easy to understand)
  3. Modular in composition (think DRY)
  4. Quick to build

Modules, at their core, are the overall meat and potatoes of your HubSpot website. It is because of this fact that they need to be written and thought of not as individual pieces that are haphazardly constructed, but rather as larger units(modules) composed of smaller elements(macros) that an end user (content editor) can make use of and manipulate.

For this example we will be creating a simple card module. However the ideas shown here can be built out quite extensively to create some pretty rich-featured, cleanly designed modules.

Basic Strategy

The strategy for building our modules in a DRY, reusable, and modular format is quite simple.

We are going to create a template partial which we will use to house our macros related to the core function of the module. This macro will take a configuration object as a parameter. The configuration object will act as a gatekeeper for each instance of the macro we are trying to build.

In our module we will import the macro file and create a config object(which will be passed eventually into the imported macro itself) and connect the options to fields that the content creator can manipulate. We then call our imported macro and pass throught he config object.

Let's take a look at an example!

Config Macro

The main point of the macro config file is to provide an interface to create elments that are as module agnostic as possible. What I mean by this is that we want to eventually be able to use this file in other modules that may have some kind of "card" component to it.

The main benefit to this is that we can build a generic "card" in this file and import it wherever we need to. This will pay dividends later on after the initial build out of a site when the clients inevitably start to revamp their designs. They may ask for a sitewide change to card structure or design and if they do so, you will now have a single source of truth.

card.html (card config macro)
{#
  Config Options:
  image: object (image object),
  heading: string (single line text),
  content: html (rich text)
#}

The first part of our config macro file is the macro's config documentation. I like to put this directly in the macro file itself so that if anyone, myself included, needs to import this into a new module it is easy to quickly determine what config options are needed and how the module's fileds should be structured. More on this later.

The main point is that as your macro's options grow you will want to keep this feature/option list up to date.

In this example we can quickly see that we have three options available to us:

  1. An image option which takes as its value a standard image object {src: img-src.jpg, alt: "image alt"}
  2. A heading option which takes a string (Text)
  3. A content option which takes html (Rich Text)
card.html (card config macro)
{#
  Config Options:
  image: object (image object),
  heading: string (single line text),
  content: html (rich text)
#}

{% macro build_macro(config) %}
  <article class="card">
    <section class="image-container">
      <img src="{{config.image.src}}">
    </section>
    <section class="content-container">
      <h3>{{config.heading}}</h3>
      {{config.content}}
    </section>
  </article>
{% endmacro %}}

fields.json

module.html
[
  {"type":"text",
  "id":"39bbdff8-f45d-a845-da2d-bda5ba627bb2",
  "validation_regex":"",
  "label":"heading",
  "name":"heading",
  "default":"Heading"
  },
  {"type":"image",
  "id":"70c01ee4-8054-2f1c-1319-19228e6d2910",
  "default":{
    "size_type":"auto",
    "src":"",
    "alt":null,
    "loading":"lazy"
    },
  "resizable":true,
  "responsive":true,
  "show_loading":false,
  "label":"Featured Image",
  "name":"featured_image"
},
{
  "type":"richtext",
  "id":"f92d5efa-87b6-4e16-6719-73a0783fda1b",
  "label":"Content",
  "name":"content",
  "default":"<p><span>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.&nbsp;</span></p>"
}
]

Here we are building out our module's fields so that the content editor can influence the card macro.

module.html

module.html
{% import "/path/to/card.html" as card %}

{% set config  {
  image: module.featured_image,
  heading: module.heading, 
  content: module.content
}
%}

{{card.build_macro(config)}}

Styling your new module

There are a few different routes that we can take when it comes to styling these modules/elements. Most of the comps that I receive from clients have similar components being used in various modules. This means it makes the most sense to write a single css style sheet and include it using {{require_css('path/to/generic-card-styels.css')}} in the card.html macro file. More specifically inside of the build_macro macro.

card.html (card config macro)
{#
  Config Options:
  image: object (image object),
  heading: string (single line text),
  content: html (rich text)
#}

{% macro build_macro(config) %}
{{require_css('path/to/generic-card-styels.css')}}
  <article class="card">
    <section class="image-container">
      <img src="{{config.image.src}}">
    </section>
    <section class="content-container">
      <h3>{{config.heading}}</h3>
      {{config.content}}
    </section>
  </article>
{% endmacro %}}

This method provides us with a few benefits. It allows us to only include the styles where the card elements are used. And it ensures that the style sheet is always loaded when the cards are needed. We also don't need to worry about loading this stylesheet multiple times because HubSpot will only include the stylesheet once, even if the macro is called multiple times.

If a module calls for additional styles/customizations beyond what the generic card styles are, they can be included in the module.css file as overrides. Those will then be specific to that module.

Final Thoughts

This strategy for building modules allows for extrememly portable components that can be moved around from portal to portal with very little worry of needing to completely rebuild or restyle the individual pieces themselves.

The majority of the legwork/development actually takes place the first time a new element's config macro is being built. After that, if done correctly, building modules with the various macros that you have created takes very little effort at all.

Furthermore, if a different developer needs to understand how a module built this way functions it is extremely readable. A complex module with many fields can be parsed down to a simple config object. (What a breeze!).

If you would like to talk through some of the ideas presented here feel free to find me on the HubSpot Dev Slack Channel or DM me on linkedIn (link in footer). I'd be happy to show you some more complex examples or help you better understand how this strategy for building modules can improve your workflow.

Happy building!