How to build an Unbounce alternative

How to build an Unbounce alternative publishes one landing page and a lead form, while the public URL keeps serving the revision you published.

  • guide
  • vibe-coding
  • unbounce

How to build an Unbounce alternative is one offer, four sections, and a public URL that serves the revision you published. Unbounce is that product. The catalog line is “A landing page, a form, and a number that went up.”

The card lists $29–$249 a month. The pricing note says Starter at $29 a month through Optimize at $249 a month, and it leaves Concierge and Agency quotes out. The source on the card is Unbounce’s pricing. The first estimate is 4 hours. The project name is Hookpage. The verdict is “One page, one form”.

The card also names Leadpages and Instapage, with their prices left off.

A personal page with stacked sections is the Carrd guide. Carrd is $2–$4 a month, about 2 hours, and the Carrd post is that teardown. A home page plus a collection is the Webflow post. Webflow is $15–$39 a month, about 12 hours. A popup on a site you already run is OptinMonster, $21–$147 a month, in Vibe code your own OptinMonster.

The sample page is Saturday workshop. The slug is saturday. The host you attach later is offer.flaviocopes.com. The hero says “Ship the small version”. The test lead is Lea, [email protected].

How to build an Unbounce alternative from one offer

The Unbounce page splits the work into three rows.

The first row is 4 hours. A landing page, a lead form, and publish. One page. Four section types. A draft you can edit, and a published revision the public URL reads.

The second row is 2 days, 16 hours. A/B variants, pixels, and conversion counts. The third row is a month and up, 160 hours. A visual builder, AI traffic routing, and agency workspaces.

If you tell an agent to “clone Unbounce”, it aims at the third row. Paste the prompts below and keep Hookpage on the first row.

What the 4-hour build does

You edit a hero, a proof block, an FAQ, and a form. Preview sits on the editor, in your session. Publish checks the headline, the email field, and the thank-you line, then points the public URL at that revision and clones a fresh draft. The public page at /saturday renders the published revision. A later edit stays on the draft until you publish again.

A visitor submits a name and an email. A hidden field source is saturday, because you defined it. The honeypot company_website is empty for a person. A new email inserts one lead tied to the published revision id. The same email again shows the thank-you line and inserts nothing. The inbox lists the lead, exports a CSV, and can POST a signed webhook.

What it leaves out

Two variants, a pixel, and a count per revision are the 16-hour row. A freeform canvas, AI routing, popups, and a workspace per client are the 160-hour row. A multi-question interview is the Typeform guide. Typeform is $29–$99 a month, about 2 hours. A form with a webhook and a results table, and no page around it, is the Tally guide. Tally is $29–$89 a month, about 8 hours.

4 HOURS · SHIP One page, four sections Draft stays off the URL One lead per email 16 HOURS · NEXT Two variants, weights 50 and 50 One pixel URL Counts from lead rows 160 HOURS · SKIP A visual builder AI traffic routing Agency workspaces
Hours are the three estimates on the Unbounce card. The 4-hour box is the build in this guide.
WHICH PAGE YOU ARE RENTING Carrd · one personal page 2 hours · $2–$4 Unbounce · one offer, one form 4 hours · $29–$249 Webflow · pages and a collection 12 hours · $15–$39 Typeform · one question at a time 2 hours · $29–$99 OptinMonster · a popup 4 hours · $21–$147
Prices and hours are the first row on each card. This guide is the Unbounce row.

Where the draft and the public page split

Node.js and Astro render the editor and the public page. Use TanStack Start if React is your weekday language. The stack post is which tab you copy. The rows live in D1. The card’s capability line is Astro, HTMX, and D1. A plain form post is enough for publish and for the lead. There is no object storage in this row. Section text is columns, not files.

The editor writes the draft revision. It never updates the revision the public pointer names. Publish is one transaction. It checks the draft, marks that revision published, sets published_revision_id, and inserts a new draft copied from those sections. If the check fails, the transaction rolls back. The live pointer stays on the previous revision, or on nothing if you have never published.

If D1 is down during publish, the form shows an error. The public page keeps serving the last good revision. If D1 is down on a public GET, the response is 503. An empty page would look like you unpublished the offer.

The public POST is a different path. It reads the published revision, then validates. A honeypot value returns the thank-you state and writes no lead. A bad email returns the form with the name still filled and the email error beside the field. More than 5 posts in an hour from one truncated address returns 429 and the line “Try again in a bit”. The counter stores the hour bucket and a hash. For IPv4, drop the last octet before hashing. For IPv6, keep the first 48 bits. The raw address is not a column.

A new email inserts the lead in a transaction with the unique key of page id plus normalized email. Normalization is trim and lowercase. [email protected] is [email protected]. After the commit, if you saved a webhook URL, the server POSTs and waits up to 8 seconds. Success marks delivered. A timeout or a non-2xx marks failed. The visitor still sees the thank-you line. A duplicate email hits the unique key, writes nothing, skips the webhook, and shows the same thank-you line.

A crash after the insert and before the POST leaves the lead at pending. The inbox retry sends the same lead id. The next visitor submit of that email is the duplicate path, so it will not send a second time by itself.

OWNER'S BROWSER Edit the draft, then publish ASTRO ON THE REQUEST Draft writes never touch live Publish is one transaction Public GET reads the pointer D1 Page, revisions Sections, leads PUBLIC POST Validate, then insert One row per email SIGNED WEBHOOK After the lead commits Failure stays on that lead D1 DOWN Public GET is 503 OUTSIDE THIS VERSION A second page Variants, pixels, visitor profiles A visual canvas
The draft and the live revision are different rows. The form writes a lead, then calls the webhook.

What a page stores

The card lists LandingPage, Section, Variant, Lead, and ConversionEvent. The 4-hour row uses the page, the section, and the lead. A variant is a published revision with a weight, and that column can wait. A conversion event is a count of leads per revision. Store the revision id on the lead now, so the 16-hour row can group rows you already have. There is no visitor table.

A page is Saturday workshop. One row in this version. A second create is refused. It stores the slug saturday, the owner, and published_revision_id. The slug is lowercase letters, digits, and hyphens, up to 40 characters. It is chosen at create and the editor has no field to change it, so printed links and the webhook stay put.

A revision belongs to that page. It is draft or published. One draft is open. Publish freezes the current draft and opens another. Older published revisions remain, because leads point at them. The public URL follows published_revision_id only.

A section belongs to one revision. The types are hero, proof, faq, and form, one of each, in that order. Hero stores a headline, up to 120 characters, and a subhead, up to 160. Proof stores up to three lines, each up to 120. FAQ stores one question, up to 200, and one answer, up to 600. Form stores the thank-you line, up to 160, the hidden pairs you defined, and the webhook URL if you set one. Fields are plain text. The renderer escapes them. A headline that contains <script> shows those characters on the page.

The form’s visitor fields in this row are name and email. Name is required, up to 80 characters. Email is required, up to 200, with one @ and a dot in the domain. Hidden pairs are keys you typed. The reserved key company_website is the honeypot, and save refuses it as a hidden field. Extra keys in the POST body are dropped.

A lead is one email on this page. It stores the published revision id, the normalized email, the name, the hidden values you defined, the time, and a webhook status: not_configured, pending, delivered, or failed. Delete removes the row and frees the email, so the same address can submit again as a new lead.

The rate-limit row is an hour bucket, a hash, and a count. It is not a lead and not a profile.

PAGE · SATURDAY WORKSHOP slug saturday · one row pointer to the live revision REVISION draft, or published Old published rows stay SECTIONS hero, proof faq, form LEAD one email revision id on the row LATER A weight on a published revision A count grouped by that id A visitor profile table
The lead remembers which revision was live. The public URL follows the pointer.

The screens you ship

The page list, the editor, the public page, and the inbox. Preview is the editor, and it requires the owner session. There is no route you can copy for the draft.

Page list

One row. The name, the slug, draft or published, the lead total, Edit, and View. View is disabled until a revision is published. New page is not on this screen.

WIREFRAME · PAGES Saturday workshop /saturday · published 1 lead EDIT
One offer. The lead total is a count of rows, and it updates after a real submit.

Editor

Four blocks, stacked. The hero fields, three proof lines, one FAQ, and the form settings: thank-you line, hidden source = saturday, webhook URL. A preview column shows the draft with the same escape rules as the public page. Publish sits at the bottom. When the headline is empty, Publish stays disabled and the empty field is named.

WIREFRAME · EDITOR Hero · Ship the small version Proof · One offer on the page. FAQ · Do I need an account? Form · name, email, source=saturday Thank you · You're on the Saturday list. Preview is this draft PUBLISH Empty headline keeps this draft
Preview is the draft, in the editor. Publish is the moment the public URL moves.

Public page

The published hero, proof, FAQ, and form. The button says “Save my seat”. The honeypot is in the HTML for a bot and off screen for a person. After a successful post, or a duplicate, the form is replaced by “You’re on the Saturday list.” Unknown slugs and the slug before the first publish use one not-found page. That page does not include draft copy.

WIREFRAME · PUBLIC Ship the small version One offer on the page. Do I need an account? Name · Lea Email · [email protected] SAVE MY SEAT
The public page is the published revision. The draft headline is on the editor only.

Inbox

Search by email, the lead total, and a CSV button. Each row shows the name, the email, the time, the revision id, source, and the webhook status. Retry appears when the status is pending or failed. A logged-out request sees sign-in and an empty body.

From a draft to one lead

You fill the four sections. Headline “Ship the small version”. Proof line “One offer on the page.” FAQ question “Do I need an account?” and answer “The page is the signup.” Thank-you “You’re on the Saturday list.” Hidden field source = saturday. You open /saturday before publish. The not-found page comes back, and the words “Ship the small version” are absent.

You publish. /saturday shows the hero. You change the draft headline to DRAFT-ONLY-HEADLINE. You reload /saturday. The live headline is still “Ship the small version”. The editor preview shows the draft words.

You submit Lea, [email protected]. The thank-you line replaces the form. The inbox has one row, webhook not_configured if you left the URL empty. You submit [email protected] again. The inbox still has one row. You submit with company_website set to “acme”. The thank-you line shows, and the inbox still has one row.

You set a headline that includes <script>alert(1)</script>, publish, and view source. The page contains the escaped characters. The browser does not run the script.

You put =1+1 in a name on a second test lead, export CSV, and open it. That cell is text, with a leading single quote, so a spreadsheet does not treat it as a formula.

EDIT THE DRAFT HEADLINE AND EMAIL FIELD? NO Stay draft, name the field YES Point the URL, clone draft LATER EDIT STAYS DRAFT PUBLIC KEEPS THE OLD HERO D1 DOWN · PREVIOUS STAYS Variants arrive on the 16-hour row
A failed check leaves the draft. A later edit does not move the live hero.
POST THE FORM HONEYPOT FILLED? YES Thank you, no row NO Check the email INSERT OR UNIQUE HIT NEW One lead, then webhook SAME EMAIL Thank you, still one row WEBHOOK FAIL · LEAD STAYS 429 after 5 posts in an hour
The thank-you line shows for a person, a duplicate, and a honeypot. Only a new email inserts.

Prompt 1. One page and a draft that publish can freeze

Copy the Astro prompt from Build your own Unbounce. Paste this above it.

Ship the 4-hour row only. Project name: Hookpage.
Node.js and Astro. Pages, revisions, sections, and leads
live in D1. TanStack Start is the same screens and the
same rules, with server functions and a SQL database.

One page. Refuse a second page.
Slug saturday, fixed at create. Pattern is lowercase
letters, digits, and hyphens, up to 40 characters.
No control edits the slug later.

A revision is draft or published.
One open draft. Publish marks that revision published,
sets published_revision_id, and clones a new draft,
in one transaction.
A failed check rolls back. The live pointer stays.
Editor saves write the draft only.

Section types, one of each, in order: hero, proof, faq, form.
Plain text fields. Escape on render.
Hero headline required, max 120.
Form has name, email, a thank-you line max 160,
and hidden pairs the owner defined.
Refuse company_website as a hidden key.

Sample headline: Ship the small version.
Sample proof line: One offer on the page.
FAQ question: Do I need an account?
FAQ answer: The page is the signup.
Thank-you: You're on the Saturday list.
Hidden source = saturday.

No visual canvas, no second page, no pixel,
no variant weights, no visitor table.

When the plan lists routes and tables, stop.

What to check after the shell

Create is not offered a second time. A posted second page is refused. The slug field is absent on the editor. Save a headline. Reload the editor and the headline is there. Clear the headline and confirm Publish is disabled and names the headline. Put <script>alert(1)</script> in the proof line, save, and read the preview source. The tags are escaped.

Prompt 2. A public URL that reads one revision

Add the public page on Hookpage.

GET /saturday renders published_revision_id only.
Before the first publish, and for an unknown slug,
return one not-found page.
That body must not include draft headline text.
If the database read fails, return 503.
Do not return a 200 with an empty offer.

Preview exists only inside the owner editor.
There is no public preview route.

After publish, editing the draft must leave
the public HTML on the previous headline
until the next successful publish.

Done when the draft headline DRAFT-ONLY-HEADLINE
is visible in the editor and absent from GET /saturday.

What to check after publish

Open /saturday before publish. The status is the not-found page, and “Ship the small version” is absent. Publish. The hero is on the public page. Change the draft to DRAFT-ONLY-HEADLINE and reload the public URL. The live hero is unchanged. Stop the database and request the public URL. The status is 503.

Prompt 3. The form, the honeypot, and one row per email

Add the public form post on Hookpage.

Read the published revision. Ignore extra body keys.
company_website is the honeypot. If it has any value,
return the thank-you state and insert nothing.

Name is required, max 80. Email is required, max 200,
one @ and a dot in the domain. A bad email redisplays
the form with the name preserved and the error on
the email field.

Normalize email with trim and lowercase.
Unique key: page id plus normalized email.
A conflict returns the thank-you state, inserts nothing,
and does not call the webhook.

Rate limit: 5 posts per hour per truncated address.
Drop the last IPv4 octet before hashing.
For IPv6, keep the first 48 bits.
Store the hour bucket, the hash, and a count.
Over the limit, return 429 and "Try again in a bit".
Store no raw address.

Insert stores the published revision id, the name,
the normalized email, and the hidden values you defined.
Then show the thank-you line in place of the form.

Fixture lead: Lea, [email protected].
Done when [email protected] submits twice and the
inbox count is 1.

What to check after the form

Submit Lea. The inbox count is 1, and the revision id matches the live pointer. Submit the same address with different capitalization. The count stays 1. Submit a second person with the honeypot filled. The thank-you line shows, and the count stays 1. Submit a name with an empty email. The name remains and the email field shows the error. Submit six different valid emails in an hour from one address. The sixth response is 429, and the inbox has 5 leads.

Prompt 4. The inbox, the CSV, and a signed webhook

Add the inbox and the webhook on Hookpage.

The inbox lists name, email, time, revision id,
hidden source, and webhook status.
Search matches a substring of the email.
The owner can delete a lead, which frees that email.

CSV columns: email, name, created time, revision id,
source, webhook status.
If a cell starts with =, +, -, or @, prefix a single quote.

Webhook URL is optional and https only. Refuse http.
Empty URL stores not_configured.
After the lead insert commits, POST JSON:
leadId, email, name, revisionId, and the hidden pairs.
Header X-Hookpage-Signature is hex HMAC-SHA256 of the
raw body. Encrypt the secret. Show it once.
Timeout 8 seconds.
2xx marks delivered.
Timeout or non-2xx marks failed.
The visitor still sees the thank-you line.
Retry on a pending or failed lead sends the same leadId
and updates that row.
A duplicate submit does not call the webhook.

Done when a failed webhook leaves one lead, and retry
delivers that same leadId once.

What to check after the inbox

Search lea@ and find the row. Search [email protected] and get an empty list. Export CSV with a name =1+1 and confirm the cell starts with a quote. Point the webhook at a local HTTPS URL that returns 500. The lead exists, the status is failed, and the thank-you line was shown. Retry. One more POST, same leadId. Read the row and confirm the signing secret is absent. Delete the lead, submit Lea again, and confirm a new row.

Failures the agent ships anyway

The draft headline is on the public URL

You set DRAFT-ONLY-HEADLINE and the public HTML contains it. The editor saved into the published revision, or the GET reads the newest revision. Point the GET at published_revision_id and save only the open draft.

Two rows for one email

[email protected] and [email protected] are both in the inbox. Normalization happened in the browser, or the unique key used the raw string. Normalize on the server and put the unique key on the stored value.

Thank-you before the insert

The visitor sees the Saturday line, and the inbox is empty, after a database error. Show the thank-you line only after the commit, or after a unique conflict on an existing lead. A failed insert redisplays the form with an error.

The webhook runs twice for one lead

The retry inserted a second lead, or the duplicate path called the URL. The POST body should carry the same leadId, and the duplicate path should stop before the HTTP call.

A script runs from the hero

The headline was written into the page as HTML. Escape every section field. The check is the script string visible as text.

A second page “for the next offer”

The list grows a New page button. This row has one page row, and the create path refuses another. A second offer is a later product. The Carrd guide is the one-page personal site if that is the rent.

The 16-hour row, after the draft stays off the URL

Publish, change the draft headline, and reload /saturday. The live hero is the one you published. Then add the split.

A variant is another published revision of the same page, with a weight. The fixture weights are 50 and 50, and they have to sum to 100. The first visit sets a first-party cookie whose value is the revision id only, SameSite=Lax, path /saturday, for 30 days. The count on the list is the number of leads with that revision id. A duplicate email does not increment it. There is still no visitor table, and the cookie is not a profile.

A pixel is one script URL on the variant, rendered on the published page only. The 4-hour sections stay plain text. An empty pixel field loads nothing.

The scope post is the three-row habit. The prompt post is the line that keeps a canvas out of the first prompt.

Deploy, then submit one real address

Hookpage wants Astro and D1. Put it on a hostname you already own, offer.flaviocopes.com. The hosting post is the adapter and the domain. Take a backup of D1 before you delete a paid account. The leads are the list.

Log a refused publish with the field name. Log a form rejection with the page id and the reason: bad email, honeypot, or rate limit. Leave the email address out of that line. Log a webhook result with the lead id and the status code. Leave the signing secret and the webhook URL out of the log, because the URL can carry a token. When the public GET returns 503, log that the database read failed.

Then use it. Publish Saturday workshop. Submit an address you can open. Read one inbox row. Edit the draft and confirm the public hero stays. Send the CSV somewhere you can read.

If the next job is mailing that list, the Mailchimp guide is one consented list and a send. Mailchimp is $20–$350 a month, about 12 hours.

What to skip on purpose

Leave the visual builder, AI routing, popups, and a workspace per client. Leave variant weights and the pixel until the draft headline has survived a reload of the public URL.

If the rent is a personal page, build the Carrd guide. If the rent is a site and a collection, start from the Webflow post. If the rent is one offer and a list of emails, build this.

Start now

Go to Build your own Unbounce. Copy the Astro prompt, or TanStack Start if that is what you write.

Tell the agent the first estimate. A landing page, a lead form, and publish. Four hours.

Publish Saturday workshop. Change the draft. Read the old hero at /saturday.