Skip to content

HubSpot facts#

These were verified against real portals while building the first site. They are portable to any HubSpot CMS site. Each one cost time to find.

Pages API#

Create a site page as a draft:

POST /cms/v3/pages/site-pages
{"name": "…", "slug": "…", "templatePath": "<Theme>/templates/base.html", "state": "DRAFT"}

Set the layout with a PATCH. The dnd area's name comes from the template (commonly main_content, not dnd_area; it is dnd_area in hubspot-site.json):

PATCH /cms/v3/pages/site-pages/{id}
{"layoutSections": {"main_content": {
  "type": "cell", "name": "main_content", "w": 12, "x": 0,
  "rowMetaData": [{"cssClass": "dnd-section", "styles": {"padding": {"top": …, "right": …, "bottom": …, "left": …}}}],
  "rows": [{"0": MODULE_CELL}]
}}}
MODULE_CELL = {"type": "custom_widget", "name": "…", "w": 12, "x": 0,
               "params": {"path": "/<Theme>/<module>", "module_id": …, "schema_version": 2, "…fields": "…"}}

Publish: POST …/{id}/schedule {"id": …, "publishDate": "<future>"}. The date must still be in the future when HubSpot validates it. One hs api round trip can take over a minute on a cold start, so the publisher schedules 150 seconds ahead. The call returns 204 with no body. Poll currentState until the page is live.

Fact Detail
Nested columns A cell (w, x, its own rowMetaData and rows) inside a section row.
Row keys The cell's x as a string: {"0": cell w8, "8": cell w4}. An index key renders fine but breaks the editor ("Column key for row does not match X value").
Section background rowMetaData[i].styles.backgroundImage {imageUrl, backgroundSize: "cover", backgroundPosition: "MIDDLE_CENTER"}.
Section padding styles.breakpointStyles.default.padding. The crawl will also see mobile overrides, so don't confuse the two.
Module ids module_id from meta.json equals moduleId on every portal.
Live widget params Carry only module_id (no path). Another portal accepts them as they are.
Head HTML Sent as headHtml. A PATCH alone doesn't reach the served page; a republish does.
Content staging Staged pages are ordinary page ids on the sandbox domain; their draft can be PATCHed. CONTENT links to draft pages resolve to the live URLs.
Landing pages A separate object (/cms/v3/pages/landing-pages) with the same layoutSections shape.

Keys and scopes#

  • A developer test portal's personal access key has the content scope and can create and publish pages.
  • A personal access key on a production portal only offers CMS Pages read. Writing drafts needs a service key with Content and Files (and Forms) scopes.
  • The test portal's key may lack the Forms scope. Form modules then stay unselected on dev, and forms are created and selected by hand.
  • HubSpot video player ids belong to one portal. Drop them when copying a layout between portals.

Module fields#

Restriction Detail
name Rejected (MODULE_VALIDATION_RESTRICTED_FIELD_NAME). Use full_name.
label, style Rejected.
items Reserved in HubL. Use questions, stats, …
text fields on the Style tab Rejected with a bare internal error. Put anchor ids and classes at the end of the Content tab.
Visibility rules Only evaluate within one tab. A Style-tab rule that points at a Content-tab field never fires.
file fields Hand back a string, not an object: read module.x.url or module.x.
Removed choices A saved page holding a value that has left the choice list fails editor validation.

Design Manager and File Manager#

  • hs cms upload silently drops .webp files. Use jpg, png or svg for theme assets.
  • A module upload is live on every page using it within a minute. Modules have no draft state.
  • The File Manager CDN caches a 404 for about 5 minutes (s-maxage=300). Probing a URL before uploading poisons it, so probe with a cache-buster query.
  • Fonts and assets the theme loads from outside its folder must be uploaded to every portal separately.
  • {% menu id=… %} falls back to the menu named default when the id doesn't exist. menu(id) (the function form) does not.
  • Menus can be created over the API:
POST /content/api/v4/menus
{"name": "footer", "pagesTree": {"children": [
  {"label": "…", "url": "…", "linkTarget": null, "isPublished": true, "isDeleted": false, "children": []}
]}}

CLI#

  • --account <name> with a name that isn't configured silently uses the default account. Run hs account list first, every time.
  • Developer test portals expire after 90 idle days.