Industry-neutral Next.js business site template. Clone it, replace the example business in content/site.ts, swap images, and deploy.
Stack: Next.js (App Router) · TypeScript · Tailwind CSS v4 · Vercel.
The repo ships with a fully filled demo business: your business (home repairs / handyman, Columbus, OH). It builds and renders out of the box. Replace every field in content/site.ts with your customer’s details — or drop in output from the Chatterbuilt MCP render_site_ts tool.
All copy and config live in one file:
content/site.ts
Change business info, hero text, services, process steps, FAQ, SEO titles/descriptions, nav labels, gallery, reviews, or set schedulingUrl there. Components render exclusively from that object — no hunting through JSX for marketing text.
In content/site.ts:
schedulingUrl: "", // empty = “Book by phone” fallback
// e.g. schedulingUrl: "https://calendly.com/your-link",When set to a Calendly/Cal.com (or similar) embed URL, the Schedule section shows a lazy-loaded iframe. When empty, it shows a call-to-book card with phone + form link.
- Append an object to
site.servicesorsite.faqincontent/site.ts. - For services, include
slug,name, descriptions,included,idealFor,icon,image(path underpublic/visuals/), andseo. - Service pages are generated automatically from the array (
/services/[slug]).
Declare brand in content/site.ts → site.brand (same shape as the marketing site):
brand: {
wordmark: "Northgate Services", // text fallback when logo is missing
// logoPath: "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/brand/logo.png", // horizontal lockup — Header + Footer
// iconPath: "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/brand/icon.png", // square — favicon + apple-touch + OG
},| Path | Use |
|---|---|
public/brand/logo.png |
Header / Footer lockup (logoPath) |
public/brand/icon.png |
Favicon + OG (iconPath) |
Omit logoPath / iconPath until assets exist — the UI uses wordmark text and never shows a broken-image icon. Drop files under public/brand/ and uncomment the paths when ready.
Sites read as high-quality when they use two tiers with different jobs:
| Tier | Folder | Job |
|---|---|---|
| 1 — Offer | public/visuals/ |
Staged, art-directed images that sell the offer (hero, services, section texture) |
| 2 — Proof | public/gallery/ |
Evidence of finished work (project grid) |
Tier 1 sells the offer; Tier 2 proves the work. Sites that ship only Tier 1 look like brochures. Brand marks (public/brand/) are separate — see Brand marks above.
| File | Count | Aspect | Orientation | Where it renders |
|---|---|---|---|---|
hero-primary.png |
1 | 16:9 (ship ~1600×900) | Landscape | Home hero full-bleed (components/Hero.tsx) — subject weighted right; copy stays left |
service-<slug>.png |
1 per site.services[] entry |
4:3 (ship ~800×600) | Landscape | Service cards + service detail page (Services.tsx, /services/[slug]) |
section-texture.png |
1 | ~3:2 (ship ~1200×800) | Landscape | Low-opacity texture under Process + FAQ (SectionTexture, ~6–9% opacity) |
1:1 service rule: every site.services[i].slug needs a matching file and path:
slug: "home-repairs" → public/visuals/service-home-repairs.png
image: "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/visuals/service-home-repairs.png" // in content/site.ts
No orphan files. No shared stock image reused across unrelated services. Slug in the filename must match the service slug exactly.
| File | Count | Aspect | Orientation | Where it renders |
|---|---|---|---|---|
project-01.png … project-NN.png |
~8–12 (ship 8; target ~12 for demos) | 1:1 (ship ~600×600) | Square | Gallery grid (components/Gallery.tsx, aspect-square) |
Each file needs a matching object in gallery.items (src, title, alt, …). Paths stay under /gallery/project-NN.png with zero-padded numbers.
| Kind | Pattern | Example |
|---|---|---|
| Hero | hero-primary.png |
/visuals/hero-primary.png |
| Service | service-<slug>.png |
/visuals/service-outdoor-maintenance.png |
| Section texture | section-texture.png |
/visuals/section-texture.png |
| Gallery | project-NN.png (01, 02, …) |
/gallery/project-03.png |
| Brand | logo.png / icon.png |
/brand/logo.png |
Prefer PNG or WebP, longest edge ~1600px for hero, ~800px for service, ~600px for gallery — large enough for sharp next/image output, small enough that quality 60–75 stays efficient. Use fill + object-cover friendly crops (subject not hard-cropped at edges).
- Real working context over abstract stock (no floating tools on white void).
- Hands-on work preferred; tools, materials, finished surfaces, workspaces.
- No visible faces (avoids uncanny generated-person problems and model-release risk).
- No fake signage or invented brand marks in the frame.
- Consistent lighting and time-of-day within one site (one mood, not a collage of five lighting setups).
- Industry-neutral filenames only — never reintroduce another vertical’s product identifiers into paths or component names.
Never ship another industry’s photographs as this business’s identity. Classic cautionary case: a church (or HVAC, restaurant, cleaning) rebrand that still shows the prior vertical’s workshop / range / trade-specific textures so visitors see someone else’s business. Before PR: every image must be this trade, this offer, or a deliberate neutral placeholder you plan to replace.
Agent-executable checklist and full rules: AGENTS.md → Images (also loaded via CLAUDE.md).
Optional theme block in content/site.ts sets CSS custom properties (background, foreground, accent, muted, panels, borders). Omit it to keep the default charcoal + blaze-orange look. See the comment on theme in content/site.ts and lib/theme.ts for keys and a light-ground example. No component edits are required to re-theme.
Optional theme.fontDisplay / theme.fontBody pick typefaces from a closed registry (next/font/google, build-time). Omit both to keep Barlow Condensed headings + Inter body — the same faces as before this contract.
| Key | Role | Default |
|---|---|---|
fontDisplay |
Headings, buttons, display chrome | Barlow Condensed |
fontBody |
Body copy | Inter |
Allowed names: Inter, Barlow Condensed, Source Serif 4, Archivo, Manrope, Zilla Slab, EB Garamond, Playfair Display. Unknown names fail the build (not silently at runtime). Implementation: lib/fonts.ts → semantic CSS slots on <html> in app/layout.tsx.
Demo fleet repos that still request older visual filenames are served via rewrites in next.config.ts until those demos are updated.
The form posts to POST /api/contact, which sends email via the Resend REST API (no SDK).
| Env var | Purpose | Default |
|---|---|---|
RESEND_API_KEY |
Resend API key | (required to actually send) |
CONTACT_TO_EMAIL |
Inbox for submissions | site.contact.email |
CONTACT_FROM_EMAIL |
From address (must be allowed in Resend) | onboarding@resend.dev |
If RESEND_API_KEY is missing, the API logs the submission server-side and still returns success so local/dev UX never breaks.
Copy .env.example to .env.local for local testing:
cp .env.example .env.local
# then fill RESEND_API_KEY, CONTACT_TO_EMAIL, CONTACT_FROM_EMAILnpm install
npm run devOpen http://localhost:3000.
npm run build # production build — run before pushing
npm run start # serve production build
npm run lint # ESLint- Deploy button (above) clones this template into a new Vercel project.
- Pushes to
mainauto-deploy when the repo is connected to Vercel. - Open a PR for a preview URL before merging.
- Set Resend env vars in the Vercel project settings for production email.
What hosting costs, what’s free, and how to hard-cap spend: docs/SPEND-GUARDRAILS.md.
app/ # routes, layout, SEO (sitemap, robots, OG image)
api/contact/ # contact form handler
contact/ # /contact
services/[slug]/ # service detail pages
components/ # UI sections (read from content/site.ts)
content/site.ts # ← edit me for copy/config (includes brand)
public/brand/ # optional logo + icon (see site.brand)
public/gallery/ # project gallery images
public/visuals/ # hero, service cards, section textures
app/llms.txt/ # AEO summary for answer engines (generated from content/site.ts)
Every template site ships two growth surfaces (config in content/site.ts → chatterbuilt):
| Flag | Default | What it does |
|---|---|---|
showBadge |
true |
Footer link: “Built with Chatterbuilt — see yours” → get-started funnel (UTM-tagged) |
showChatWidget |
true |
Floating Instant answers bubble (keyword search over content/kb.json) |
Set either flag to false to hide that surface — no component edits. MCP-rendered site.ts files may omit chatterbuilt; product defaults still apply. Badge copy stays plain (no fabricated stats). Clicks fire first-party Umami/GA4 events when you have analytics configured.
- Metadata + Open Graph / Twitter cards
app/sitemap.ts,app/robots.ts(allows GPTBot, ClaudeBot, PerplexityBot, Google-Extended)- JSON-LD: LocalBusiness, Service, FAQPage, BreadcrumbList
app/llms.txt/route.ts— honest business summary for AI agents/crawlers, generated fromcontent/site.ts(same single-source pattern as sitemap/robots)
This template is built so an AI coding agent (or you) can rebrand the whole site by editing one file.
- Clone or “Use this template” on GitHub, then open the project in your AI tool of choice.
- Tell the AI who the business is, for example:
- “Replace your business with [Business Name], a [trade] in [City, State]. Update phone, address, three services, FAQ, reviews, and SEO titles. Keep the layout.”
- The agent should only change
content/site.ts(and images underpublic/if you supply photos). Components already read fromsite— no prose rewrites in JSX. - If you use the Chatterbuilt MCP, run the interview →
validate→render_site_tsflow and drop the generated source intocontent/site.ts. - Run
npm run build, push a branch, open a PR for a Vercel preview, then merge tomainfor production.
Rules for any agent editing this repo are in AGENTS.md.
See AGENTS.md: all copy in content/site.ts; components never hold business prose; run npm run build before pushing; main auto-deploys; preview via PR. Spend caps: docs/SPEND-GUARDRAILS.md.