Playbook
Playbook/Closing & operations

Deployment & Tech Setup Guide

097 min read1,436 words

How to take both sites from "works on my machine" to "live on the internet under your own domain."


What you're deploying

Two separate Next.js 15 apps, each configured for static export (output: 'export' in next.config.ts):

  1. apps/mixed-greens — the demo client site
  2. apps/agency — your sales site

Both produce a static out/ directory that any static host can serve. No server, no database, no API needed. This means free hosting forever and zero maintenance surprises.


Why:

  • Free tier covers unlimited sites and hundreds of GB of bandwidth
  • Global CDN — sub-200ms loads worldwide
  • Automatic HTTPS / SSL
  • Git-connected (push to deploy)
  • Supports custom domains for free

Alternatives:

  • Vercel — also free, also great. Use this if you prefer their UI or want to upgrade to ISR later.
  • Netlify — fine. Slightly less generous free tier.
  • GitHub Pages — free but slower, fewer features. Not recommended.

The instructions below are for Cloudflare Pages. The Vercel flow is similar — push to GitHub, import, deploy.


Step-by-step: deploy mixed-greens to Cloudflare Pages

1. Push to GitHub

If you haven't yet:

# from the website-local-business root
git init
git add .
git commit -m "Initial: web design outreach kit"
git branch -M main
gh repo create web-design-outreach --private --source=. --push

(If gh CLI isn't installed, do it via the GitHub website — create an empty repo, follow the "push existing repository" instructions.)

2. Connect Cloudflare Pages

  1. Go to https://dash.cloudflare.com/Workers & PagesCreate applicationPagesConnect to Git.
  2. Select your web-design-outreach repo.
  3. Project name: mixed-greens (this becomes mixed-greens.pages.dev).
  4. Production branch: main.

3. Build settings (this is the part that trips people up)

Because we have a monorepo (apps/mixed-greens, apps/agency), set:

FieldValue
Framework presetNext.js (Static HTML Export)
Build commandcd apps/mixed-greens && npm install --legacy-peer-deps && npm run build
Build output directoryapps/mixed-greens/out
Root directory (advanced)leave blank
Environment variableNODE_VERSION = 20 (or 22)

Click Save and Deploy. First build takes 2–4 minutes.

4. Repeat for the agency site

Create a second Cloudflare Pages project:

FieldValue
Project nameagency (becomes agency.pages.dev)
Build commandcd apps/agency && npm install --legacy-peer-deps && npm run build
Build output directoryapps/agency/out
NODE_VERSION20 or 22

Both sites are now live at *.pages.dev URLs. They will redeploy automatically every time you push to main.


Custom domains

Buy the domains

  • Use Cloudflare Registrar (at-cost, no markup, ~$10/year for a .com).
  • Or your existing registrar (GoDaddy, Namecheap) if you've got a deal.

Point them at Cloudflare Pages

In Cloudflare Pages → your project → Custom domainsSet up a custom domain → enter your domain. CF walks you through the DNS records.

If your domain is already in Cloudflare, it's literally one click. If it's at GoDaddy/Namecheap, you'll add CNAME records pointing to <project>.pages.dev.

Suggested domains:

  • For the demo: register the actual restaurant's preferred domain (e.g., mixedgreensut.com) only after they sign, and register it under their account or transfer to them. Don't squat domains.
  • For the agency: pick something short and memorable. Examples: pinecone.web, firstpage.studio, groundedweb.co. Avoid keyword-stuffed ones like bestlocalwebsites.com — they look spammy and hurt outreach trust.

Environment variables (per site)

Neither site needs env vars by default. If you add features that do:

Cloudflare Pages → Project → Settings → Environment variables

Common future vars:

  • NEXT_PUBLIC_GA_ID — Google Analytics
  • NEXT_PUBLIC_FORMSPREE_ID — Form handler
  • RESEND_API_KEY — Email sending (server-side)
  • STRIPE_PRICE_ID_GROWTH — Subscription product ID

Variables prefixed with NEXT_PUBLIC_ are exposed to the browser. Anything sensitive must NOT have that prefix.


Local development

Run a single site

cd apps/mixed-greens
npm install --legacy-peer-deps   # first time only
npm run dev
# Open http://localhost:3000
cd apps/agency
npm install --legacy-peer-deps   # first time only
npm run dev
# Open http://localhost:3001 (different port, won't conflict)

Run both at the same time

Open two terminals, run npm run dev in each app folder. They run on 3000 and 3001 simultaneously.

Build & preview production

cd apps/mixed-greens
npm run build
# Inspect what's in: apps/mixed-greens/out
# To preview locally:
npx serve out -p 4000
# Open http://localhost:4000

Adding a new client site

The fast workflow once you have a few clients:

  1. Duplicate apps/mixed-greens/apps/<client-slug>/
  2. Update package.json name field
  3. Update lib/restaurant.ts (or whatever data file applies) with their info
  4. Replace placeholder images
  5. Update app/layout.tsx metadata
  6. Add a new Cloudflare Pages project pointing to apps/<client-slug>
  7. Connect their custom domain
  8. Done — usually < 4 hours of work for a Starter, < 8 for Growth

Eventually, factor out the shared components into a local packages/ui and import them. Don't do this on day one — duplicating two or three times is faster than abstracting prematurely.


Forms (when you need them)

Both sites currently use mailto: for contact. This works but:

  • Doesn't capture into a CRM
  • Doesn't filter spam
  • Loses people without a desktop email client

Upgrade options (in order of effort):

Easiest: Formspree / Web3Forms / Tally

  • Sign up free
  • They give you a form action URL like https://formspree.io/f/abc123
  • Replace your <form> action with that URL
  • Submissions land in your inbox + their dashboard
  • ~$10/mo paid tier removes branding

Middle: Resend + a Cloudflare Worker

  • Resend gives you 3,000 free email sends/month
  • Write a Cloudflare Worker that accepts POST and sends via Resend
  • More control, no third-party form service
  • Documented at: https://resend.com/docs

Heavy: Pages Functions / API routes (and lose static export)

  • Switch to output: 'standalone' in next.config
  • Add API routes
  • Now you need a runtime, not just static
  • Don't do this unless you have a real reason

Analytics (optional)

The free + privacy-respecting move:

  • Plausible ($9/mo) or Fathom ($14/mo) — both load <1KB scripts, no cookie banners needed
  • Add the snippet to app/layout.tsx

If you want to be on the Google ecosystem:

  • Google Analytics 4 + Google Tag Manager — free, but adds 50KB+ and requires a cookie banner in EU/CA jurisdictions

For client sites, GA4 is what most owners expect because it integrates with their Google Business Profile data. For your own agency site, use Plausible — looks more sophisticated.


Monitoring

Free options:

  • UptimeRobot — free 50 monitors, 5-minute checks, email/SMS alerts. https://uptimerobot.com
  • BetterStack (Better Uptime free tier) — pretty status pages too
  • Cloudflare Analytics — built into Pages dashboards, gives you page views and core web vitals

Set up uptime monitors on every client domain. When something breaks at 9pm, you want to know before the client does. Customer trust is built in those moments.


Things that will go wrong (and how to fix)

"Build failed: image hostname not configured"

Add the hostname to next.config.ts:

images: {
  remotePatterns: [
    { protocol: "https", hostname: "your-new-image-host.com" },
  ],
}

"Module not found" in deploy but works locally

Usually means a case mismatch (Mac is case-insensitive, Cloudflare's Linux build is case-sensitive). Check your import paths exactly match the file names.

Custom domain shows "Not Secure"

SSL provisioning takes a few minutes after adding the domain. If it's still failing after 30 minutes, check your DNS records — there's probably an existing A record fighting the CNAME.

Site loads but images don't show

You set output: "export" but forgot images: { unoptimized: true }. Add it.

npm install fails with peer dependency errors

React 19 + older Radix versions sometimes conflict. Use npm install --legacy-peer-deps. (Already in our build commands above.)


What "done" looks like

You're production-ready when:

  • Both sites are pushed to GitHub
  • Both Cloudflare Pages projects are deployed and green
  • Both sites are reachable on a .pages.dev URL with HTTPS
  • At minimum the agency site is on a custom domain
  • Uptime monitoring is on (UptimeRobot is free)
  • You can do git commit && git push from your machine and see the change live in <5 minutes

Once that's true, you're ready to start dialing.