CloudPage Maestro

A browser extension that brings batch operations to Salesforce Marketing Cloud's CloudPages — publish, unpublish, move, search, download, and export landing pages and code resources without leaving SFMC.

What it is

CloudPage Maestro injects a fixed-position panel into the SFMC interface. It hits SFMC's own internal /cloud/fuelapi/ proxy using your existing session cookies — no backend, no OAuth flow, no server-side credentials. Everything runs in the browser.

Who is this for

SFMC operators who manage more than a handful of CloudPages assets. The native SFMC interface forces one-at-a-time interaction with landing pages. This extension lets you select 50 pages and unpublish them at once, download every HTML in your business unit as a ZIP with the folder tree preserved, or run a quick search across content and folder paths that the SFMC UI doesn't surface.

What it isn't

This is not an official Salesforce product. It rides the internal endpoints that SFMC's own UI uses, so it stays available as long as those endpoints do. It works within whichever Business Unit you're currently signed into and inherits all SFMC permission gates.

Features

The extension breaks into five capability groups. Everything below is implemented and shipping in v1.0.0.

Bulk Publish / Unpublish

Select any number of landing pages or code resources, click Publish or Unpublish. Concurrent batches of five with a progress toast and automatic 401 recovery.

Bulk Folder Move

Tree picker with full SFMC category hierarchy. Move dozens of assets to a new folder in one operation.

Search & Filter

Server-side search across name, content, description, and folder. Type filters for Landing Pages, JSON, JavaScript, and CSS code resources.

Download All

Single click ZIPs every HTML and code resource source file with the SFMC folder tree preserved. Flat "HTML only" mode also available.

Export to CSV

Every asset, every page, fully enriched with status, URL, folder, and customer key. UTF-8 BOM ensures Excel renders correctly.

Live URL Preview

Hover any published landing page URL to see a live iframe preview of the page without leaving the panel.

Dark / Light Mode

Theme toggle in the header. Every surface, badge, toast, modal, and dropdown adapts. Preference persists across sessions.

Keyboard Shortcuts

Esc closes the panel, Ctrl+Shift+F focuses search, Ctrl+A selects all visible rows.

Installation

Two browser-extension paths and one userscript fallback. Use the Chrome extension if you can; the Firefox extension if you can't; the Tampermonkey variant only if browser-extension installation is restricted on your machine.

Chrome (recommended)

  1. Clone or download this repository.
  2. Open chrome://extensions/ in Chrome.
  3. Enable Developer mode using the toggle in the top-right.
  4. Click Load unpacked and select the CloudPage_Maestro_Chrome/ folder.
  5. Navigate to your SFMC instance — the panel auto-activates on any *.exacttarget.com or *.marketingcloudapps.com page.

Firefox

  1. Open about:debugging#/runtime/this-firefox.
  2. Click Load Temporary Add-on….
  3. Select CloudPage_Maestro_Firefox/manifest.json.
  4. Navigate to SFMC. The panel auto-activates the same way.
Firefox temporary install

Firefox unloads temporary add-ons when the browser closes. For permanent installation you need to sign the extension or use Firefox Developer Edition / Nightly with xpinstall.signatures.required set to false.

First-run checklist

  • You're already logged into SFMC in the same browser profile.
  • The panel appears as a slide-in from the right edge of the page.
  • The token badges in the panel header turn green within a few seconds of opening.
  • The asset list populates within ~2 seconds on a typical account.

System Overview

Three moving parts: a content script that owns the entire UI, a service worker that proxies API calls and listens for tokens, and a slim JSZip dependency for the bulk download feature.

graph LR USER[User] --> PANEL[Content Script Panel] PANEL --> SW[Service Worker] SW --> SFMC[SFMC API] SFMC --> SW SW --> PANEL SW -.captures.-> TOKENS[chrome.storage.local] TOKENS -.reads.-> SW style USER fill:#0176d3,stroke:#0176d3,color:#fff style SFMC fill:#1f7a3a,stroke:#1f7a3a,color:#fff

Components

FileRoleSize
manifest.json MV3 declaration. Permissions storage and webRequest. Host permissions for the three SFMC domains. 58 lines
background.js Service worker. Captures x-csrf-token headers, stores them, acts as a fetch proxy with credentials: 'include' so cookies flow on cross-origin reads. ~180 lines
content.js Single content script. UI panel, state in window.CPM_STATE, search / filter / sort, batch ops, downloads, exports, theming. ~6,400 lines
lib/jszip.min.js ZIP archive builder used only by Download All. Loaded before content.js in the manifest. 97 KB

Why a single content script

Splitting the content script into ES modules requires dynamic import() from chrome-extension:// URLs — workable but adds friction. For a single-feature panel the trade-off didn't pay off: scrolling 6,400 lines of cohesive code is easier than chasing types across ten files.

Authentication

The original architecture used a "ghost tab" pattern: invisible popup windows spawn to make SFMC perform authenticated requests, the service worker captures the x-csrf-token headers, every API call sends them back. v1.0.0 rebuilt this around a cookie-only proxy that SFMC's own SPA uses internally.

The cookie-only proxy

SFMC exposes https://mc.{stack}.exacttarget.com/cloud/fuelapi/... as an internal proxy that authenticates against session cookies, not CSRF tokens. The same endpoints reachable through the brittle content-builder.{stack}.marketingcloudapps.com and cloud-pages.{stack}.marketingcloudapps.com hosts are reachable through this proxy without any token gymnastics.

For reads, every request goes through this proxy. The service worker forwards with credentials: 'include'; the browser attaches the user's SFMC session cookies; the proxy authenticates and serves the response.

For writes (publish, unpublish, move), the legacy CSRF path is still required. The service worker captures the x-csrf-token via chrome.webRequest.onBeforeSendHeaders from outbound traffic the user generates by normally navigating SFMC. Hidden iframes pre-warm the capture by loading SFMC's Content Builder and CloudPages app routes — those loads trigger internal API calls whose headers we observe.

Net effect

Panel open went from 15+ seconds (waiting on ghost-tab CSRF capture) to under 2 seconds. Read failures from stale tokens went to zero. Write operations still need a valid CSRF token but auto-recapture and retry on 401.

Token badge states

The header surfaces two badges. They actively probe the server — clicking a badge re-probes on demand, and the badges auto-refresh every four minutes.

StateMeaningWhat you can do
OK Token was just verified against the server (200 response). Everything works.
STALE Token is present but the probe failed. The session may have expired. Operations will likely 401 and auto-recapture.
MISSING No token captured. The user hasn't navigated SFMC enough to trigger capture. Click the badge to manually trigger iframe-based capture.
CHECKING Probe in flight. Pulsing dot animation. Wait a second.

Auto-recovery on 401

Every write operation is wrapped in a retry layer. On a 401 / 403 / EBADCSRFTOKEN response, the extension:

  1. Surfaces a "Publish token expired — refreshing and retrying" toast.
  2. Injects hidden iframes to recapture the CSRF token.
  3. Polls chrome.storage.local until a fresh token lands.
  4. Retries the operation exactly once with the new token.
  5. If the retry also fails, surfaces a hard error.

API Endpoints

Every endpoint the extension calls, grouped by which auth model it uses.

Cookie-only reads — mc.{stack}.exacttarget.com/cloud/fuelapi/

No CSRF token needed. Session cookies authenticate.

MethodPathPurpose
POST /asset/v1/content/assets/query?scope=ours Paginated asset search across all CloudPages types
GET /asset/v1/content/assets/{id} Single asset details (used by single-asset download)
GET /asset/v1/content/assets/{id}/file SFMC-generated thumbnail (base64 via SW)
GET /asset/v1/content/categories/{id} Single category metadata for path resolution
GET /asset/v1/content/categories?categoryType=cloudpages Full CloudPages folder tree
GET /internal/v2/cloudpages/landing-pages Bulk landing-page list with status, URL, siteId — front-loads enrichment
GET /internal/v2/cloudpages/sites?siteAssetId={id} Per-landing-page enrichment for items not in the bulk response
GET /internal/v2/cloudpages/landing-pages/{siteId}/states/ Step 2 of HTML download (state ID lookup)
GET /internal/v2/cloudpages/landing-pages/{siteId}/states/{stateId}/contents/ Step 3 of HTML download (actual HTML payload)

CSRF writes — cloud-pages.{stack}.marketingcloudapps.com

Requires x-csrf-token header. Captured via webRequest.onBeforeSendHeaders.

MethodPathPurpose
POST /fuelapi/internal/v2/cloudpages/landing-pages/{id}/publish Publish a landing page
POST /fuelapi/internal/v2/cloudpages/landing-pages/{id}/unpublish Unpublish a landing page
POST /fuelapi/internal/v2/cloudpages/code-resources/{id}/publish Publish a code resource
POST /fuelapi/internal/v2/cloudpages/code-resources/{id}/unpublish Unpublish a code resource
PATCH content-builder.{stack}.marketingcloudapps.com/fuelapi/asset/v1/content/assets/{id} Move asset to a new folder (used by batch move)

Data Flow

What happens between "user clicks Refresh" and "table populates."

sequenceDiagram participant U as User participant P as Panel participant SW as ServiceWorker participant SFMC as SFMC U->>P: Click Refresh P->>P: showLoading skeleton rows par CB query P->>SW: MAKE_REQUEST assets/query SW->>SFMC: POST /cloud/fuelapi/assets/query SFMC-->>SW: 200 items SW-->>P: items and V2 bulk P->>SW: MAKE_REQUEST landing-pages SW->>SFMC: GET /cloud/fuelapi/landing-pages SFMC-->>SW: 200 entities SW-->>P: entities end P->>P: Merge V2 onto CB items P->>P: renderTable + progressiveReveal P->>P: enrichVisibleItems for non-V2 rows loop per non-enriched row P->>SW: MAKE_REQUEST sites SW->>SFMC: GET SFMC-->>SW: 200 SW-->>P: site data P->>P: patchEnrichedRow end

The Content Builder query and the V2 bulk landing-pages query fire in parallel via Promise.all. V2 returns status, URL, siteId for every landing page in one round trip — no per-item enrichment needed if the V2 response covered the row. Only code resources and orphaned landing pages fall through to the per-item sites?siteAssetId enrichment, and even that runs in concurrent batches of ten.

Batch Operations

Three batch operations: publish, unpublish, move. Each follows the same pattern.

Concurrency model

Bulk publish and unpublish run with concurrency 5 — five operations in flight at any moment. Batch move runs with concurrency 5 as well. Reads (enrichment) run with concurrency 10. The limits were tuned to fit comfortably under SFMC's rate limits while still parallelizing enough to feel instant.

const CONCURRENCY = 5;
for (let i = 0; i < items.length; i += CONCURRENCY) {
  const slice = items.slice(i, i + CONCURRENCY);
  await Promise.all(slice.map(operation));
}

Progress feedback

A bottom-right toast slides in showing the action verb, an animated spinner, a progress bar, and "current / total · pct%". The toast survives panel renders (it's a body-level fixed element) and updates in place across successive ticks. On completion the spinner stops and turns green; the toast slides out shortly after.

No auto-reload

Batch operations do not auto-refresh the table when they finish. The success notification reads "Click Refresh to update." This is intentional — auto-refresh on a 500-item account with stale enrichment would feel slower than letting the user trigger refresh when they're ready.

Search & Filter

Two ways to narrow what's visible.

Server-side search

Type a query in the search box. The extension POSTs to the Content Builder asset query endpoint with a name / content / description / category.name compound filter. Results paginate at 20 per page in search mode. Press Esc or clear the search input to return to the unfiltered view.

Type filters

The stats grid below the header is also a filter row. Click any tile to filter the table to that asset type:

  • Overview — every asset across every page (no filter)
  • Landing Pages — only LP assets on the current page
  • JSON — only JSON code resources
  • JavaScript — JS code resources + code snippet blocks
  • CSS — only CSS code resources
Scope note

The Overview tile shows the entire Business Unit's asset count. The four type tiles show counts on the current page only — that's why a "100" total can break into 73 / 12 / 8 / 7 even though there are 5,000 assets across all pages. Each tile has a "all assets" / "on this page" caption to make this explicit.

Download & Export

Three output options. Two ZIP-based for source files, one CSV for metadata.

Download All — folder tree

Header → Download All dropdown → "All files + folder tree". Steps:

  1. Paginate through every Content Builder asset in the Business Unit.
  2. Fetch the complete CloudPages folder tree (every category, including ones with no direct assets).
  3. For each asset, fetch its source — three-step chain for landing pages, single GET for code resources.
  4. Place each file into a JSZip archive under FolderA/FolderB/AssetName.ext, matching the SFMC hierarchy exactly.
  5. Generate the ZIP, trigger browser download as cloudpage-maestro-files-YYYY-MM-DD.zip.

Download All — flat HTML

Same dropdown → "HTML only (flat)". Filters to landing pages only and skips folder nesting. Result is a single-folder ZIP of Name.html files. Useful when you want to grep across HTML or feed it to a static-site converter.

Export All to CSV

Header → Export All. Fetches every page of assets, enriches each with status and URL, and produces a CSV with twelve columns:

  • Name, Type, Status, Folder (full path), Modified Date, Created Date
  • Modified By, Created By, Customer Key, Asset ID, Site ID, URL

UTF-8 BOM is prepended so Excel renders accented characters correctly.

Single-asset download

Every row has a download icon next to the asset name. Landing pages download as name.html. Code resources download with the appropriate extension (.json, .js, .css).

Dark / Light Mode

Theme toggle lives in the header. Preference persists in chrome.storage.local under cpm_theme. Every surface — panel chrome, section cards, stats grid, table, modals, toasts, dropdowns, badges, skeleton shimmer — adapts.

Dark palette

The panel uses a calibrated dark palette designed for GitHub-style readability. No pure black backgrounds, no oversaturated accents, no neon glows.

  • #0d1117 — panel background
  • #161b22 — section cards
  • #1f242c / #21262d — borders and inset elements
  • #388bfd / #58a6ff — accent / hover
  • #e6edf3 / #8b949e / #6e7681 — primary / secondary / muted text

Light palette

SFMC's Lightning blue at #0176d3 drives the header gradient and primary buttons. Section cards are pure white on a warm off-white panel background. Borders are #e7e8eb hairlines.

Performance

Numbers from a 700-asset dev account on Chrome stable, no profiling overhead.

Phasev1.0.0Pre-migration baseline
Service worker startup → token ready~150 ms (cookie-only, no capture)15–30 s (ghost-tab capture)
Panel open → first 100 rows rendered~1.5 s15+ s
Refresh after publishing a page~3 s60+ s
Batch unpublish of 50 items~12 s~150 s sequential
Download All (700 assets, ZIP)~90 snot available

What made the difference

  1. Cookie-only proxy for reads. Eliminated the ghost-tab token capture entirely.
  2. V2 bulk endpoint returns status + URL + siteId for every landing page in one call. Pre-migration each landing page needed its own enrichment GET.
  3. Concurrent enrichment. Sequential for-await loops became Promise.all batches of ten.
  4. Concurrent batch writes. Publish / unpublish loops also moved to Promise.all batches of five.
  5. 5-minute enrichment cache. Repeat navigations of the same page within five minutes skip the enrichment GETs entirely.

Bottlenecks that remain

Download All is bounded by SFMC's response times for the HTML chain (three round-trips per landing page). At ten-concurrent, a 700-asset account takes about a minute and a half. Caching the V2 bulk response in the service worker with a 10-minute TTL is a possible future win for re-opens.

Troubleshooting

Panel doesn't appear after install

  • Check you're on *.exacttarget.com or *.marketingcloudapps.com.
  • Hard-reload the SFMC tab after installing the extension.
  • Open chrome://extensions/ and confirm the service worker status is active with no errors.

Token badges stay red

  • Make sure you're logged into SFMC in the same browser profile.
  • Navigate to Content Builder or CloudPages once — that triggers passive token capture.
  • Click the "Capture Tokens" button next to the badges to force iframe-based capture.
  • Click either badge to manually re-probe.

Publish / unpublish fails with 401

The extension auto-detects this and recaptures the CSRF token before retrying. If it still fails after recapture, the SFMC session itself is invalid — re-authenticate in a new SFMC tab and refresh.

"Session Lost" but the asset list still works

Reads no longer require the Content Builder token (cookie-only proxy). The Session probe occasionally fails on a single endpoint while other reads succeed. Click the badge to re-probe.

Download All gets stuck mid-fetch

Click Cancel. SFMC occasionally rate-limits aggressive concurrent fetches — wait 60 seconds, then retry. Failed items land in _FAILED.txt inside the ZIP for selective retry.

Service worker errors after extension reload

Chrome occasionally garbage-collects MV3 service workers. The next message from the content script wakes it up. If you see Could not establish connection. Receiving end does not exist, click Refresh in the panel.

Limitations

  • Requires an active SFMC session — the extension piggybacks on browser cookies, no embedded credentials.
  • Works within the currently-selected Business Unit only. Switch BU in SFMC and click Refresh in the panel.
  • Subject to SFMC API rate limits. The default concurrency (10 reads, 5 writes) is conservative; aggressive use over short periods may trigger throttling.
  • Firefox temporary add-ons unload when the browser closes. Permanent install requires signing or developer-edition flags.
  • Single browser tab only — the panel is per-tab. Multiple SFMC tabs each get their own panel instance.
  • Unofficial community tool. Not affiliated with or supported by Salesforce. Internal endpoints are subject to change.

Tampermonkey Variant

A legacy userscript variant lives at tampermonkey_cloudpages_maestro.user.js. It predates the v1.0.0 cookie-only migration and is missing the dark mode, the Download All folder tree fix, the new progress toast, and the auto-recovery on 401.

It's preserved for machines where browser extension installation is restricted by IT policy. Install Tampermonkey, paste the userscript, save. The script auto-injects on SFMC pages the same way the extension does.

Not the primary install path

If you can use a browser extension, use the Chrome or Firefox version. The userscript is functional but lacks several quality-of-life features and won't get the same level of ongoing attention.

Specifications

Tech stack

Manifest versionMV3
JavaScriptVanilla ES2020+ (no framework, no build step)
DependenciesJSZip 3.10.1 (Download All only)
Permissionsstorage, webRequest
Host permissions*.exacttarget.com, *.marketingcloudapps.com, *.sfmc-content.com

Browser support

BrowserStatusNotes
Chrome 109+Primary targetTested daily on stable channel
Edge 109+WorksSame Chromium runtime, same install steps
Firefox 109+WorksTemporary install only without signing
SafariNot supportedDifferent extension format

Versioning

Current release: v1.0.0. Semantic versioning. Major bumps signal breaking changes to the public-facing extension behavior. Minor for new features, patch for bug fixes.

Repository

Source code: github.com/MetalHacker01/CloudPage_Maestro

Author

Aldorino Rrushi · @MetalHacker01 · Portfolio

For the SFMC community. Use at your own risk.