I'm a freelance developer. Over the past few years I've built a lot of client websites across different industries, and I kept rebuilding the same patterns — while the "templates" I borrowed online were more like design mockups than real starting points.
So I built 20 real, multi-page website templates and packaged them properly. Here's what I built, the architecture, and what I'd do differently.
The problem with most templates
A typical "Next.js template" in practice:
/
└── app/
└── page.tsx ← everything is in here
One file. Hero, features, pricing, footer — all scroll sections in a single route. That's not a website, it's a brochure. The moment a client says "I also need an About page, a Services page, a Contact form that actually sends email, and a Blog" you're starting from scratch anyway.
What "complete" means
Each template ships with 5–15+ real routes (not scroll sections). The law firm template: /, /practice-areas, /attorneys, /case-results, /about, /contact + a shared /api/contact. The hotel: /, /rooms, /amenities, /gallery, /booking, /contact.
A shared layout with active-link nav:
// app/layout.tsx
import { Nav } from "@/components/Nav"import { Footer } from "@/components/Footer"export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
<Nav />
<main>{children}</main>
<Footer />
</body>
</html>
)
}
Working contact forms (no dead Submit buttons):
const [status, setStatus] = useState<"idle"|"submitting"|"success"|"error">("idle")
async function handleSubmit(e: React.FormEvent) {
e.preventDefault()
setStatus("submitting")
const res = await fetch("/api/contact", {
method: "POST",
body: JSON.stringify(formData),
headers: { "Content-Type": "application/json" },
})
setStatus(res.ok ? "success" : "error")
}
The /api/contact route validates and returns { ok: true } — a clean stub, ready to wire to Resend or any provider without touching the form.
The pattern I landed on: single-file config
Every template feeds from one file:
// src/lib/siteConfig.ts
export const siteConfig = {
name: "Sterling Law Group",
nav: [
{ label: "Practice Areas", href: "/practice-areas" },
{ label: "Our Attorneys", href: "/attorneys" },
{ label: "Contact", href: "/contact" },
],
contact: { email: "info@sterlinglaw.com", phone: "+1 (555) 000-0000" },
attorneys: [
{ name: "Jane Sterling", title: "Founding Partner", specialty: "Corporate Law" },
],
}
Change the brand once, it updates everywhere. Add an attorney to the array, the team page updates. One file to touch when rebranding — no grep-replacing across 30 components.
The stack
Next.js 15 · React 19 · TypeScript (strict) · Tailwind CSS 4 · App Router. No third-party UI library — plain Tailwind so buyers tweak any class without fighting someone else's abstraction. tsc --noEmit and next build pass clean on all 20 (buyers run npm run build immediately — if it errors, refund + bad review).
Lessons from building all 20
-
Form-stub pattern wins — validate + return
{ ok: true }; the buyer wires their email provider in 10 min. -
TS strict catches build-time bugs manual testing misses (missing keys,
undefinedconfig fields, server/client prop mismatches). -
next/imageeverywhere — buyers add real images; protect their Lighthouse score from day one. - Mobile-first is faster than desktop-then-responsive and holds up better.
-
Active-link state is always forgotten —
usePathname()in the Nav, trivial, always missing. - Honest demo labels matter — the web3 token chart and the e-commerce cart are labeled "Demo data" / "Demo checkout — no real payment". Small thing, big trust difference.
What I'd do differently
- Validate the config with Zod for buyer autocomplete + clear errors
- Set up live demos from day one — the single biggest conversion factor for template products
Where to find them
All 20 live demos + source: https://cenkkurtoglu.com — single $49, all 20 for $299 (instant ZIP, commercial license, lifetime updates). Happy to go deeper on any pattern in the comments.
Top comments (0)