- Credits
- About
- Getting started
- How it works
- Icons
- Developing new modules
- PREACT
- HubDB Tables
- Menu
- Search
- Blog
- Hubspot documentation
This boilerplate is based on the Hubspot Local Development Boilerplate. The added packages are selected by Linsey Brander & Teun Rutten.
This is a starter pack for developing Hubspot projects. The project structure is:
/src
folder for all your source files,/dist
folder that is synced with Hubspot.
- Gulp
- SCSS
- Bundles SVG icons to a single file
- Prettier & ESLint
- Webpack (ES6)
- Install Java.
- Install Node.js, a Javascript runtime environment that enables the local tools. Versions 8.9.1 or higher of Node are supported, but we recommend the long-term support (LTS) version.
- Install the gulp-cli globally:
yarn global add gulp-cli
Run yarn install
in the project root.
- Empty the existing hubspot.config.yml or create a new one with the following command:
touch hubspot.config.yml
- Create a private developer app. NOTE: If you select more than the "Content" scope in your apps permissions, your app will not work with this tooling. The app will work with no scopes selected.
- Run
yarn hubspot:auth
in the command line. This will begin a series of command line prompts. - Enter your CMS Portal id.
- Enter your client id and client secret from your private developer app. This will open a window in your default browser requesting authorization.
- Select your CMS portal on the authorization page.
- Request for Integrations Permissions: your private app will request permission to access your account data. Click "Grant Access".
- If successful, you should see "Authorization Succeeded", and your hubspot.config.yml file will be updated.
- Add a
name
to the first portal and select adefaultPortal
:
defaultPortal: 'DEV'
portals:
- name: 'DEV'
portalId: *******
authType: oauth2
...
This boilerplate handles the building and deploying of JavaScript, CSS and Hubspot modules. It uses Gulp and Webpack to create minified main.js and main.css files and 'builds' all files in the /dist
folder. The dist folder is the only folder that is synced with Hubspot, so all files in the project root and in /src
are for local development. There are a few default commands that can be used in the package.json
. The main command you need to build and sync files with Hubspot is yarn deploy
.
- Syncing doesn't delete files in Hubspot
- Create modules local only. NEVER create or edit custom modules in the 'Design Tools' of Hubspot
- Templates are not created locally, you have to create 'Drag and drop' templates in the 'Design Tools' of Hubspot
- You have to connect the
main.css
andmain.js
files to every template you create
To get started with HubSpot development you'll need some commands. These commands will build the CSS and JavaScript and help you automatically sync to HubSpot.
First things first, you'll need to clone the repository like so:
git clone https://github.com/teunrutten/hubspot-boilerplate.git your-project
Change the project name inside the gulpconfig.js
.
Install all dependencies in the project like so:
yarn
Now you'll need to create a HubSpot Sandbox account via https://offers.hubspot.com/free-cms-developer-sandbox. Next up, you'll take note of the portal ID in the url. Search for number which is similiar to this one 7036122
. You replace the portalId inside the hubspot.config.yml
specifacally for dev with the one you copied. Then, you'll need to generate an API-key, which can be found underneath the gear icon > Integrations > API. Copy the API-key over and place the key in the hubspot.config.yml
file.
Now, you can use the following commands to kick start you development:
yarn watch
This command will watch for changes regarding to SCSS, macro's and modules. Each module and macro will be copied over to the dist folder; The dist folder is used to publish to HubSpot.
yarn watch:webpack
This command will watch for changes regarding to JavaScript. The JavaScript will be transpiled and placed inside the dist folder;
yarn hs:watch
This command will watch for file changes inside the dist folder. When triggered these files will be uploaded to the HubSpot portal.
You also have some extra commands at your disposal:
yarn gen:fields
This command can be used for generating HubSpot fields;
yarn build:icons
This command can be used to build the icons and merge them together in the icons.svg. The icons.svg is later on merged inside the _icons.module/module.html.
yarn gen:module
This command can generate a whole module for you.
Hubspot doesn't allow the upload of SVG files in Design Tools. That's why this boilerplate includes an Icons
module. The src/modules/icons.module/module.html
file contains a SVG of all icons in src/icons
. Add the Icons
module in the 'Header' section of every template. That way you can use icons like:
<svg>
<use href="#example" xlink:href="#example"></use>
</svg>
You can add your SVG icons to src/icons
. Combining SVG and adding them to the Icons
module is handled by Gulp.
Use the command yarn hubspot:module <name> src/modules
to create a new module. This will create a new folder in the modules folder. This folder contains the following files:
/fields.json
/meta.json
/module.css
/module.html
/module.js
Don't use the module.css
and module.js
files of a module. We only use the src/js
folder and src/scss
folder to edit css and javascript. Do delete the comments in the module.css
and module.js
files, otherwise Hubspot will load them.
BUG (18 sep 2019): Don't leave module.css
and module.js
empty (i.e. don't remove the comments), otherwise Hubspot will error when you try to add the module to a page.
You can find a lot of info on modules in Modules JSON.
Every module contains a meta.json
file. This file contains properties with settings for the Hubspot module:
{
"css_assets": [ ],
"external_js": [ ],
"global": true,
"help_text": "",
"host_template_types": [ "PAGE" ],
"js_assets": [ ],
"other_assets": [ ],
"smart_type": "NOT_SMART",
"tags": [ ],
"is_available_for_new_content": true
}
To be able to use the new module in the Hubspot templates you should edit the meta.json
file and change is_available_for_new_content
to true
.
If you need a global module, you can edit the property global
and set it to true
.
If the module may also be used on blog, change host_template_types
to [ "PAGE", "BLOG_LISTING", "BLOG_POST" ]
.
Each module contains a fields.json
file. This file contains all the fields a Hubspot user can edit to change the content of the module.
This boilerplate includes various fields utils (/src/utils
). Create a /fields/fields.js
file in each module. Use this file to list which utils and custom fields should be used to build up a fields.json
file (see Gulp buildModuleFields
).
Example fields.js
:
exports.config = function() {
return [
'src/modules/example.module/fields/content.json',
'src/modules/example.module/fields/image.json',
'src/utils/fields/padding.json',
'src/utils/fields/mobile_padding.json'
]
}
First, install the dependencies:
yarn add preact
yarn add @babel/plugin-transform-react-jsx --dev
Add the react-jsx plugin to .babelrc:
{
"presets": [
// ...
],
"plugins": [
["@babel/plugin-transform-react-jsx", {
"pragma": "Preact.h",
"pragmaFrag": "Preact.Fragment"
}]
]
}
Add a new folder in src/js
for your PREACT app. Example of src/js/example/index.js
:
import { h, render } from 'preact'
import App from './containers/App'
// Tell Babel to transform JSX into h() calls:
/** @jsx h */
const container = document.getElementById('js-example-app')
if ( container ) {
render( <App />, container )
}
Add a new entry
to webpack.config.js and make sure output
has filename: '[name].js'
:
module.exports = {
mode: 'production',
entry: {
main: './src/js/main.js',
example: './src/js/example/',
},
output: {
filename: '[name].js',
path: path.resolve(__dirname, 'dist/js')
},
// ...
}
Don't forget to add the new javascript file to your template in HubSpot.
Make sure your website does not load theme-foundation.js
! This file prevents PREACT from working correctly.
- Get table data and columns: https://api.hubapi.com/hubdb/api/v2/tables/{tableId}?portalId={portalId}
- Get table rows: https://api.hubapi.com/hubdb/api/v2/tables/{tableId}/rows?portalId={portalId}
- Make sure the table is publicly queryable. You do this per table in Settings > Allow public API access.
- IE11 fix: Add
whatwg-fetch
to package.json to add support forfetch
.
Navigate to Marketing > Files and templates > HubDB to create a table. Add some columns and fill in some rows.
Create a module and then loop through the table rows in the module.html
.
For example, we have a Cases table (ID: 1667842). The table has 4 columns: id, name, image and url.
{% set cases = hubdb_table_rows('1667842') %}
{% for case in cases %}
<div>
<img src="{{case.image.url}}" alt="{{case.image.alt}}"/>
<p>{{case.name}}</p>
<a href="{{case.url}}">Read more</a>
</div>
{% endfor %}
Instead of hard-coding, you can also let users choose a table by adding a hubdbtable
field:
{
"name" : "table_id",
"label" : "Tabel",
"type" : "hubdbtable"
}
Navigate to Settings > Website > Navigation to create a menu.
Choose "Add item without link" for items that'll have a dropdown. We will use this to check in our module.html
if an menu item has children or not (if item.url
).
In your module, create a field that allows users to pick one menu:
{
"name" : "menu_id",
"label" : "Menu",
"type" : "menu",
"default" : 12181326389 //optional default menu id
}
<div>
{% set node = menu(menu_id) %}
{% for item in node.children %}
{% set current = item.activeNode ? 'current' : '' %}
{% set currentParent = item.activeBranch ? 'current' : '' %}
{% if item.url %}
<a href="{{item.url}}" class="{{current}}">
{{item.label}}
</a>
{% else %}
<div class="{{current}} {{currentParent}}">
<span>{{item.label}}</span>
<div>
{% for child in item %}
{% set currentChild = child.activeNode ? 'current' : '' %}
<a href="{{child.url}}" class="{{currentChild}}">
{{child.label}}
</a>
{% endfor %}
</div>
</div>
{% endif %}
{% endfor %}
</div>
Create a new module and call it search_results or something. Add to module.html
:
<div id="js-search-app" ></div>
You could pass data from your module to the app if you'd like. For example:
<div
id="js-search-app"
data-language="{{module.language}}"
data-placeholder="{{module.search.placeholder}}"
data-none="{{module.search.no_results}}"
data-pathprefix="{{module.search.path_prefix}}"
></div>
First, add PREACT and create a folder for your search app.
Make a GET request to the Hubspot Search API:
fetch(`https://api.hubapi.com/contentsearch/v2/search?portalId=${portalId}&term=${searchTerm}`)
.then(
// handle the response
).catch(
// handle errors
)
The response will look something like this:
{
"total": 20,
"offset": 0,
"limit": 9,
"results": [
{
"id": 123456789,
"score": 0.017206732,
"type": "SITE_PAGE",
"domain": "www.example.com",
"url": "https://www.example.com/lorem",
"language": "nl-nl",
"title": "Example post title",
"description": "Lorem ipsum <span class=\"hs-search-highlight hs-highlight-html\">dolor</span> iset amet."
},
{
// ...another result
},
{
// ...another result
}
]
}
The portalId
and term
are required fields. You can pass additional parameters like offset, limit, pathPrefix, language, etc. Check the docs.
The pathPrefix
parameter helps you limit search to parts of your website. It "specifies a path prefix to filter search results. Will only return results with URL paths that start with the specified parameter. Can be used multiple times." source
You can create a search form in your app and set the search term from there. For example:
<form onSubmit={this.handleSubmit}>
<input
type="search"
placeholder="Search..."
value={this.state.searchTerm}
onChange={this.handleSearch}
/>
<input type="submit" value="search" />
</form>
// ...
handleSearch(searchTerm) {
this.setState({
searchTerm: searchTerm,
})
}
handleSearchSubmit() {
this.setState({
posts: [],
offset: 0
}, this.fetchPosts )
}
Or create a search_form module and pass the data to your app. For example:
<form method="GET" action="/search-results">
<input
type="search"
name="ss"
placeholder="Search..."
value=""
/>
<input
type="submit"
value="Send"
/>
</form>
The action can be any page as long as that page contains the search_results module (step 1). Then, check for url params in the search app:
componentDidMount() {
let urlParams = new URLSearchParams(window.location.search)
if ( urlParams.has('ss') ) {
this.setState({
searchTerm: urlParams.get('ss')
}, this.fetchPosts )
} else {
this.fetchPosts()
}
}
Create a new module and name it 'blog_listing' or something.
In meta.json
set host_template_types
to [ "BLOG_LISTING" ]
.
Loop through contents
in module.html
to show posts. For example:
{% for content in contents %}
<div>
<a href="{{content.absolute_url}}">
{{ content.name }}
</a>
{% if content.post_list_summary_featured_image %}
<img src="{{ content.post_list_summary_featured_image }}" alt="{{ content.featured_image_alt_text | escape }}" />
{% endif %}
{% for topic in content.topic_list %}
<a href="{{ blog_tag_url(group.id, topic.slug) }}">{{ topic.name }}</a>{% if not loop.last %}, {% endif %}
{% endfor %}
{% if content.blog_post_author %}
<a href="{{ blog_author_url(group.id, content.blog_post_author.slug) }}">
{{ content.blog_post_author.display_name }}
</a>
{% endif %}
</div>
{% endfor %}
You can loop through the tags with blog_tags()
:
{% set tags = blog_tags('default', 250) %}
{% for item in tags %}
{% set current = item.slug in content.absolute_url ? 'current' : '' %}
<a href="{{ blog_tag_url(group.id, item.slug) }}" class="tag {{current}}">{{ item }}</a>
{% endfor %}
An example of pagination:
{% if not simple_list_page %}
<div>
{% if last_page_num %}
<a class="prev" href="{{ blog_page_link(last_page_num) }}">Previous</a>
{% endif %}
<a class="all" href="{{ blog_all_posts_url(group.id) }}">All posts</a>
{% if next_page_num %}
<a class="next" href="{{ blog_page_link(next_page_num) }}">Next</a>
{% endif %}
</div>
{% endif %}
simple_list_page
is true if a user clicks the 'all posts' link.
You can use blog_author
to check if we're on an author archive page:
{% if blog_author %}
<div>
{% if blog_author.avatar %}
<img src="{{ blog_author.avatar }}" alt="{{ blog_author.display_name }}">
{% endif %}
<h3>{{ blog_author.display_name }}</h3>
<p>{{ blog_author.bio }}</p>
</div>
{% endif %}
You can also create a blog module field.json:
{
"name" : "blog_id",
"label" : "Blog",
"type" : "blog"
}
Tip: You can check Hubspot's own Blog Content module for inspiration.
You can create one big module for a single post or break it up in multiple modules (hero, content, similar posts, etc). But always set host_template_types
to [ "BLOG_POST" ]
in the meta.json
.
To print the post content, you need to use:
{{ content.post_body }}
To print the title, date, topics, author and featured image:
<h1>{{ content.name }}</h1>
<span>{{ content.publish_date_localized }}</span>
{% if content.topic_list %}
{% for topic in content.topic_list %}
<a href="{{ blog_tag_url(group.id, topic.slug) }}">{{ topic.name }}</a>{% if not loop.last %}, {% endif %}
{% endfor %}
{% endif %}
{% if content.blog_post_author.avatar %}
<img src="{{ content.blog_post_author.avatar }}" />
{% endif %}
<a href="{{ group.absolute_url }}/author/{{ content.blog_post_author.slug }}">
{{ content.blog_post_author.display_name }}
</a>
{% if content.featured_image %}
<img src="{{ content.featured_image }}" />
{% endif %}
To get similar posts (i.e. in the same topic):
{% set tag_posts = blog_recent_tag_posts('default', content.topic_list[0].slug, 3) %}
{% for post in tag_posts %}
<h2>{{ post.name }}</h2>
{% endfor %}
Tip: You can check Hubspot's own Blog Content module for inspiration.
Navigate to Marketing > Files and Templates > Design Tools.
Create a new blog template and name it Blog Listing or something. Add your blog listing module to the new template (and any other modules, such as header and footer).
Then, create another blog template and name it Blog Post or something. Add your blog post module(s) to the new template.
Navigate to Settings > Website > Blog and click "Create new blog". Choose a Blog root URL in the General tab. Next, go to the Templates tab and choose the templates we just made.
The blog won't show until you have at least one post.