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
RenderingJen CSR (client-side)SSR, SSG, ISR, CSR — na výběr per stránka
RoutingReact Router (manuální)File-based routing (automaticky)
API backendNutný separátní serverAPI Routes v app/api/ složce
SEOPrázdný HTML → problémyHTML s obsahem → skvělé SEO
ObrázkyVanilla img<Image> s auto-optimalizací
FontyManuálnínext/font — zero CLS
BundleJeden bundleAutomatický code splitting
📦
Rychlý start: 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).
// 01 / 09 App Router →

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.

muj-projekt/
app/ ← App Router root
layout.tsx ← Root layout (HTML, body)
page.tsx ← / (homepage)
globals.css
o-nas/
page.tsx ← /o-nas
blog/
page.tsx ← /blog
[slug]/ ← Dynamická route
page.tsx ← /blog/muj-clanek
api/
users/
route.ts ← /api/users (GET, POST...)
loading.tsx ← Loading UI (Suspense)
error.tsx ← Error boundary
not-found.tsx ← 404 stránka
components/ ← Sdílené komponenty
lib/ ← DB, helpers, utils
next.config.js
tailwind.config.ts

Speciální soubory v App Routeru

SouborÚčel
page.tsxUI pro danou route — musí být default export
layout.tsxSdílené UI (nav, footer) — zachová stav při navigaci
loading.tsxAutomatický loading UI (React Suspense)
error.tsxError boundary — musí být Client Component
not-found.tsx404 stránka pro segment
route.tsAPI endpoint — HTTP handlery (GET, POST...)
middleware.tsSpustí před requestem (auth, redirects)
// 02 / 09 Rendering →

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.

🖥️
Server-Side Rendering
SSR
HTML generován na serveru per request. Vždy čerstvá data.
⏱ Každý request. Pomalé začátky, čerstvá data. Uživatelsky specifické stránky, dashboard, košík.
Static Site Generation
SSG
HTML generován při buildu. Servírováno z CDN.
🏗 Build time. Nejrychlejší. Blog, dokumentace, marketing pages.
🔄
Incremental Static Regen.
ISR
SSG + automatická revalidace po N sekundách nebo on-demand.
⏰ Configurable revalidation. E-shop produkty, novinky, ceníky.
⚛️
Client-Side Rendering
CSR
Data fetchována v prohlížeči. SPA chování.
🌐 Runtime. Za auth, real-time data, interaktivní dashboardy.
// 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
VlastnostServer ComponentClient 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 size0 JS na klientaPř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!
next/link vs <a>: Vždy používejte <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.
// 05 / 09 API Routes →

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!
}
// 06 / 09 Metadata & SEO →

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?

// 07 / 09 Cvičení →

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.

📋
Checklist pro každou stránku:
  • 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?
💡
Doporučené odpovědi: Homepage → ISR (revalidate: 3600), Katalog → ISR nebo SSR (pokud filtr v URL), Detail → ISR (revalidate při změně ceny), Košík → CSR (localStorage/cookie, user-specific), Checkout → SSR + Server Action, Admin → SSR + middleware auth check.
🏅

Next.js Architect

Rozumíte App Routeru, rendering strategiím, Server Components a API Routes.

// 08 / 09 Taháček →

// 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 />
// Lekce 4 dokončena L5: Node.js & Express →