DEV Community

Take2 Land Guides
Take2 Land Guides

Posted on Edited on

Building a done-for-you website service as one React SPA: site, order flow, client portal and admin

Written by the Take2 team. Disclosure: I'm a Take2 affiliate (Take2 Innovations Digital, a small web design shop).

Take2 Innovations Digital is a very small web design shop in Fort Lauderdale, Florida. We build websites for local businesses that don't have one yet, like diners, plumbers, salons, churches and lawn care crews. One person builds and one handles sales.

Before building anything for clients, we had to build the thing that sells and runs the service. That's this app: https://take2-innovations-digital.rork.app. This post covers what's in it, how it's put together, and what we'd do differently.

What we built

One app does four jobs:

  1. Marketing site. Home, Packages, Add-ons, Portfolio, Reviews, About, Contact and Legal pages.
  2. Order funnel. A multi-step order that ends in a signed agreement and a deposit.
  3. Client portal. After ordering, clients log in with a magic link to send content, track progress, request changes and pay the balance.
  4. Staff/admin area. Quotes, jobs, pricing overrides and stats.

The public pitch is simple: pick a package, tell us about your business, we build it, you approve it, and we keep it running every month. Setup is paid in two halves, half up front and half once the client is happy.

Stack

Frontend

  • Vite + React + TypeScript
  • Tailwind CSS with shadcn/ui (Radix primitives)
  • react-router-dom for routing
  • @tanstack/react-query for server state
  • react-hook-form + zod for the long order and quote forms
  • recharts for admin charts, jspdf for generating documents

Backend

  • A Cloudflare Worker with a Durable Object backed by SQLite, split into modules: billing, finance, jobs, portal, staff, store, stats and backup. Shared libs handle Stripe, email, push and crypto.
  • Stripe PaymentIntents for deposits and final payments, plus a /webhooks/stripe endpoint.
  • A small Supabase project that stores only analytics events (a site_events table and an export function).

The app is built and hosted on Rork, so the Worker runs on Rork's Cloudflare hosting.

API shape

Splitting routes by who's calling kept auth simple:

/public/{config, order, quote-request, contact, review, help}
/auth/{login, logout, magic, magic/verify}
/portal/*      # signed-in clients
/staff/*       # signed-in staff
/webhooks/stripe
/analytics/event
/stats/*
Enter fullscreen mode Exit fullscreen mode

/public/config is the important one. It returns prices, feature flags and payment readiness, and the frontend renders from it. Defaults live in a pricing.ts file and an admin can override them without a deploy.

The order funnel

The order flow is a step machine:

Package → Your pages → About you → Your business → Web address → How we work → Sign & pay → Order placed → Client portal

Things that worked:

  • One zod schema per step. Each step validates on its own, so the user never gets a wall of errors at the end.
  • The agreement and deposit happen on one step. The client signs the terms and pays 50% together, which gives a clean record.
  • A parallel quote path. Not everyone wants to buy from a price list. Quote request → staff sends a quote → the client gets a quote link → quote checkout. It uses the same payment code with a different entry point.
  • Magic-link portal auth. Small business owners don't want another password. Email links fit the audience better.

Lessons learned

1. Run a funnel event on every step from day one

We log an analytics event per order step, which shows exactly where people stop. Our early numbers made it plain that most visitors look at packages and never reach the second step. Without per-step events we would only have known "nobody ordered."

2. A client-side SPA on a shared subdomain is close to invisible to search

The whole site is a single HTML shell rendered on the client. Every route has the same title and description, there's no sitemap.xml, no og:image and no LocalBusiness JSON-LD, and it sits on a platform subdomain. For a business that sells "get found on Google," that's the first thing we're fixing:

  • Prerender the marketing routes at build time
  • Give each route its own <title>, meta description and Open Graph tags
  • Add sitemap.xml and LocalBusiness structured data
  • Move to our own domain

3. Show samples before you have clients

A web shop with an empty portfolio is a hard sell. We put up sample designs for the kinds of businesses we serve, including a corner diner, a community church, lawn and landscaping, a local plumber, a hair salon, a barber shop, a roofer, auto repair, heating and air, a tire shop and a dry cleaner. Each one is labeled as a sample design. Labeling them keeps us honest, and visitors can still picture their own site.

4. Know your platform dependency

Building on a hosted platform got us from idea to a working site, portal and payments quickly. The trade-off is that the backend runs on someone else's hosting and is harder to move. Keep the data layer and Stripe logic in plain modules so you can migrate if you ever need to.

What's next

  • Prerendering plus per-page meta and a sitemap
  • A custom domain
  • Real client reviews to replace the empty Reviews page

If you're building a productized service, whether websites, design or bookkeeping, the pattern of a config-driven price list, a step-validated order flow, a magic-link portal and per-step analytics carries over well.

Sample work: https://take2-innovations-digital.rork.app/portfolio


Disclosure: written by the Take2 Innovations Digital team; we build websites for small businesses, and free quotes are available at https://take2-innovations-digital.rork.app.

Top comments (0)