Proč Next.js?
Čistý React řeší UI — ale produkční aplikace potřebuje víc: routing, SSR pro SEO, optimalizaci obrázků, API backend, code splitting. Next.js přidává vše toto nad React.
| Funkce | Čistý React (CRA/Vite) | Next.js |
|---|---|---|
| Rendering | Jen CSR (client-side) | SSR, SSG, ISR, CSR — na výběr per stránka |
| Routing | React Router (manuální) | File-based routing (automaticky) |
| API backend | Nutný separátní server | API Routes v app/api/ složce |
| SEO | Prázdný HTML → problémy | HTML s obsahem → skvělé SEO |
| Obrázky | Vanilla img | <Image> s auto-optimalizací |
| Fonty | Manuální | next/font — zero CLS |
| Bundle | Jeden bundle | Automatický code splitting |
npx create-next-app@latest muj-projekt --typescript --tailwind --app — vytvoří projekt s App Routerem, TypeScriptem a Tailwind CSS. Vite alternativa: npm create vite (pro SPA bez SSR).App Router — struktura projektu
Next.js 13+ přineslo App Router — nový systém routování postavený na React Server Components. Složka app/ je kořen celé aplikace.
Speciální soubory v App Routeru
| Soubor | Účel |
|---|---|
page.tsx | UI pro danou route — musí být default export |
layout.tsx | Sdílené UI (nav, footer) — zachová stav při navigaci |
loading.tsx | Automatický loading UI (React Suspense) |
error.tsx | Error boundary — musí být Client Component |
not-found.tsx | 404 stránka pro segment |
route.ts | API endpoint — HTTP handlery (GET, POST...) |
middleware.ts | Spustí před requestem (auth, redirects) |
Rendering strategie
Největší síla Next.js — každá stránka může mít jinou strategii renderování. Volíte na základě dat a požadavků na výkon/aktuálnost.
// SSG (default) — fetch při buildu, revalidate = ISR
async function getData() {
const res = await fetch('https://api.example.com/posts', {
next: { revalidate: 3600 } // ISR: obnov každou hodinu
});
return res.json();
}
// SSR — force dynamic (nový fetch per request)
export const dynamic = 'force-dynamic';
// nebo: fetch s cache: 'no-store'
const res = await fetch(url, { cache: 'no-store' });
// generateStaticParams — SSG pro dynamické routes
export async function generateStaticParams() {
const posts = await getPosts();
return posts.map(post => ({ slug: post.slug }));
// Vytvoří /blog/prvni-clanek, /blog/druhy-clanek atd. při buildu
}
// Kvíz: Blog stránka zobrazuje články, které se mění jednou denně. Která strategie je ideální?
Server vs Client Components
App Router defaultně renderuje vše jako Server Components — výkonné, ale bez hooks a event handlers. Pro interaktivitu přidejte 'use client'.
// app/page.tsx — Server Component (default)
// ✅ Může: async/await, přímý DB přístup, fs, env secrets
// ❌ Nemůže: useState, useEffect, onClick, browser APIs
import { db } from '@/lib/db';
export default async function HomePage() {
// Přímý DB dotaz — žádný API request!
const products = await db.product.findMany({
take: 10,
orderBy: { createdAt: 'desc' }
});
return (
<main>
<h1>Produkty</h1>
{products.map(p => (
<ProductCard key={p.id} product={p} />
))}
</main>
);
}
// components/AddToCartButton.tsx — Client Component
'use client'; // ← direktiva nahoře
import { useState } from 'react';
export function AddToCartButton({ productId }: { productId: number }) {
const [added, setAdded] = useState(false);
return (
<button onClick={() => { addToCart(productId); setAdded(true); }}>
{added ? '✅ Přidáno' : '🛒 Do košíku'}
</button>
);
}
// Pravidlo: "push client components down" — čím hlíže k listům,
// tím více věcí může zůstat jako Server Components
| Vlastnost | Server Component | Client Component |
|---|---|---|
| Direktiva | (nic — default) | 'use client' |
| useState, useEffect | ❌ | ✅ |
| Event handlers (onClick) | ❌ | ✅ |
| async/await přímo | ✅ | ❌ (přes useEffect) |
| Přístup k DB, fs | ✅ (přímý) | ❌ (přes API) |
| Env secrets | ✅ (server-only) | ❌ (exposuje klientovi) |
| Bundle size | 0 JS na klienta | Přidá do JS bundle |
// Kvíz: Komponenta potřebuje zobrazit data z databáze A mít onClick handler. Jak to vyřešit?
Routing & Layouts
App Router používá file-based routing — složky = URL segmenty, page.tsx = renderovaná stránka. Layouts sdílejí UI mezi routes a zachovávají stav.
// app/layout.tsx — Root layout (každá stránka sdílí)
export default function RootLayout({
children,
}: { children: React.ReactNode }) {
return (
<html lang="cs">
<body>
<Navbar />
<main>{children}</main>
<Footer />
</body>
</html>
);
}
// app/blog/[slug]/page.tsx — Dynamická route
interface Props {
params: { slug: string };
searchParams: { [key: string]: string };
}
export default async function BlogPost({ params }: Props) {
const post = await getPost(params.slug);
if (!post) notFound(); // vyvolá not-found.tsx
return <article>{post.content}</article>;
}
// Navigace — next/link (prefetch automaticky!)
import Link from 'next/link';
<Link href="/blog/muj-clanek">Číst článek</Link>
// Programatická navigace
import { useRouter } from 'next/navigation';
const router = useRouter();
router.push('/dashboard');
router.replace('/login'); // bez history entry
router.back();
// Route Groups — bez URL segmentu
// app/(marketing)/page.tsx → /
// app/(marketing)/o-nas/page.tsx → /o-nas
// app/(app)/dashboard/page.tsx → /dashboard
// Skupiny mohou mít různé layouty!
<Link> pro interní navigaci. Next.js automaticky prefetchuje stránku při hoverování nad odkazem — viditelné stránky se načítají na pozadí. Výsledek: okamžitá navigace.API Routes
Next.js App Router umožňuje vytvářet REST API endpointy přímo v projektu — soubor route.ts v libovolné složce v app/.
// app/api/products/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { db } from '@/lib/db';
// GET /api/products
export async function GET(request: NextRequest) {
const { searchParams } = new URL(request.url);
const page = parseInt(searchParams.get('page') ?? '1');
const limit = 10;
const products = await db.product.findMany({
skip: (page - 1) * limit,
take: limit,
orderBy: { createdAt: 'desc' },
});
return NextResponse.json({ products, page });
}
// POST /api/products
export async function POST(request: NextRequest) {
const body = await request.json();
// Validace
if (!body.nazev || !body.cena) {
return NextResponse.json(
{ error: 'Chybí povinné pole' },
{ status: 400 }
);
}
const product = await db.product.create({ data: body });
return NextResponse.json(product, { status: 201 });
}
// app/api/products/[id]/route.ts — Dynamická API route
export async function GET(
request: NextRequest,
{ params }: { params: { id: string } }
) {
const product = await db.product.findUnique({
where: { id: parseInt(params.id) }
});
if (!product) return NextResponse.json({ error: 'Nenalezeno' }, { status: 404 });
return NextResponse.json(product);
}
export async function DELETE(
request: NextRequest,
{ params }: { params: { id: string } }
) {
await db.product.delete({ where: { id: parseInt(params.id) } });
return new NextResponse(null, { status: 204 });
}
Server Actions — formuláře bez API route
// Moderní alternativa k API route pro formuláře (Next.js 14+)
// app/actions.ts
'use server';
import { revalidatePath } from 'next/cache';
export async function vytvorProdukt(formData: FormData) {
const nazev = formData.get('nazev') as string;
const cena = Number(formData.get('cena'));
await db.product.create({ data: { nazev, cena } });
revalidatePath('/produkty'); // invaliduje cache stránky!
}
// app/produkt/new/page.tsx — použití Server Action
export default function NewProductPage() {
return (
<form action={vytvorProdukt}>
<input name="nazev" required />
<input name="cena" type="number" required />
<button type="submit">Vytvořit</button>
</form>
);
// Žádný JavaScript na klientovi — formulář funguje bez JS!
}
Metadata API & SEO
Next.js má vestavěné Metadata API — definujete objekty v layout.tsx nebo page.tsx a Next.js automaticky generuje správné <meta> tagy.
// app/layout.tsx — globální metadata
import type { Metadata } from 'next';
export const metadata: Metadata = {
title: {
default: 'MůjShop',
template: '%s | MůjShop', // "Produkty | MůjShop"
},
description: 'Nejlepší e-shop s kapesními noži',
keywords: ['nože', 'kapesní nůž', 'victorinox'],
authors: [{ name: 'Jana Nováková' }],
openGraph: {
type: 'website',
locale: 'cs_CZ',
url: 'https://www.mujshop.cz',
siteName: 'MůjShop',
images: [{ url: '/og-image.jpg', width: 1200, height: 630 }],
},
twitter: {
card: 'summary_large_image',
creator: '@mujshop',
},
robots: {
index: true,
follow: true,
googleBot: { index: true, follow: true },
},
};
// app/blog/[slug]/page.tsx — dynamická metadata
export async function generateMetadata({ params }: Props): Promise<Metadata> {
const post = await getPost(params.slug);
return {
title: post.title,
description: post.excerpt,
openGraph: {
title: post.title,
description: post.excerpt,
images: [{ url: post.coverImage }],
type: 'article',
publishedTime: post.createdAt.toISOString(),
},
};
}
// next/image — automatická optimalizace
import Image from 'next/image';
<Image
src="/hero.jpg"
alt="Hero obrázek"
width={1200}
height={600}
priority // ← LCP obrázek — načtení s prioritou
placeholder="blur"
/>
// Kvíz: Co přidá export const dynamic = 'force-dynamic' do Next.js page?
Cvičení — Navrhnout Next.js architekturu
Navrhněte strukturu složek a rendering strategii pro e-shop s: homepage (bestsellery), produktový katalog (filtrovatelný), detail produktu, košík, checkout, admin panel.
- Jaká data stránka potřebuje?
- Jak často se data mění?
- Je obsah uživatelsky specifický?
- → SSG / ISR / SSR / CSR
- Které komponenty musí být Client (
'use client')?
// Navrhněte soubory pro každý segment:
app/
layout.tsx // Root layout — co patří sem?
page.tsx // Homepage — jaká rendering strategie?
produkty/
page.tsx // Katalog — SSG / ISR / SSR?
[slug]/
page.tsx // Detail produktu — jak revalidovat?
kosik/
page.tsx // Košík — CSR? Proč?
checkout/
page.tsx // Checkout — SSR? Server Action?
admin/
layout.tsx // Admin layout — middleware auth?
produkty/
page.tsx
new/page.tsx
api/
products/route.ts // GET, POST
products/[id]/route.ts // GET, PUT, DELETE
// Zodpovězte:
// 1. Kde použít generateStaticParams?
// 2. Kde next: { revalidate: N }?
// 3. Které stránky potřebují 'use client'?
// 4. Kde použít Server Actions?
Next.js Architect
Rozumíte App Routeru, rendering strategiím, Server Components a API Routes.
// Taháček
// Rendering strategie
export const dynamic = 'force-dynamic'; // SSR
const res = await fetch(url, { next: { revalidate: 3600 } }); // ISR
const res = await fetch(url, { cache: 'no-store' }); // SSR
// Dynamické routes
export async function generateStaticParams() {
return items.map(i => ({ slug: i.slug }));
}
// Metadata
export const metadata: Metadata = { title: '...', description: '...' };
export async function generateMetadata({ params }) { return { title: ... }; }
// Server Action
'use server';
export async function myAction(formData: FormData) {
// DB operations...
revalidatePath('/path');
}
// Navigace
import Link from 'next/link';
<Link href="/stranka">Text</Link>
const router = useRouter(); router.push('/path'); // Client only
// Image
import Image from 'next/image';
<Image src="/img.jpg" alt="..." width={800} height={400} priority />