← Plugin catalog
Developer Tools

Unbounce - Classic Builder

Unbounce Marketing Solutions Inc. v1.0.0

Publisher description

From the marketplace listing

Connect your Unbounce account to create and edit landing pages, add A/B test variants, set traffic splits, and check page performance without leaving the chat. Describe the page you want and Unbounce will build it; ask for a new variant, a headline change, or a conversion report and it's handled in your account. Works with your existing Unbounce pages, domains, and page groups. Requires an Unbounce account and the Classic Builder.

Language: English · Automatically detected from descriptions.

Files & skills

File archives

Plugin package12 files · 30 KBBrowse files →
Skill instructions
mcp-feedback4.55 KB

View saved version →

---
name: mcp-feedback
description: Record a user's feedback about the Unbounce MCP tools — a bug, praise, a feature request, confusion, or anything else — straight to the team via the submit_feedback tool, with no copy/paste. Use when the user wants to report a bug, share feedback, praise something, request a feature, or flag that a tool was confusing. Also covers login/connection issues — the user can't connect the MCP in their client, sees an OAuth error ("access denied", "invalid_grant", a failed redirect), or keeps getting asked to log in again. You may also offer it after a hard MCP tool failure. It confirms the exact content with the user and redacts secrets before anything is stored.
requires:
  mcpServers:
    - unbounce-mcp
---

# Submit Unbounce MCP feedback

Record the user's feedback about the **Unbounce MCP tools** to the team's store by
calling the **`submit_feedback`** tool. The tool persists the record server-side, so
nothing has to be copied out of the conversation into Slack or anywhere else.

> The `mcp-` prefix here means "pertains to Unbounce MCP," not "about a page." This
> skill is about capturing feedback on the MCP tools themselves — whether they broke,
> delighted, confused, or fell short.

## Hard rule: report, don't repair

**Do NOT try to fix, retry, or work around the thing being reported.** This skill
records feedback on what already happened. Never invent tool-call arguments or error
text — anything not actually present is `unknown`. (Fixing the underlying task, if the
user wants that, is separate work done after the feedback is recorded.)

## 1. Gather the feedback

Settle two things — from the conversation where possible, asking only what you can't
infer:

- **`type`** — one of `bug` · `praise` · `feature_request` · `confusion` · `other`.
- **`message`** — the feedback in the user's own words. Keep it faithful; don't
  editorialize.

For a **`bug`**, also assemble the **`context`**: the verbatim failing tool call(s)
from the transcript — exact tool name, exact arguments JSON, and the raw result/error
text (call out any `code`/`reason`/`remedy` fields). Include the relevant one(s),
especially the failing call. If the failure happened in another session and isn't in
the transcript, ask a couple of targeted questions and mark transcript-only fields
(exact arguments, exact raw error) as `unknown`. For non-bug types, `context` is
usually unnecessary — include a light pointer (the tool or page in question) only if
it genuinely helps.

**Login / connection problems** are a `bug`: capture what the user was doing, the
client they're in, and the exact error text (e.g. `access denied`, `invalid_grant`, a
failed redirect, repeated re-login prompts). Note that if the MCP can't connect _at
all_, `submit_feedback` itself won't be reachable — that's expected, and step 4's
inline fallback is how the feedback still gets out.

## 2. Redact secrets

Replace any access token, API key, password, or bearer token in the `message` or
`context` with `«REDACTED»`. Count how many you redacted. (The server re-scrubs as a
backstop, but do your pass first — the user is about to review this content.)

## 3. Confirm the exact content, then submit

Show the user the **exact record** you're about to store — the `type`, the `message`,
and the `context` if any (post-redaction) — and get an **explicit yes** before
calling the tool. This is the consent step; do not skip it and do not submit
silently. (Same ask-first bar the page tools hold for Dynamic Text Replacement.)

On yes, call **`submit_feedback`** with `type`, `message`, and `context` (if any). The
server stamps identity, timestamp, and build itself — you don't pass those.

## 4. On failure, don't lose the feedback

If `submit_feedback` fails (an error result, the tool isn't available, or an older
deploy predates it): **retry once.** If it still fails, **print the composed,
already-redacted record inline** in the conversation and tell the user to pass it to
the team directly. There is no file to write — the goal is simply that the feedback is
never silently lost.

## 5. Report back

On success, print to the conversation — nothing more:

1. **Recorded.** A one-line plain-language summary of what was captured, with the
   `type`.
2. The returned **`feedback_id`**.
3. **Only if the server redacted something you missed:** `Server redacted N
additional secret(s).` (compare the tool's `redacted_secrets` to your own count).

Example:

> Recorded your **bug** report: `set_page_url` returned `DOMAIN_NOT_FOUND` for a domain
> `list_client_domains` had just listed. (feedback_id `a1b2c3d4`)
mcp-modernize8.45 KB

View saved version →

---
name: mcp-modernize
description: Rewrite an imported (absolutely-positioned) Unbounce page variant as clean responsive HTML through Unbounce MCP. Use when the user asks to modernize a page, make a page responsive, get rid of absolute positioning, make a clean-HTML version of an existing Unbounce page, or edit a page that was built in the Unbounce builder — phrasings like "modernize this page", "make my page responsive", "convert my old page", "clean HTML version". Replication-faithful — this is a layout rewrite, never a redesign.
requires:
  mcpServers:
    - unbounce-mcp
---

# Modernizing an Unbounce page (responsive rewrite)

Turn a page built in the Unbounce builder into clean, responsive flexbox/grid
HTML — **pixel-faithful to the original on desktop, reflowing sensibly on
mobile**. This is a REPLICATION task with zero creative latitude: same copy,
same images, same fonts, same colors, same element counts. The only thing that
changes is the layout model (absolute positioning → flex/grid).

The pipeline is two deliberate stages:

1. **Import** — `create_variant_from_existing_variant` clones the builder-built
   source into an MCP-managed variant that renders identically (deterministic;
   every string, image URL, font, and form field lands in the copy verbatim).
2. **Rewrite** (this skill) — read that copy, restructure its layout to
   responsive flex/grid, and write it back **in place** with
   `update_variant_from_html`.

Extraction fidelity is stage 1's job and it is deterministic — never work from
the original builder page directly, and never re-derive content from screenshots
or memory. Your job is only the layout transformation.

## Language with the user

Never say "modernize", "modernization", "replica", or platform-internal terms
to the user. Their page is fine; say "make your page responsive" and "an
editable copy of your page". The result cannot be edited in the Unbounce
builder — all future edits go through MCP tools; say so at handover, and never
suggest the builder.

## Workflow

**Step 1 — Resolve the target.** Identify the page (`list_client_pages`) and
variant (`list_page_variants`). If the target variant is builder-built (not
MCP-managed), tell the user in one line what is about to happen — e.g. _"I'll
create an editable copy of your page, then make that copy responsive — your
original stays untouched"_ — then run `create_variant_from_existing_variant`
(no `target_page` ⇒ new page). No further permission ceremony is needed; the
copy is additive and safe. Relay any conversion warnings honestly. A
fixed-width "autoscale" source converts fine — the conversion report notes that
runtime viewport scaling isn't preserved; that is expected and harmless here,
because you rewrite the page responsive from scratch (Step 4), discarding the
scaling entirely. If the target is already an MCP-managed imported copy, skip
straight to Step 2.

**Step 2 — Read the copy.** `get_variant` on the imported variant. Download the
`html_ref` / `css_refs` sources and any `scripts` entry's `ref` (via `download`, or
`download_inband` if you cannot run shell commands). This body is the **sole
source of truth** for every value: copy strings, image URLs (with their
`srcset`), `@font-face` blocks, colors, dimensions, form fields, `<ub:dynamic>`
tags. Never guess a value, never take one from a screenshot, never reconstruct
from memory. If the copy came from an autoscale source, its CSS wraps lengths in
`calc(<px> * var(--scale, 1))` and it may carry a viewport scale script —
ignore both: read the plain px value (the `* var(--scale)` is the fixed-width
scaling you are removing) and never copy the scale script into the rewrite. Your
responsive layout comes from the Step 3 rows, not these fixed dimensions.

**Step 3 — Get the layout skeleton and fidelity signals.** `get_layout_hints`
on the same variant. It returns, per section: `rows` (element ids clustered
into visual rows, left-to-right), per-element geometry,
`cross_section_overlays` (elements whose vertical span crosses into later
sections), and `design_width` (the fixed canvas width). **The rows are your
flex/grid structure.** Do not derive structure from the absolute coordinates
yourself, and never translate `top`/`left` values through into the rewrite.
It also returns measured fidelity signals — `page_defaults` (the page's real
base text color and typography), per-section backgrounds, per-element
colors/typography/text/image/link inventories, `palette`, `loaded_fonts`,
`row_stats`, `content_extent`, `hidden_elements`. Those are ground truth:
author from them per [rules/color.md](rules/color.md), never from assumption.
If hints come back empty for a variant that plainly has a positioned tree,
stop and report it (see [rules/verification.md](rules/verification.md)).

**Step 4 — Audit, in writing, before any HTML.** Post a compact audit into the
conversation (it must survive context compaction): per section — the row
structure from hints, the element **counts** per row, every text string
(verbatim), every image URL + srcset, fonts, colors, form fields
(name/type/order/options/submit label), interaction states (`:hover`/`:focus`),
and every cross-section overlay with its pattern (column vs centered). Every
value in your output must trace back to an audit row.

**Step 5 — Write the responsive HTML.** Follow
[rules/layout.md](rules/layout.md) (section/inner max-width pattern, flex/grid
from hint rows, overlay handling) and
[rules/replication.md](rules/replication.md) (verbatim preservation). If the
user also asked for a change ("modernize it and swap the headline"), use
two-pass authoring: pass 1 is a content-identical rewrite, verified; only then
apply their change (see TWO-PASS in rules/replication.md).

**Step 6 — Self-check before writing back.** Run the checklist in
[rules/verification.md](rules/verification.md) — text walk, count checks, the
closure checks against the extracted signals (image-src, link-href, palette,
font sets; the WCAG contrast table from [rules/color.md](rules/color.md)), and
the hard stops (`position:absolute` budget, zero authored `cbc-*` references,
flex/grid present, per-section max-width, overlay columns reserved). Fix
failures before delivering; never rationalize past a hard stop.

**Step 7 — Write back in place and hand over.** `update_variant_from_html` on
the same variant. It is a TRUE FULL REPLACE: pass the complete artifact set
(html + all css + all js) every time — anything omitted is dropped. The change
is staged, never published by you. Hand the user: what you did (one line), the
`preview_ref` from a fresh `get_variant` (plus the builder preview link from
`list_page_variants` for the original, so they can compare), the evidence
summary from the self-check, and any genuine differences — concrete and
specific, or "none". Publishing, traffic, and goals are the user's call.

## Hard invariants (the load-bearing rules)

- **No creative changes. No additions.** Same typo, same order, same counts.
  Nothing added that has no source element — no nav, no sticky header, no hover
  effects the source lacks. Conversion-optimization defaults from
  `mcp-page-authoring` do NOT apply to a rewrite — replication trumps
  landing-page best practices.
- **Every source text string appears verbatim** in the rewrite (whitespace-
  insensitive). Element names, ids, and image filenames are NOT content.
- **Reuse every asset by its exact URL**, keeping `srcset`/`image-set` and
  `background-position`/`size`/`repeat` verbatim. Copy `@font-face` blocks and
  `<ub:dynamic>` tags through unchanged.
- **Forms:** plain `<form>` in your output; preserve field names, types, order,
  options, and the submit label exactly (the server re-wires submission on
  write). Preserve the hidden form-confirmation overlay (`cfm-*` markup) and
  its trigger script verbatim if the copy has one.
- **Preserve user customizations verbatim**: custom-HTML embeds, tracking
  scripts, hand-applied CSS classes. Flag (don't rewrite) any script selectors
  that target old ids.
- **No subagent delegation.** The source must stay in the same context as the
  build — a subagent without the verbatim source invents copy.
- **Stay in your lane at delivery**: staged in-place update, no publish, no
  traffic, no goal changes.

Long-form rules, worked examples, and the catalog of real past failures:
[rules/replication.md](rules/replication.md) ·
[rules/layout.md](rules/layout.md) ·
[rules/color.md](rules/color.md) ·
[rules/verification.md](rules/verification.md) ·
[rules/failure-catalog.md](rules/failure-catalog.md)

Referenced files: 5

mcp-page-authoring24.4 KB

View saved version →

---
name: mcp-page-authoring
description: Best practices for authoring an Unbounce landing page through Unbounce MCP — writing the HTML/CSS/JS for create_page_from_html, create_variant_from_html, and update_variant_from_html. Use when the user asks to create, build, design, generate, or edit an Unbounce landing page or variant, or to personalize page text by URL parameter (Dynamic Text Replacement). Applies only to pages authored through Unbounce MCP, not pages hand-built in the Unbounce builder.
requires:
  mcpServers:
    - unbounce-mcp
---

# Authoring an Unbounce MCP page

These rules apply when you write the HTML/CSS/JS body for `create_page_from_html`,
`create_variant_from_html`, or `update_variant_from_html`. The page is a full-bleed
custom-HTML page — you own the markup end to end. Design the page to be optimized
for conversions: by default, every layout, copy, and hierarchy decision should
serve the page's single conversion goal.

This covers **MCP-authored pages only**, and the split is exclusive both ways:

- A page hand-built in the Unbounce builder is not editable this way —
  `update_variant_from_html` refuses it. Replicating or modernizing a
  builder-built page is a different, audit-first job.
- An MCP-authored page **cannot be edited in the Unbounce builder**. Never tell
  the user they can open, tweak, swap images, or finish the page "in the
  Unbounce builder/editor" — they cannot. Every edit goes through these MCP
  tools: the user asks for the change, or supplies updated HTML.

## Updating a variant: HTML/CSS replace, JS is left alone

`update_variant_from_html` full-replaces the body HTML and the stylesheet, so
send every stylesheet the variant should keep on every call. **JavaScript is the
exception: omit `scripts` and the variant's existing custom-JavaScript elements
are kept**, reported back as `scripts_preserved`.

That matters because a variant can carry scripts nobody authored through these
tools — an **Unbounce popup** attaches itself as one. So:

- Editing HTML or CSS? Just omit `scripts`. The popup and anything like it
  survive, and you don't need to know they were there.
- Changing the scripts? Pass `scripts` with the **complete** desired set — it
  replaces all of them, including external includes. Whatever it drops comes
  back in `removed_scripts`. Those entries — and the `scripts_preserved` ones —
  are already in `scripts` entry form: pass one straight back to restore it,
  unchanged, no translation. A plain include appears as `src` (with `async` /
  `defer` if it had them); everything else — including an include whose tag
  carries other attributes, such as a `data-*`-configured analytics embed — comes
  back as verbatim `tag`. Read the variant first
  (`get_variant`) if you don't know what's there.
- Removing every script is deliberate: pass `scripts: []`.

## Writing a `scripts` entry

Each entry is one custom-JavaScript element. Use exactly one of:

- **`ref`** — an `upload://` reference to a .js file. Its text is the script
  body; don't wrap it in `<script>` tags. A bare string entry is shorthand for
  this.
- **`src`** — an absolute http(s) URL to include, for a third-party embed: an
  Unbounce popup (`https://<id>.js.ubembed.com`), a tag manager, a chat widget, a
  pixel. Add `async` / `defer` here if the vendor's snippet has them. This is the
  right form when the user hands you a `<script src=…>` tag.
- **`tag`** — verbatim `<script>` markup, for what the other two can't say: a
  `type="module"` script, a tag with both a `src` and a body, or several tags
  that belong together.

Add `name` (what the user calls it — it labels the element in Unbounce's Custom
JavaScripts panel) and `placement` (`head`, `body_top`, or `body_bottom`,
default). Consent managers, tag managers, and anti-flicker snippets belong in
`head`; most other things are fine at the default.

`get_variant` returns every script in the matching form, so you can read, edit,
and re-submit any of them unchanged.

**A `<script>` in the body HTML does run on the published page** — it is stored
verbatim in the Custom HTML block and served as real markup. But it lands
body-placed, unnamed, and invisible in the builder's Custom JavaScripts panel, so
prefer a `scripts` entry whenever the user names the script or asks for a
particular placement.

## Conversion-focused structure

A landing page exists to convert — design toward that goal affirmatively, not
just by obeying constraints. Treat the bullets below as the default posture: if
the user explicitly asks for something that works against conversion (a nav menu,
multiple offers, a long form), note the trade-off briefly once, then build what
they asked for.

- **Benefit-led headline.** The headline states the visitor's payoff and matches
  the offer and traffic intent — not the company name or a clever tagline.
- **CTA prominence and hierarchy.** One primary call to action, visible without
  scrolling; size, contrast, and whitespace should point the eye at it. On longer
  pages, repeat the CTA as anchor links.
- **Minimal form friction.** Ask only for the fields the offer genuinely needs —
  every extra field costs conversions. Label the submit button with the payoff
  ("Get the guide"), not "Submit".
- **Real social proof near the ask.** Place testimonials, numbers, or logos the
  user supplied next to the form or CTA. **Never fabricate trust signals** — no
  invented testimonials, star ratings, customer logos, statistics, or scarcity
  claims ("Only 3 spots left!"). If the user hasn't supplied any, ask for it while
  you're still gathering requirements; once you're already generating the page,
  leave social proof out and mention the omission so the user can supply proof
  for a follow-up edit.
- **One form.** A lead-gen page has exactly one `<form>`. Never render two. CTA
  buttons in sections where the form isn't visible are anchor links
  (`<a href="#form">`) that scroll to the single form — not extra forms. Pages
  with no form (click-through pages) are exempt.
- **No navigation.** No nav bars, menus, or footer link lists. Every outbound
  link is an exit that costs conversion. The only acceptable links are the primary
  CTA anchors and legally required links (privacy/terms), placed quietly in the
  footer.
- **Responsive, clean HTML.** Use normal document flow (flexbox/grid), relative
  units, and `max-width` — not absolute positioning. The page must reflow on
  mobile.

## Assets

- **Prefer `upload` over fat data URIs.** Base64 data URIs work but bloat the
  payload (~33% larger) and count against the 1 MB tool-input cap. Upload real
  images/fonts first and reference the returned CDN URL. Relative refs in your
  bundle and data URIs are rehosted for you; identical bytes are de-duplicated
  automatically against the client's asset library.

- **One upload call per bundle.** Every `upload` / `upload_inband` call creates a
  fresh upload folder, and a relative reference inside your HTML/CSS (e.g.
  `src="page.js"`, `url(images/hero.png)`) resolves only within the HTML's own
  folder — so upload the HTML together with everything it references by relative
  path in one call. Refs from different calls still work anywhere a tool takes an
  explicit `upload://` reference (`html_ref` / `css_refs` / a `scripts` entry's
  `ref`, or written
  in full inside the markup); refs are immutable snapshots, so re-uploading a file
  never changes what an earlier ref points to.

- **An image's bytes must match its extension.** The file extension declares the
  type an asset is stored and served as, so the bytes are checked against it: a PNG
  named `.svg`, or anything that isn't a real image, is refused rather than stored
  under the wrong type. Name files for what they actually are.

- **SVGs must be static artwork.** An SVG is served as a live document, so one
  carrying `<script>`, an `on…=` event handler, a `<foreignObject>`, a
  `javascript:` URL, or an external `href` is refused. Ordinary exported artwork —
  including animated SVG — is fine. If an SVG is rejected, flatten it on export or
  use a PNG.

## Authoring from a client that can't run shell commands

`upload` and `download` hand you `curl` commands to run locally. If your client
has no shell or filesystem (a web/desktop chat that only calls tools), you can't
run them — use the **in-band** pair instead:

- **`upload_inband`** — pass each file's **text content inline** (an array of
  `{ path, content }`); it returns the same `upload://` references you'd get from
  `upload`. Wire those to `create_page_from_html` / `create_variant_from_html` /
  `update_variant_from_html`'s `html_ref` / `css_refs` / `scripts` exactly as
  usual — those tools don't change.
- **`download_inband`** — pass the `upload://` references from `get_variant` (or
  `upload_inband`) and it returns the content **inline in the result**, so you can
  read a variant back and iterate.

Constraints, because the bytes travel through the conversation:

- **Text only.** HTML/CSS/JS. **Images and fonts must be external `http(s)`
  URLs** — you can't upload binary bytes this way. Host the image elsewhere and
  reference its URL, or reuse an existing asset's `cdn_url` from `list_assets`;
  `create_page_from_html` leaves external URLs untouched. `download_inband`
  refuses a binary or over-2 MiB object and tells you to use `download`.
- **Keep files small.** The request rides the normal transport, so a very large
  paste will fail — author lean pages.
- Because your images are always external URLs, keep `check_external_refs` at its
  default (`fatal`) so a dead image URL is caught before the page is created,
  not after publish.

## Scripts, forms, and the auto-injected bits — don't fight them

- **Don't hand-wrap JS in `<script>`.** Provide raw JavaScript, not a `<script>`
  block — it's wrapped for you, so wrapping it yourself double-wraps it. (This is
  the opposite of what you'd do writing a static HTML file.)
- **Form submission works out of the box; the confirmation is a customizable
  fallback.** Submission is wired up automatically from the form fields you
  author — no plumbing needed. If you don't handle the post-submit experience
  yourself, a default confirmation dialog is shown. To customize it, register a
  post-submit handler — see the next bullet for the one way that works.
- **Register post-submit handlers with `.push(fn)`, never `= fn`.** The published
  runtime stores `window.ub.hooks.afterFormSubmit` as an **array** and invokes it
  with `Promise.all(hooks.map(h => h(...)))`. Assigning a function replaces the
  array, so the first submit throws `TypeError: hooks.map is not a function`, the
  post-submit promise rejects, and **neither your custom confirmation nor the
  default dialog ever shows**. Always push:

  ```js
  window.ub = window.ub || {};
  window.ub.hooks = window.ub.hooks || {};
  window.ub.hooks.afterFormSubmit = window.ub.hooks.afterFormSubmit || []; // defensive
  window.ub.hooks.afterFormSubmit.push(function () {
    // reveal your confirmation panel here
  });
  ```

  This is independent of whether the form is multi-step. When reviewing a page,
  grep the controller for `afterFormSubmit =` and rewrite any assignment to a push.

- **A signature comment is auto-injected** at the top of every variant body
  (Unbounce MCP version · client · timestamp). It's re-stamped on each write — don't
  duplicate it, don't strip it, and don't treat it as page content when you read a
  variant back.

## Conversion goals — ask the user what should count

A page converts when a visitor does the thing it exists for. Unbounce counts
that through **conversion goals**: the form submission (on by default) and any
links you nominate. After creating or updating a page, the tool result's
`conversion_goals` block lists every candidate — the form plus each distinct
link/phone URL with its anchor texts (`get_variant` shows the same for an
existing page). Use it to have the goal conversation:

- **Ask the user which candidates should count**, then call
  `set_conversion_goals` with the **complete** desired set (it replaces, not
  adds). Recommend the goal that matches the page's purpose: a click-through
  page's goal is its CTA link, not an incidental newsletter form.
- **Goals are URL-identified.** Every anchor pointing at a chosen URL converts —
  the candidate's `anchor_texts` length shows how many places that is. Two
  near-identical URLs (trailing slash, query param) are separate candidates;
  point that out if they look like the same destination.
- **A trackable CTA must be a real `<a href>`** (http(s) or `tel:`). JS-driven
  buttons can't be tracked — goal tracking rewrites the href. `#anchors` and
  `mailto:` can't be goals (platform rule).
- **Turning the form goal off** (`form_submission: false`) still captures every
  lead; it just stops counting submissions as conversions.
- **Link/phone goals don't show in the Unbounce builder's Conversion Goals
  panel** — but they _are_ tracked. That panel lists only builder-native elements
  (form, button, image, linked text box), and an MCP-authored page is a single
  custom-HTML element, so its anchors never appear there even though a click on
  the published page counts as a conversion. If a user asks why a link goal is
  "missing" in the builder, that's expected: manage these goals here with
  `set_conversion_goals`, not in the builder (editing goals in the builder can
  drop them). The form goal is unaffected — a form is builder-native.
- **Changing goals on a published page** redefines its conversion rate
  mid-history. Relay the tool's stats note: `reset_page_stats` gives a clean
  baseline if the user wants one (destructive — their call).
- Goals persist across edits automatically — updates re-apply them and report
  any new candidate URLs or orphaned goals; you only need `set_conversion_goals`
  when the _set_ should change.

## Google Analytics click tracking — add it on every page

When a page's domain has the Google Analytics integration on, the published page
already tracks pageviews and form submissions — but **not clicks on links**,
because Unbounce's built-in GA link tracker only attaches to elements the Unbounce
builder produces, which an MCP page (hand-written `<a>` tags) doesn't have. Close
that gap on every page you author:

- **Include [`scripts/ga-click-tracking.js`](scripts/ga-click-tracking.js) as a
  `scripts` entry** (name it e.g. "GA click tracking", default `body_bottom`).
  Read the file and pass its contents verbatim — don't hand-roll your own. It
  wires every `<a>` and emits builder-matching GA events.
- **It's safe by default.** The script feature-detects `gtag`/`ga` and no-ops when
  the domain has no GA integration, so add it unconditionally — that's why it's
  default-on, not something to ask about.
- **Never add the GA loader** (`gtag.js` / `analytics.js` / a `gtag('config', …)`
  call). GA is injected by Script Manager; a second loader double-counts
  pageviews. This script only sends events.

The why, the guardrails, and the maintenance note live in
[rules/google-analytics.md](rules/google-analytics.md) — read it if you need to
explain the behaviour or hit an edge case.

## Third-party form endpoints (Insightly, Marketo, HubSpot, Pardot, …)

Sometimes the form must POST leads to an external system — the user is cloning a
page with an existing vendor-hosted form handler, or their CRM owns the lead
flow. That is the one case where you deliberately bypass the built-in form
handling described above.

- **A literal `<form>` gets taken over.** Submission of any `<form>` you author
  is wired to Unbounce lead capture automatically — the `action` you wrote is
  not what handles the submit. To preserve an external endpoint, don't put the
  `<form>` in the HTML: leave a mount point (`<div id="form-mount"></div>`) and
  **inject the form markup at runtime from your JavaScript** (build the markup
  as a string, set the mount's `innerHTML`, then load the vendor's scripts in
  the order the reference page loads them). The runtime leaves JS-injected
  forms alone. The injected string must be the vendor's documented embed
  snippet (or the reference page's markup) reproduced verbatim — never
  interpolate unsanitised user-supplied or URL-derived values into it.
- **Say what the user gives up.** A JS-injected form bypasses Unbounce lead
  capture entirely: no leads in Unbounce, no
  conversion tracking, and `get_variant` reports `has_form: false`. Submissions
  exist only in the external system. (`has_form` is derived from the authored
  HTML source, so it is deterministically false for an injected form — that's
  expected, not a defect.) State this trade-off when you propose the approach —
  the user may prefer the built-in form plus a separate CRM integration
  instead.
- **`afterFormSubmit` hooks never fire for an external form.** The
  `window.ub.hooks` machinery only runs for Unbounce-captured forms. Build the
  post-submit experience (thank-you message or redirect) into your own code, the
  way the vendor's embed does it.
- **Expect domain allowlists — the top cause of "the button does nothing".**
  reCAPTCHA site keys and vendor form handlers are typically locked to approved
  domains. On a preview or test-domain URL the key fails domain validation, no
  token is issued, the submit callback never runs, and clicking the button
  silently no-ops — the form is not broken, the domain isn't approved. Warn the
  user up front that the form can't fully work until the page's final domain is
  added to the reCAPTCHA key and the vendor's allowed domains.
- **Verifying before go-live.** Have the user open the browser console and click
  submit — a reCAPTCHA "Invalid domain for site key" error confirms the
  allowlist diagnosis. To exercise the rest of the flow on a test domain, they
  can temporarily swap in Google's public reCAPTCHA **test** site key, then
  restore the real key before publishing to the approved domain. Never leave
  the test key on a live page — it returns a passing token for **every**
  request, including automated bots, so spam and abuse submissions flow
  straight through to the vendor endpoint unchallenged.
- **The one-form rule still applies.** The injected form is the page's single
  form; other CTAs anchor-link to its section.

## Dynamic Text Replacement (DTR)

DTR personalizes page text from a URL query parameter, so the same page shows
different words depending on how it was reached — e.g. visiting with
`?city=Portland` shows "Portland" where the page would otherwise read "Vancouver".
Common for paid-search keyword insertion and location/audience personalization.

There is no DTR tool: you add it by writing a `ub:dynamic` tag directly in the
HTML you author; it takes effect when the page is served.

```html
<ub:dynamic method="titlecase" parameter="city">Vancouver</ub:dynamic>
```

- **`parameter`** — the URL query-string key that supplies the replacement value
  (`parameter="city"` → `?city=Portland`). Pick a short, lowercase name that
  matches the meaning of the text.
- **Default text** — the tag's inner text (`Vancouver` above) is shown when the
  page is visited without that parameter. Always make it a sensible standalone
  value; most visitors arrive with no parameter and see exactly this.
- **`method`** — casing applied to the incoming value: `titlecase` (the right
  default for display text), `uppercase`, `lowercase`, or `capitalized`. Omit
  `method` to insert the value exactly as passed.
- **`wrap`** (optional) — set `wrap="true"` to wrap the substituted value in a
  `<span class="ub-dynamic">…</span>` so you can style or target just the dynamic
  text with CSS. Omitted, the value is inserted inline. Rarely needed.
- **Testing** — resolution happens at serve time on the **published** page (editor
  previews show the default, not a substituted value). To verify, publish, then
  load the page with `?<parameter>=SomeValue` appended — and give the user that
  test URL so they can check it.

**Ask before adding DTR.** Do not add it on your own initiative. When creating or
editing a page, ask the user whether they want any text personalized by a URL
parameter; if so, confirm which text and which parameter name(s), and wrap only
the text they approve. If they decline, author plain text.

**DTR is a paid feature — it may be plan-gated.** If the client's account isn't
entitled to Dynamic Text Replacement, the content-write tools reject HTML
containing a `<ub:dynamic>` tag with a `…dynamic-text-not-entitled` error (the
`bulk_edit_variants` batch reports it as a per-variant `error`). When that
happens, either remove the `<ub:dynamic>` tag(s) and author plain text, or tell
the user their plan needs Dynamic Text Replacement to use it — check the plan
with `get_account_plan`. Already-published DTR pages keep working regardless.

**DTR also works in the page title and meta fields.** `set_page_metadata`'s
`title`, `description`, and `keywords` accept `<ub:dynamic>` tags — e.g. a
`<title>` that echoes the ad keyword for paid-search campaigns. The same
ask-first rule and plan gate apply. The Open Graph fields do **not** take DTR
(social scrapers fetch the page without campaign parameters anyway).

## Page metadata (SEO & social sharing)

The page `<title>`, meta description, robots noindex, favicon, and Open Graph
(`og:*`) tags are **page settings, not page HTML** — your authored HTML becomes
the page **body**, where search engines and social scrapers never look for
them. Set them with `set_page_metadata`; read them back via `get_variant`'s
`metadata` block. As a safety net, the HTML-authoring tools **auto-extract**
these head tags from submitted markup and apply them as metadata (reported
under `head_metadata` in the result) — but on `update_variant_from_html` /
`create_variant_from_html` that lands on **one variant only**, so prefer
`set_page_metadata` for deliberate metadata work and to converge variants.

- **Always set a title and description** on a page the user intends to publish
  — a page without them shows the platform default (or nothing) in search
  results and social shares. Derive them from the page's offer; keep the title
  ≲60 characters and the description ≲160 (search results truncate past that).
- **Open Graph drives the social share card** (Facebook, LinkedIn, Slack,
  iMessage, …). `type: "website"` is right for landing pages; give it a
  `title`, `description`, and an `image` (1200×630, under 5 MB, https) — a
  card without an image renders as bare text. X/Twitter reads these `og:*`
  tags too, but shows the small card without a `twitter:card` meta tag — which
  the platform cannot store, so don't promise a large Twitter card.
- **Skip `keywords`.** Google has ignored meta keywords since 2009 and heavy
  keyword lists can read as a spam signal. Leave it unset unless the user
  insists.
- **`hide_from_search_engines: true`** emits a robots noindex — right for
  campaign pages the user doesn't want in organic search results; ask rather
  than assume.
- **Metadata is applied to every active variant by default** (variants share
  one URL, so divergent titles/OG make the search snippet and share card
  depend on the traffic split). Target one variant with `letter` only when the
  user deliberately wants that.
- **Staged like content**: on a published page the live head keeps its old
  values until the next `publish_page` — say so when you set metadata on a
  live page.
- **Head tags with no platform slot** — `twitter:*`, `<link rel="canonical">`,
  hreflang alternates, `og:*` beyond type/title/description/image/url — cannot
  reach the published `<head>`; the authoring tools warn when submitted HTML
  contains them. Don't promise them to the user.

## Reviewing your work

- Read a variant's current content back with `get_variant`.
- **Previewing locally:** the source refs `get_variant` returns (`html_ref` /
  `css_refs` / a `scripts` entry's `ref`) are streams of one page and do **not** render
  individually — `body.html` opened alone shows an unstyled fragment. For a
  local preview, fetch the also-returned **`preview_ref`** (`preview.html`, via
  `download` or `download_inband`) and open that file: it is a standalone
  composed document (images load from their CDN URLs; form submission is
  disabled in it). It is **view-only** — never edit it or submit it back as
  `html_ref`; make edits in the source files and resubmit those.
- Content is **staged** by the create/update tools; it goes live on `publish_page`.
- `list_page_variants` returns a `preview_url` per variant — a logged-in link for
  a human to **review** a staged variant before publish. It is a viewing
  affordance only, not an editing path — don't present it as a way to edit.

Referenced files: 2

Package details

Publisher declarations from the archived package. These are separate from our research and the live service's terms.

Package author
Unbounce Marketing Solutions Inc.

Package observed Oct 2, 2026.

Technical details
First seen
Sep 30, 2026 · 22:02 UTC
Last seen
Oct 2, 2026 · 00:00 UTC
Collection status
Collected

plugin_asdk_app_6a7a3df1e608819188444011032d91cc

Download plugin data (JSON)