Two Spouts
The Renaissance Marketer
Chapter 12 of 15

Web Dev

"Any sufficiently advanced technology is indistinguishable from magic." — Arthur C. Clarke

This is the chapter where you stop waiting on a dev. You build the site, ship the site, host the site — yourself, today. The bottleneck for most marketing isn't ideas — it's 'I need a dev to change this and they're slammed til next sprint.' Cut that and you move at a completely different speed. Speed almost always beats perfection. I'll be opinionated and hands-on here: less theory, more 'paste this, run that, done.'

The stack I actually use

Three pieces, and they all talk to each other for basically free:

  • Next.js — the front-end and API routes. Your pages, forms, logic.
  • Supabase — a real Postgres database + auth, with a dead-simple JS client.
  • Vercel — hosting. Push to GitHub, Vercel builds and deploys. No server to babysit.

Add Cloudflare R2 for video/images and a registrar for your domain — that's the whole kit. I run a dozen sites on this for close to nothing. (I am a cheapskate.)

Spin up a new app in one command — a live, hot-reloading site on your machine. Edit, save, the browser updates instantly:

npx create-next-app@latest my-site
cd my-site
npm run dev
# → http://localhost:3000

Why hard-coded beats drag-and-drop

WordPress, Wix, Squarespace, Shopify themes feel easy for the first hour. Then you want speed, or control, or one custom thing — and they fight you the whole way. They bloat, lock you in, and load like it's 2012. Google notices. Your conversion rate notices.

WordPress runs a huge chunk of the web AND a huge chunk of the web's security headaches. Plugins break, everything needs constant updating, and 'fast' is not a word anyone uses to describe it.

A hard-coded Next.js site is the opposite: blazing fast, total control, nothing to bloat, near-zero to host. The old objection was 'but you'd have to be a developer.' That's gone — Claude writes the code now. You steer it.

Web Dev — gif

Coding with Claude inside Antigravity

The part that changes everything for a non-developer: you don't write the code, you direct it in plain English.

Antigravity (the IDE) + Claude (the model). Antigravity is the editor; Claude Opus 4.8 is the brain inside it doing the coding — Anthropic's most capable model, genuinely good at building and refactoring real apps. Type what you want, watch it change the right files. I can't stress how freaking useful this is. Use it to edit your site by describing the change, build whole pages from a one-line brief, ask 'what does this file do?', and wire up Supabase/forms/redirects — without memorising the syntax.

Prompt it like a colleague, not a search engine — specific about the file and the outcome:

In app/page.tsx, add a 3-column pricing section under the hero.
Cards: Starter $0, Pro $29, Team $99. Highlight Pro.
Tailwind, match existing spacing, stack on mobile.
Cursor / v0 (alternatives). Cursor is another AI editor, same idea. v0 (by Vercel) turns a prompt or a screenshot into a working page — great for a first landing-page draft in minutes, then refine in Antigravity.

The AI writes the code, but YOU own it. Read the diff, load the page, click the buttons. 'It worked when the AI did it' is not a plan when it breaks at 11pm.

Supabase: your database in 10 minutes

A real Postgres database with a friendly dashboard, built-in auth, and a JS client so clean you'll barely notice you're 'doing backend.' The mental model: make tables (like spreadsheets), then read and write them from your Next.js code. Create a project at supabase.com, then make a table in the UI or with SQL:

create table leads (
  id uuid primary key default gen_random_uuid(),
  email text not null,
  name text,
  created_at timestamptz default now()
);

Grab your URL + anon key (Settings → API) into a git-ignored .env.local, install the client, and make a tiny helper:

# .env.local
NEXT_PUBLIC_SUPABASE_URL=https://yourproject.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJhbGci...

# then: npm install @supabase/ssr @supabase/supabase-js
// lib/supabase/client.ts
import { createBrowserClient } from "@supabase/ssr";

export function createClient() {
  return createBrowserClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
  );
}

Now reading data is basically one line — the 10 newest leads:

const { data, error } = await createClient()
  .from("leads")
  .select("email, name, created_at")
  .order("created_at", { ascending: false })
  .limit(10);

Supabase caps reads at 1,000 rows by default. If a query could ever return more, paginate with .range() + .order() — don't trust .select() to give you everything. This has caused wrong analytics totals more than once.

Auth is built in too — magic links, Google, GitHub, a few lines each:

await supabase.auth.signInWithOtp({ email });
const { data: { user } } = await supabase.auth.getUser();

Turn on Row Level Security (RLS) on every table before going live. Without it, your anon key can read/write everything; with it, you write rules like 'a user can only see their own rows.' Ask Claude to write the RLS policies — it's good at it.

GitHub + push-to-deploy

GitHub is version control — every change saved and reversible. The real magic is the loop: push to GitHub, Vercel auto-deploys. You deploy by typing 'git push.' First time, from your project folder:

git init
git add .
git commit -m "initial site"
gh repo create my-site --private --source=. --push

Connect the repo to Vercel once (import in the dashboard, or run 'vercel'). After that, every change is just 'git add . / git commit / git push' — Vercel sees the push, builds, and your live site updates in ~30 seconds. No FTP, no server restart, no drama.

Every push to a non-main branch gets its own preview URL — sweet for testing a redesign or sending a client a link before it's live. Merge into main → it's in production.

DNS & domains — get this right

The part people fear, but it's simple: a domain is just a pointer. You buy it at a registrar, then point it at your host.

A registrar (Porkbun, Cloudflare, Namecheap). Buy somewhere cheap and decent — Porkbun and Cloudflare are my picks (clean dashboards, near-cost pricing). Avoid registrars that hook you with $1 year one then renew at $40. Two record types do 95% of the work: an A record points a domain at an IP (use for the root domain, example.com), and a CNAME points a domain at another domain (use for subdomains — www, app, blog).

Add your domain in Vercel (Settings → Domains) and it tells you exactly what to set. The standard setup at your registrar:

# root domain → Vercel
Type: A      Name: @      Value: 76.76.21.21

# www subdomain → Vercel
Type: CNAME  Name: www    Value: cname.vercel-dns.com

Set what your own Vercel dashboard shows for your project — not what you remember. The exact IP and CNAME target can differ.

Then wait for propagation — usually minutes, occasionally a few hours. Check with 'dig www.example.com +short' or whatsmydns.net. Vercel issues the SSL cert (the https padlock) automatically once records resolve. Point the records, wait, done.

Cloudflare R2 — self-host your media

Big videos and images don't belong in your code repo, and you don't need a bloated plugin or pricey CDN.

Cloudflare R2 — object storage, a giant cloud folder. The killer feature is zero egress fees: you don't pay every time someone watches your video. Create a bucket (R2 → 'site-media'), enable Public access (R2.dev URL) or connect a subdomain like cdn.example.com, then upload — drag-drop, or rclone for bulk (handles big folders without timing out):
rclone copy ./videos r2:site-media/videos --transfers 6

<video src="https://cdn.example.com/videos/demo.mp4" controls />
<img src="https://cdn.example.com/img/hero.webp" alt="Product hero" />

Compress before you upload. A 40MB hero video kills your load time and your conversions. Convert to .webp for images and .mp4/H.264 for video first — speed IS a conversion factor.

Landing pages that convert

A landing page has ONE job: get the visitor to do the one thing. The structure that works almost everywhere:

  1. A clear headline stating the benefit — what do I get, why care?
  2. A subhead that backs it up in one line.
  3. One obvious call-to-action button, above the fold.
  4. Proof — testimonials, logos, results, numbers.
  5. Benefits (what they get), not just features (what it is).
  6. Repeat the CTA at the bottom. People decide at different points.

Cut, cut, cut. Every extra link or paragraph is a chance to wander off. A confused visitor doesn't buy. One page, one goal, one action — and fast, because a money page that loads slow loses people before they read a word. That's why hard-coded wins here.

Front-end techniques worth stealing

A few small, high-leverage things that make a site feel custom. Tell Claude to add them — but know what they are so you ask for the right thing.

Remember the user's last-used filters

Store a user's defaults in localStorage and the page comes back the way they left it — same sort, same filter. Tiny touch, feels premium.

localStorage.setItem("sortBy", "price-low");          // save on change
const sortBy = localStorage.getItem("sortBy") ?? "newest"; // restore

Cookie-based offers

Show a promo bar once, then remember you've shown it so it doesn't nag every visit. A cookie persists across sessions (and can be read server-side if you need that).

function setCookie(name, value, days) {
  const expires = new Date(Date.now() + days * 864e5).toUTCString();
  document.cookie = `${name}=${value}; expires=${expires}; path=/`;
}

if (!document.cookie.includes("offer_seen=1")) {
  showOfferBar();
  setCookie("offer_seen", "1", 30); // don't show again for 30 days
}

A real form → API route → Supabase

The whole funnel in one move: a form posts to a Next.js API route, which inserts the lead into Supabase. No third-party form tool, no monthly fee. The route runs server-side, so it can use a service key safely:

// app/api/lead/route.ts
import { createClient } from "@supabase/supabase-js";

const supabase = createClient(
  process.env.NEXT_PUBLIC_SUPABASE_URL!,
  process.env.SUPABASE_SERVICE_ROLE_KEY! // server-only, never ship to browser
);

export async function POST(req: Request) {
  const { email, name } = await req.json();
  const { error } = await supabase.from("leads").insert({ email, name });
  return Response.json({ ok: !error }, { status: error ? 500 : 200 });
}
// the form that hits it
async function onSubmit(e) {
  e.preventDefault();
  const form = new FormData(e.target);
  await fetch("/api/lead", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ email: form.get("email"), name: form.get("name") }),
  });
}

The SERVICE_ROLE_KEY bypasses RLS — it must ONLY live in a server-side file and your env vars. Never in a browser component. Anon key for the browser, service key for the server.

Migrating a site without nuking your SEO

Rebuilding an existing site on this stack? Brilliant — but moving a site that already ranks is where people throw away months of SEO. Here's how not to:

  1. Keep the URL structure. If a page was /pricing, keep it /pricing — same slugs = rankings carry over. Don't 'tidy up' URLs for fun.
  2. For any URL that DID change, set a 301 (permanent) redirect — it passes the SEO juice to the new page. Implement them in next.config.js (below) or middleware.
  3. Re-submit your sitemap.xml in Google Search Console so Google recrawls fast, then watch it for 2-4 weeks and fix any 404s with more redirects.
// next.config.js
module.exports = {
  async redirects() {
    return [
      { source: "/old-pricing", destination: "/pricing", permanent: true },
      { source: "/blog/:slug", destination: "/articles/:slug", permanent: true },
    ];
  },
};

permanent: true = a 301 — the one that preserves rankings. A 302 (permanent: false) tells Google 'this is temporary, keep the old one' — wrong for a migration. Get this right or you bleed traffic.

SEO & speed, baked in

On a modern stack most of this comes for free, but you still have to set it. The non-negotiables on every page:

  • A real title tag + meta description — what shows in Google. In Next.js, export a metadata object per page.
  • Exactly one H1, then a sensible heading structure under it.
  • Alt text on every image — for accessibility and Google.
  • Compressed images, no bloat — speed is an SEO factor AND a conversion factor.
  • Mobile-first — most traffic is on a phone. Check it there FIRST.
// app/pricing/page.tsx
export const metadata = {
  title: "Pricing — Simple plans for every team",
  description: "Start free. Scale when you're ready. No hidden fees.",
};

Run any page through Google's PageSpeed Insights — free, fast, tells you exactly what's slow and how to fix it.

That's the whole pitch. A Renaissance Marketer who can build, ship and host their own site doesn't wait on anyone. You think it, you push it, it's live the same day. That's leverage you can't unsee.

APIs — how everything talks to everything

Once you can call an API, half the SaaS tools you pay for become optional. An API is just a URL you send a request to and get data back — usually JSON. Describe what you want to Claude and it writes the call; you just need to understand the shape so you can tell when it's wrong.

const res = await fetch("https://api.example.com/v1/stats", {
  headers: { Authorization: "Bearer " + process.env.API_KEY },
});
const data = await res.json();

Four things you deal with every time: the endpoint + method (GET to read, POST to send), auth (an API key in an Authorization header), the request body (the JSON you send up on a POST), and the response (the JSON you parse and use). I use this to pull ad spend, sync leads into Supabase, post to Slack, and replace whole tools — like the LinkedIn scheduler from earlier.

Find the tool's 'REST API' docs page, paste it into Claude, and say 'write me a script that does X.' It'll handle headers, pagination and retries. Just check the rate limit and page size first — most APIs cap requests per minute and return only ~100–1000 rows per page, so read those two numbers before you write a loop or you'll get half your data and a ban.

API keys & secrets — never commit them

Every API key, password and token is a secret — leak one and someone runs up your bill or walks off with your data. The rule: secrets live in environment variables, never in your code, never in git. Locally that's a git-ignored .env.local you read with process.env:

# .env.local  (git-ignored)
ANTHROPIC_API_KEY=sk-ant-...
SUPABASE_SERVICE_KEY=eyJ...

// read in code:
const key = process.env.ANTHROPIC_API_KEY;

In Next.js, anything prefixed NEXT_PUBLIC_ gets shipped to the browser — never put a real secret there. Server-only keys have no prefix and stay on the server.

In production you don't upload .env.local — you add the same keys in Vercel → Settings → Environment Variables. Same names, set once, done.

If you ever paste a key into a commit, screenshot or chat — rotate it immediately. Assume it's burned the second it leaves your machine.

Sweet, you’ve completed this section! 🥳 Move on to the next section on technical marketing.