Everything *[NYC] 2026: see what we announced.

Trades Starter Lite — Contractor & Trades Business Site

A Next.js 16 + Sanity v5 starter for trades and contractor businesses — electricians, plumbers, HVAC, roofers, landscapers. Services, projects and careers pages, working contact and job-application forms, and a Studio built around one Business Info document. Mobile-first. MIT licensed.

By Robert Riddle

Loading
Preview

README

Trades Starter Lite

A free Next.js + Sanity starter for trades and contractor businesses — electricians, plumbers, HVAC, roofers, landscapers, painters. Services, projects, careers, and a working contact form, content-managed in Sanity and deployable to Vercel.

Live demo — the template running as shipped, with the demo content seeded. Try the forms: submissions are saved to Sanity, but the demo runs without a Resend key, so no notification email is sent. Add your own key and they send.

This is the lite foundation of the IronLine Trades Starter Kit. It ships seeded with a fictional electrician (Brightwork Electric) so the site looks real the moment you run it.

What's included

  • Sanity-managed contentbusinessInfo (singleton), service, project, jobPosting, plus contactSubmission / applicationSubmission capture.
  • 5 pages — Home (hero + services + recent projects + CTA), Services, Projects, Careers (with apply form), Contact.
  • Working forms — contact + job application, validated and honeypot-protected, saved to Sanity every time, and emailed to the owner via Resend once you add a key. The save never depends on the email, so no enquiry is lost while email is still being wired up.
  • Standalone Sanity Studio — singleton-enforced business info, deployable to *.sanity.studio.
  • Mobile-first — sticky tap-to-call bar, Oswald/Montserrat type, brand color tokens.

Tech stack

  • Next.js 16 (App Router, Turbopack) · React 19
  • Sanity v5 (standalone Studio) · next-sanity 12 · @sanity/image-url
  • Tailwind CSS v4 · Base UI + shadcn-style components · lucide-react
  • react-hook-form + Zod · Resend (email)
  • TypeScript, ESLint · monorepo (frontend + studio) via npm workspaces

Getting Started

You need Node 20+ and a free Sanity account.

  1. Create a Sanity project at sanity.io/manage. Note the Project ID and the dataset name (default production).
  2. Generate an API token in your project → API → Tokens → add a token with the Editor role (used to save form submissions).
  3. Install dependencies (from the repo root — installs both workspaces):
    npm install
  4. Configure environment variables. Copy the examples and fill them in — use the same project ID in both files:
    cp studio/.env.example studio/.env
    cp frontend/.env.local.example frontend/.env.local
    • studio/.envSANITY_STUDIO_PROJECT_ID, SANITY_STUDIO_DATASET
    • frontend/.env.localNEXT_PUBLIC_SANITY_PROJECT_ID, NEXT_PUBLIC_SANITY_DATASET, NEXT_PUBLIC_SITE_URL, SANITY_API_WRITE_TOKEN (and optionally RESEND_API_KEY + OWNER_EMAIL)
  5. Add a CORS origin so the Studio and site can read your project:
    npx sanity cors add http://localhost:3333 --credentials
    npx sanity cors add http://localhost:3000 --credentials
  6. Run it (Studio on :3333, site on :3000):
    npm run dev
    Or run them separately with npm run dev:studio and npm run dev:frontend.
  7. (Optional) Seed demo content so the site isn't empty — log in once, then seed:
    npx sanity login
    npm run seed

    Seed once, onto an empty dataset. The seeder writes fixed document IDs, so a re-run replaces whatever now lives at them — including the Business Info singleton holding your name, phone, address and license. It refuses to run once the dataset holds content it didn't create, saved form submissions, or a business name that is no longer the demo one. Override with SEED_FORCE=1 only when you actually want the demo content back.

Visit http://localhost:3000 for the site and http://localhost:3333 for the Studio.

Environment variables

frontend/.env.local

VariableRequiredPurpose
NEXT_PUBLIC_SANITY_PROJECT_IDyesYour Sanity project ID (public)
NEXT_PUBLIC_SANITY_DATASETyesDataset name, e.g. production (public)
NEXT_PUBLIC_SITE_URLyesSite URL — http://localhost:3000 locally
SANITY_API_WRITE_TOKENyesEditor token — saves contact/application submissions (server only)
RESEND_API_KEYoptionalResend API key for owner email notifications
OWNER_EMAILoptionalWhere form notifications are sent

studio/.env

VariableRequiredPurpose
SANITY_STUDIO_PROJECT_IDyesSame project ID as the frontend
SANITY_STUDIO_DATASETyesSame dataset as the frontend

Email note: frontend/lib/email.ts ships with Resend's sandbox sender (onboarding@resend.dev). It works for testing but lands in spam. Before production, verify your domain in Resend and change the from address. Until RESEND_API_KEY + OWNER_EMAIL are set, forms still save to Sanity — they just skip the email.

Project structure

trades-starter-lite/
├── frontend/                 # Next.js 16 public site
│   ├── app/                  # home, services, projects, careers, contact, api/{contact,apply}
│   ├── components/{ui,site}/ # primitives + header/footer/hero/cards/forms
│   ├── lib/                  # utils, format, email, zod schemas, types
│   └── sanity/lib/           # client (read), client-write, image, queries, fetch
└── studio/                   # standalone Sanity Studio
    ├── schemaTypes/          # businessInfo, service, project, jobPosting, *Submission
    └── scripts/seed.ts       # demo content seeder

Scripts

CommandWhat it does
npm run devRun Studio (:3333) + frontend (:3000) together
npm run dev:frontend / npm run dev:studioRun one at a time
npm run buildProduction build of the frontend
npm run build:studioBuild the Studio
npm run deploy:studioDeploy the Studio to *.sanity.studio
npm run seedSeed demo content onto an empty dataset (run npx sanity login first)
npm run lintLint the frontend
npm run validateRun the Sanity template validator

Deploy

  • Frontend → Vercel: import the repo and set the Root Directory to frontend. Add the frontend/.env.local variables in the Vercel dashboard. After the first deploy, set NEXT_PUBLIC_SITE_URL to the production URL and add it to CORS:
    npx sanity cors add https://your-project.vercel.app --credentials
  • Studio → Sanity: npm run deploy:studio publishes it to https://<your-studio>.sanity.studio.

Customizing

Want to changeEdit
Business name, phone, hours, servicesBusiness Info in the Studio — no code. Page titles, <title> tags and OG cards all read from it.
Brand color--brand in frontend/app/globals.css
Fontsfrontend/app/layout.tsx (swap the next/font/google pair)
Faviconreplace frontend/app/icon.svg
Service & project photosadd a Featured Image in the Studio. The demo seeds no photos on purpose — service cards fall back to their icon, so you see layout rather than stock images of someone else's work.
Service iconsthe ICONS map in frontend/components/site/service-card.tsx
Demo contentstudio/scripts/seed.ts (read the seeding note above before re-running)

Security & rate limiting

The contact and application forms include a hidden honeypot field (blocks naive bots), validate input with Zod, and cap stored header values. Tokens (SANITY_API_WRITE_TOKEN, RESEND_API_KEY) are server-only — never prefixed with NEXT_PUBLIC_ — and live only in .env.local / .env (gitignored; only .env.*.example is committed).

Before going live with real traffic, add a rate limit so a scripted client can't flood the form endpoints (junk submissions + burned Resend quota):

  • Vercel (recommended): Project → Firewall → Rate Limiting → add a rule matching /api/contact and /api/apply (POST), e.g. 5 requests / 5 min per IP, action Deny. No code changes. On the Hobby plan, use the @vercel/firewall SDK in the route handlers.
  • Not on Vercel: put Cloudflare's free WAF in front and rate-limit those paths, or add @upstash/ratelimit (Upstash Redis) at the top of each route handler.

Upgrade to the full Trades Starter Kit

Need visual editing, applicant tracking, an employee hub, and full SEO tooling? The full Trades Starter Kit includes all of that and more. This lite template uses the same schema field names and component patterns, so upgrading is a natural step up — not a rewrite.

License

MIT — free to use for client and personal projects.