I've built 4 production Next.js 15 templates in the last few months.
I made a lot of mistakes along the way.
Here are the ones that wasted the most of my time โ so you don't have to repeat them.
1. Forgetting suppressHydrationWarning with next-themes
This one took me hours to debug.
// ๐ซ This causes hydration errors
<html lang="en">
<body>
<ThemeProvider>{children}</ThemeProvider>
</body>
</html>
// โ
This fixes it
<html lang="en" suppressHydrationWarning>
<body>
<ThemeProvider attribute="class" defaultTheme="dark">
{children}
</ThemeProvider>
</body>
</html>
The suppressHydrationWarning on the html tag is not optional when using next-themes. Without it you get a hydration mismatch on every page load.
2. Using useEffect for Data That Should Be Server-Side
// ๐ซ Fetching on client when you don't need to
'use client';
export default function Page() {
const [data, setData] = useState(null);
useEffect(() => {
fetch('/api/data').then(r => r.json()).then(setData);
}, []);
return <div>{data?.title}</div>;
}
// โ
Fetch on server โ faster, simpler, better SEO
export default async function Page() {
const data = await fetch('https://api.example.com/data').then(r => r.json());
return <div>{data.title}</div>;
}
If your data doesn't change based on user interaction โ fetch it on the server. Faster page load, better SEO, less JavaScript.
3. Not Using Next.js Image Component
// ๐ซ Regular img tag
<img src="/hero.jpg" alt="Hero" className="w-full" />
// โ
Next.js Image โ automatic WebP, lazy loading, size optimization
import Image from 'next/image';
<Image
src="/hero.jpg"
alt="Hero"
width={1200}
height={630}
priority // add this for above-the-fold images only
className="w-full"
/>
The priority prop is important โ only use it for images visible without scrolling. Using it everywhere defeats the purpose.
4. Wrong Font Loading
// ๐ซ This causes layout shift and slow loading
<link href="https://fonts.googleapis.com/css2?family=Inter" rel="stylesheet" />
// โ
Use next/font โ zero layout shift, self-hosted automatically
import { Inter } from 'next/font/google';
const inter = Inter({
subsets: ['latin'],
display: 'swap', // prevents invisible text during load
});
export default function RootLayout({ children }) {
return (
<html className={inter.className}>
{children}
</html>
);
}
5. Client Components Too High in the Tree
// ๐ซ Making the whole page a client component for one button
'use client';
export default function Page() {
const [open, setOpen] = useState(false);
return (
<div>
<HeavyContent /> {/* This is now client-side JS */}
<button onClick={() => setOpen(!open)}>Toggle</button>
</div>
);
}
// โ
Push client state down โ keep the page as server component
export default function Page() {
return (
<div>
<HeavyContent /> {/* Still server-rendered */}
<ToggleButton /> {/* Only this is client */}
</div>
);
}
// Separate file
'use client';
export function ToggleButton() {
const [open, setOpen] = useState(false);
return <button onClick={() => setOpen(!open)}>Toggle</button>;
}
6. Not Handling Loading States Properly
// ๐ซ Page just shows nothing while loading
export default async function Page() {
const data = await slowFetch(); // user sees blank page
return <div>{data.title}</div>;
}
// โ
Use loading.tsx โ automatic Suspense boundary
// app/dashboard/loading.tsx
export default function Loading() {
return (
<div className="animate-pulse space-y-4">
<div className="h-8 bg-slate-700 rounded w-1/3" />
<div className="h-4 bg-slate-700 rounded w-2/3" />
<div className="h-4 bg-slate-700 rounded w-1/2" />
</div>
);
}
Just create a loading.tsx file next to your page.tsx โ Next.js handles the rest automatically.
7. Ignoring Metadata for SEO
// ๐ซ No metadata = bad SEO
export default function Page() {
return <div>Content</div>;
}
// โ
Export metadata from every page
import type { Metadata } from 'next';
export const metadata: Metadata = {
title: 'Dashboard โ Your SaaS',
description: 'Manage your AI SaaS with our premium dashboard.',
openGraph: {
title: 'Dashboard โ Your SaaS',
description: 'Manage your AI SaaS with our premium dashboard.',
images: ['/og-image.jpg'],
},
};
export default function Page() {
return <div>Content</div>;
}
Every page needs its own metadata. The template from layout.tsx is a fallback, not a replacement.
Summary
| Mistake | Fix |
|---|---|
| Hydration errors with themes | Add suppressHydrationWarning to <html>
|
| Client fetching server data | Use async server components |
| Regular img tags | Use next/image
|
| Google Fonts via link tag | Use next/font/google
|
| Client components too high | Push state down to leaf components |
| No loading states | Create loading.tsx files |
| Missing page metadata | Export metadata from every page |
I applied all of these while building my Next.js templates.
If you want to see them in a real production codebase:
Live demos:
- https://og-ai-next.vercel.app/
- https://neurodash-dashbord.vercel.app/
- https://agencyx-template.vercel.app/
Templates: https://pixelanas.gumroad.com
Drop your own Next.js mistakes in the comments ๐
Anas โ full-stack Next.js developer. Building premium templates and web apps. X: @pixelanas
Top comments (0)