Going to live#
There are four ways to put work on the live portal, and none of them publishes anything. A module upload is the one change that is visible immediately, which is why it goes through a gate.
In Claude Code
The hubspot-live-change skill of the bb-hubspot plugin covers this page, and only you can start it: type /bb-hubspot:hubspot-live-change. The guard hook refuses a live module upload until today's before-* snapshot and the module's backup exist.
| What | Tool | Lands as |
|---|---|---|
| A module | hs cms upload, behind the regression gate |
Live on every page that uses it, within a minute |
| A new page | publish-page.py <slug> --account <client>-live |
A DRAFT site page |
| A change to an existing page | patch-live-draft.py |
The page's draft, or a Content Staging copy |
| A dev page copied into a landing page | stage-landing-page.py |
A DRAFT in Content Staging |
Live assets (heroes, logos, posters) go to the live File Manager first, in the same folder paths as on dev, and are probed for a 200 response before any page references them.
The visual-regression gate#
Every module upload to live is wrapped in a before/after snapshot of every live page:
hs cms fetch "<Theme>/<module>.module" backups/live-modules/<date>/<module>.module --account <client>-live
python3 $BBH/scripts/visual-regression.py snapshot before-<x>
hs cms upload <theme>/<module>.module "<Theme>/<module>.module" --account <client>-live
python3 $BBH/scripts/visual-regression.py snapshot after-<x>
python3 $BBH/scripts/visual-regression.py compare before-<x> after-<x>
A snapshot saves, for each page in pages.json that has a live source: the served HTML as it came, a normalised
copy (scripts, nonces, asset hashes and cache-busters stripped), and full-page screenshots at 1440px, plus 390px for
a representative set.
To pass: no HTML change, and no page above 0.5% of pixels changed. Changes between 0.1% and 0.5% are reported as notes. Where a page has dynamic content (carousels, video), read the diff image before calling it noise.
Fetch the portal's copy before you upload
The portal can be ahead of git. Once, someone else's round of changes was on the portal but not yet in git, and
restoring from git would have reverted it. Always hs cms fetch the live module into the git-ignored
backups/live-modules/<date>/ first. That copy is also your rollback.
Upload one module at a time, and only additive modules or changes whose defaults reproduce the current look (see Module conventions). When the only visible change is intended (for example, a card-title rewrap on eight pages), the client reviews it and signs it off in the log.
New pages as live drafts#
python3 $BBH/scripts/publish-page.py <slug> --account live
This is the same publisher as on dev, in draft-only mode. It authenticates with the service key from .env, creates
or updates a DRAFT site page, rewrites dev File Manager URLs to live ones, and never calls schedule. It refuses to
touch a page at the target slug that it didn't create itself. It records live_page_id and the editor URL in
pages.json, so a person can open the editor and click Publish.
Changing an existing live page#
The safest place for a change to an existing page is HubSpot Content Staging. A person stages the page (which creates a copy on the sandbox domain), and the tool patches that copy:
# dry run: prints the layout diff and the preflight checks, writes nothing
python3 $BBH/scripts/patch-live-draft.py <live-slug> --staging <staging page id> \
--replace <widget-name> <draft-slug>:<section>
# swap a widget for a draft section's module, or replace one field from a JSON file
python3 $BBH/scripts/patch-live-draft.py <live-slug> --staging <id> --replace <widget> <draft>:<section> --apply
python3 $BBH/scripts/patch-live-draft.py <live-slug> --staging <id> --set-field <widget> <field> <file.json> --apply
Every other byte of the layout stays as it was. The preflight checks that the module exists on live and that every
asset returns 200. After --apply, the tool reads the draft back to verify it. --insert-before adds a new row
instead of replacing one. Without --staging, the tool patches the live page's own /draft. Either way it never
publishes: publishing from staging is a person's click.
Landing pages through staging#
Landing pages are a separate API object. stage-landing-page.py <dev site-page id> <live landing-page id>
copies a dev page's layout into an existing Content Staging landing-page shell that a person created. It rewrites
File Manager URLs and PATCHes layoutSections only (name, slug, template and meta stay as they were). Watch for
things the old page had that the new one doesn't, such as a lead form.
Mirroring live onto dev for review#
To show a reviewer "this live page, but with module X instead of module Y" without rebuilding the page from its copy doc:
python3 $BBH/scripts/clone-live-page.py <live-slug> <dev-slug> --replace <widget> <draft>:<section> [--dry-run]
The script reads the live page (read-only, and keeps a copy in backups/live-pages/<date>/), swaps the named
widgets, drops HubSpot video player ids (they belong to one portal), creates and publishes the page on dev, and
registers it with cloned_from. --insert-before adds a row instead of replacing one. Live widget params carry
only module_id, with no path, and dev accepts them as they are.
Pages on another theme can't be mirrored
A live page built on an older theme's template has no main_content dnd area, so there is nothing to clone. The
stagger-loading branch adds a clear message for this, plus --template (put the copy on a different template)
and --head-html (carry the live page's head snippet). Both move to main when that branch merges.