Build a Carrd alternative

Build a Carrd alternative as stacked sections, a contact form, and a publish button. $2–$4 a month is still rent if you already write HTML yourself.

  • getting-started
  • vibe-coding
  • tonight

You can build a Carrd alternative as one page you host. Carrd is one page. The catalog line is “It’s one page. You can build one page.”

You pay $2–$4 a month for it. That is $24–$48 a year. The basic replacement is 2 hours: stacked sections, a contact form, and a publish button.

I would ship this if I still rented a landing page and I already write HTML. It is marked Tonight. The verdict on the card is “One page, zero rent”.

This morning I wrote about how Calendly works. That was a teardown. This one is a regular backlog topic, a page builder whose useful v1 is the page itself.

What you need to build a Carrd alternative

The Carrd page splits the work into three rows.

The first row is the one you want tonight. Stacked sections, a contact form, publish. A hero, a block of text, a few links, a form, a URL.

The second row is 8 hours. Themes, live preview, a gallery, draft states. Those can wait until the live URL is already in a bio.

The third row is a month and up. A visual editor, templates, multi-tenant hosting, custom domains as a product. The table labels that row as the part you should skip.

If you tell an agent to “clone Carrd”, it aims at the third row. Use the prompt on the page. It is written for a useful v1, and you still have to keep it on the first row.

Here is the shape of the data you are building. Nothing else has to exist at 2 hours.

const site = {
  slug: 'flavio',
  theme: {
    bg: '#f4f1e8',
    ink: '#171713',
    accent: '#ff755d',
    font: 'Georgia',
  },
  sections: [
    { id: 'sec-hero', type: 'hero', heading: 'I write tiny software.', sub: 'Tutorials at flaviocopes.com' },
    { id: 'sec-about', type: 'text', body: 'I write about JavaScript and shipping small tools you own.' },
    {
      id: 'sec-links',
      type: 'links',
      items: [
        { label: 'Newsletter', url: 'https://flaviocopes.com' },
        { label: 'YouTube', url: 'https://youtube.com/@flaviocopes' },
      ],
    },
    { id: 'sec-contact', type: 'contact', heading: 'Say hello' },
  ],
}

A visitor hits a stable public URL and sees those sections in order. You change a heading, publish again, and the same address still works.

STACKED SECTIONS Hero About Contact CONTACT FORM Store the FormMessage Then try to send email PUBLISH SNAPSHOT Copy draft to a version Flip the live pointer
The 2-hour v1 is stacked sections, a stored message, and a publish pointer.

Who a Carrd alternative is for

The audience line on the page is specific. One person who needs exactly one landing page.

The goal is to assemble a fast single page from sections, collect form messages, and publish it in an evening. Tonight the done state is a live URL plus one test message in a dashboard.

If you sell landing pages to clients, or you need a fleet of sites with a template marketplace, keep paying. That is the honest rebuild. The site puts it at a month on purpose.

A page of links is a smaller job. That is Linktree, $9–$24 a month, about 30 minutes. The Linktree walkthrough is that post. Build the Carrd version if you need sections and a form, not five buttons.

How a Carrd page is put together

Carrd is a one-page site builder. You stack blocks, pick colors, hit publish. I am describing the catalog’s model here, not reverse-engineering their editor.

The prompt names three end-to-end journeys. Tonight you need the first one.

Pick a theme and add hero, about, and contact. Edit the copy. Preview at phone width before you publish.

The screens the prompt asks for are an editor, a publish panel, a messages inbox, and the public page. For 2 hours you can live with the editor and the public page. Messages can be a table. Theme can be CSS variables.

The editor is an ordered rail of sections next to a preview. You can add a section, remove one, or reorder them. Each section has a type from a fixed set: hero, text, image, gallery, links, contact. The public page renders that list and nothing else.

The data model on the page is Site, Section, Theme, FormMessage. Tonight you need Site and Section. Theme is four CSS variables. FormMessage is a row in D1.

The capability line is Astro, Alpine, and D1. This site is an Astro app on Cloudflare, so that is the path I default to. Use Rails if that is your weekday language. The scope matters more than the framework.

Copy the prompt. Before you run it, add one extra line at the top: ship the 2-hour row only. Stacked sections, contact form, publish. Stop.

Read the non-goals before you copy anything

Open the guide. Read the audience and the non-goals before you touch the prompt.

The non-goal is the useful sentence: do not build multi-page sites, a template marketplace, e-commerce, or hosting for other people’s pages.

The guardrail sits next to it. Sanitize all section content server-side and rate-limit the contact form with a honeypot field. Together they keep you on one page you own.

Sanitize what gets pasted into a section

Agents trust innerHTML. People paste from Google Docs. The paste arrives with inline styles, nested spans, and the occasional javascript: link.

The mini-prompt on the page is “The paste that arrives wearing Word’s clothes”. Tonight, intercept paste and keep a tiny allowlist: paragraphs, bold, italic, links, lists. The client pass is UX. The server pass on save is the security boundary.

function hrefOrDrop(raw) {
  try {
    const url = new URL(raw)
    if (url.protocol !== 'http:' && url.protocol !== 'https:') return null
    return url.href
  } catch {
    return null
  }
}

hrefOrDrop('javascript:alert(1)')
//null

hrefOrDrop('https://flaviocopes.com')
//'https://flaviocopes.com/'

Strip every attribute except a validated href. Collapse the span soup into those tags. Fall back to plain text when the HTML is a mess.

The scenario on the page is explicit: given script tags pasted into a text section, they are stripped before rendering and the public page stays safe.

Store the contact message before you send email

The form is why this is not just Linktree. A visitor types a name, an email, a message. You want that text even if the mail provider is down.

The mini-prompt is “The message that outlives the email outage”. Validate, store, then attempt the email. In that order.

async function submitContact(db, mail, { name, email, body, honeypot }) {
  if (honeypot) {
    return { ok: true }
  }

  const message = await db.insertFormMessage({
    name,
    email,
    body: stripMarkup(body),
    notification: 'pending',
  })

  try {
    await mail.send({
      to: '[email protected]',
      subject: `Onepager: ${name}`,
      text: message.body,
    })
    await db.markNotified(message.id)
  } catch {
    await db.queueRetry(message.id)
  }

  return { ok: true, id: message.id }
}

The dashboard reads from storage. The email is a pointer to a row that already exists. A filled honeypot returns the same success screen and writes nothing. Rate-limit by IP. The catalog already says that.

A full form product is a different job. That is Typeform, $29–$99 a month, about 2 hours for a one-question flow plus a CSV. Build this contact block if you wanted a “say hello” section, not a survey.

Publish is a snapshot

The second journey on the page is the one people skip, then regret.

You edit a published heading. The public URL stays the same. You see the unpublished change in preview. You press publish on purpose.

If save and publish are the same button, a half-written about section goes live while you are still typing. For a page that sits in a bio, that gets old.

The prompt already keeps draft and published snapshots separate. Editing cannot alter the live page by accident. The mini-prompt about unpublished edits is the evening-length version: copy the draft into an immutable snapshot, point the live page at it in one step, purge the cache, keep a short history for rollback.

Skip that machinery if the 2 hours are real. A single published page still beats a $2–$4 subscription. Add the snapshot the first time you almost shipped a half-edited hero.

async function publish(db, siteId) {
  const draft = await db.getDraft(siteId)
  const version = await db.insertPublished({
    siteId,
    snapshot: draft,
    publishedAt: new Date().toISOString(),
  })
  await db.setLivePointer(siteId, version.id)
  await db.purgeCache(`/${draft.slug}`)
  return version
}

A visitor mid-publish gets the old complete page or the new complete page, never a mix of sections from both.

2-HOUR V1 Stacked sections Contact form in D1 One publish to a URL SKIP THIS HALF Visual editor Template gallery Hosting other people's pages
Ship the 2-hour page. Leave the builder platform on the skip row.

What not to build in a Carrd clone

Leave out multi-page navigation, a template marketplace, e-commerce, and anything that looks like hosting for other people.

The non-goal on the page already names those. You are replacing the one page you currently rent.

If the agent starts a theme gallery, stop it. Paste the non-goal again. The month-long row exists so you have a sentence to point at.

Custom domains as a product are the same trap. Point a subdomain you already own at this page. hello.flaviocopes.com or a path next to a site you already deploy. Do not invent a domain checkout.

The 8-hour row, after you use the page

Use the page as the real landing page for a week. Then look at the second estimate.

Fonts, colors, and spacing, still one theme at a time. A live preview that reads the same draft the rail edits. A gallery section. Draft versus published, if you skipped it at 2 hours.

The section-rail mini-prompt is “A section rail you can shuffle with live preview”. Stable section ids plus position keys, so a drag writes one key rather than renumbering the list. Keyboard users get move-up and move-down. Phone and desktop width toggles reflow the same draft.

That is the solid v1. The catalog says 8 hours. Stop there.

The month-long row is the visual editor, the templates, and hosting other people’s pages, which you can skip.

A bio page with five URLs is Linktree. $9–$24 a month, 30 minutes. I would build that first if the rent is a list.

A Carrd alternative is sections plus a form. If you have an about paragraph and a “say hello” box, this is the build.

Compressing the hero image is TinyPNG Pro, $25–$39 a month, about 60 minutes. The TinyPNG walkthrough stays in the tab. Drop the PNG, download the smaller file, put it in the hero.

Host it, send a real URL, cancel

The product pages put hosting this way: a focused replacement costs your build time plus close-to-zero hosting.

This one can live on a subdomain you already own. There is a D1 table for messages, so take a backup before you delete the paid account.

Use it for a week as the real page, not as a demo. If you miss a gallery or a draft snapshot, add them from the 8-hour row. You will not miss a template marketplace.

Then cancel Carrd.

If you want another Tonight card after this one, the audit is still the index. Linktree is about 30 minutes if you still need the list.

Start now

Go to Build your own Carrd. Copy the AHA prompt, or the stack you already write.

Tell the agent the first estimate only: stacked sections, contact form, publish. Two hours.

Publish the URL. Point one real bio at it tonight.