Fiszki Online Next.js (Preview)
Darmowy podgląd 15 z 40 dostępnych pytań
Next.js - Podstawy i Architektura
Czym jest Next.js i jakie problemy rozwiązuje w porównaniu do czystego React?
Odpowiedź w 30 sekund: Next.js to framework React oferujący gotowe rozwiązania dla SSR (Server-Side Rendering), SSG (Static Site Generation), routingu i optymalizacji. Rozwiązuje problemy czystego React takie jak SEO, początkowe ładowanie strony, konfiguracja routingu oraz brak wbudowanych mechanizmów renderowania po stronie serwera.
Odpowiedź w 2 minuty: Next.js to production-ready framework zbudowany na React, który dostarcza rozwiązania dla najczęstszych wyzwań w tworzeniu aplikacji webowych. Podczas gdy czysty React to biblioteka UI wymagająca dodatkowych narzędzi do routingu (React Router), SSR (własna konfiguracja), bundlingu (Webpack/Vite) i optymalizacji, Next.js oferuje to wszystko "out of the box".
Framework rozwiązuje kluczowe problemy: po pierwsze, SEO - dzięki renderowaniu po stronie serwera (SSR) i generowaniu statycznemu (SSG) content jest dostępny dla crawlerów. Po drugie, wydajność - automatyczne code splitting, optymalizacja obrazów przez komponent Image, prefetching linków. Po trzecie, developer experience - file-based routing eliminuje potrzebę konfiguracji tras, API routes pozwalają tworzyć backend endpoints w tym samym projekcie, a Fast Refresh zapewnia instant feedback podczas developmentu.
Next.js umożliwia również hybrydowe podejście - możesz mieszać SSG, SSR i CSR (Client-Side Rendering) w jednej aplikacji, wybierając optymalną strategię dla każdej strony. To czyni framework idealnym dla aplikacji wymagających zarówno dynamicznych jak i statycznych treści, e-commerce, dashboardów czy content-heavy websites.
Przykład kodu:
// Czysty React - wymaga dodatkowej konfiguracji routingu
import { BrowserRouter, Route } from 'react-router-dom';
function App() {
return (
<BrowserRouter>
<Route path="/about" component={About} />
</BrowserRouter>
);
}
// Next.js - file-based routing, automatyczny SSR/SSG
// app/about/page.tsx
export default function About() {
return <h1>O nas</h1>;
}
// Renderowanie po stronie serwera z danymi
export async function generateMetadata() {
return {
title: 'O nas',
description: 'Dowiedz się więcej o naszej firmie'
};
}
Materiały
↑ Powrót na góręJakie są różnice między App Router a Pages Router w Next.js?
Odpowiedź w 30 sekund:
Pages Router to klasyczny system routingu Next.js (katalog pages/), podczas gdy App Router to nowszy system (katalog app/) wprowadzony w Next.js 13. App Router wspiera React Server Components, ulepszone layouty, streaming oraz nowy model data fetching, podczas gdy Pages Router używa tradycyjnych metod jak getServerSideProps.
Odpowiedź w 2 minuty:
Pages Router był głównym systemem routingu w Next.js od początku do wersji 12. Wykorzystuje katalog pages/, gdzie każdy plik automatycznie staje się routem. Data fetching odbywa się przez specjalne funkcje jak getStaticProps (SSG), getServerSideProps (SSR) i getInitialProps. Każda strona domyślnie jest Client Component, a optymalizacje wymagają manualnej konfiguracji.
App Router, wprowadzony w Next.js 13, to fundamentalna zmiana architektury. Wykorzystuje katalog app/ i wprowadza React Server Components jako domyślne - komponenty renderowane tylko po stronie serwera, co redukuje bundle size. Nowy model data fetching używa natywnego fetch() z automatycznym cache i revalidation. Layouty są teraz zagnieżdżone i współdzielone między routami, co eliminuje niepotrzebne re-rendery. Streaming i Suspense są wbudowane, pozwalając na progressive rendering.
Kluczowe różnice: w App Router domyślnie wszystko to Server Component (wymaga 'use client' dla interaktywności), podczas gdy Pages Router domyślnie to Client Component. App Router oferuje lepszą wydajność dzięki automatycznemu code splitting na poziomie komponentów, parallel routes, intercepting routes i loading UI. Pages Router pozostaje stabilny i dobrze udokumentowany, ale nie otrzymuje nowych features - to legacy approach.
Przykład kodu:
// PAGES ROUTER - pages/blog/[slug].tsx
export async function getServerSideProps({ params }) {
const post = await fetchPost(params.slug);
return { props: { post } };
}
export default function BlogPost({ post }) {
return <article>{post.content}</article>;
}
// APP ROUTER - app/blog/[slug]/page.tsx
async function BlogPost({ params }) {
// Fetch bezpośrednio w komponencie - Server Component
const post = await fetchPost(params.slug);
return <article>{post.content}</article>;
}
// Współdzielony layout dla wszystkich postów
// app/blog/layout.tsx
export default function BlogLayout({ children }) {
return (
<div>
<nav>Menu bloga</nav>
{children} {/* Layout persists między nawigacją */}
</div>
);
}
graph TB
subgraph "Pages Router"
A[pages/] --> B[index.tsx]
A --> C[about.tsx]
A --> D[blog/[slug].tsx]
B --> E[getStaticProps]
D --> F[getServerSideProps]
end
subgraph "App Router"
G[app/] --> H[page.tsx]
G --> I[layout.tsx]
G --> J[blog/[slug]/page.tsx]
J --> K[Server Component<br/>async/await]
I --> L[Nested Layouts]
end
Materiały
↑ Powrót na góręCzym są Server Components i Client Components w Next.js 13+?
Odpowiedź w 30 sekund:
Server Components to komponenty React renderowane wyłącznie po stronie serwera, które nie trafiają do bundle JS przeglądarki. Client Components (oznaczone 'use client') to tradycyjne komponenty React z interaktywnością i hooks. Server Components domyślnie w App Router, redukują bundle size i mogą bezpośrednio odwoływać się do backendu.
Odpowiedź w 2 minuty: Server Components to rewolucyjna funkcja React zintegrowana z Next.js 13+, która zmienia sposób myślenia o renderowaniu. Komponenty te wykonują się tylko na serwerze, nigdy nie są wysyłane do przeglądarki jako JavaScript, co drastycznie redukuje bundle size. Mogą bezpośrednio odwoływać się do baz danych, API, filesystem - kod nie jest eksponowany klientowi. Nie mają dostępu do browser APIs, hooks stanu (useState, useEffect) ani event handlers.
Client Components to znane nam komponenty React - interaktywne, z dostępem do hooks, browser APIs i event handlers. W App Router wymagają dyrektywy 'use client' na początku pliku. Są niezbędne dla: formularzy, animacji, zarządzania stanem lokalnym, integracji z browser APIs (localStorage, geolocation), używania React hooks i third-party libraries wymagających przeglądarki.
Architektura hybrydowa: Next.js automatycznie optymalizuje granicę między server a client. Server Components mogą importować Client Components (ale nie odwrotnie), co pozwala na precyzyjne kontrolowanie co trafia do bundle. Najlepsza praktyka to Server Components jako domyślne, Client Components tylko tam gdzie potrzebna interaktywność. Server Components mogą przekazywać dane do Client Components przez props, eliminując potrzebę dodatkowych API calls.
Przykład kodu:
// app/dashboard/page.tsx - Server Component (domyślnie)
import { ClientCounter } from './ClientCounter';
import { db } from '@/lib/database';
export default async function Dashboard() {
// Bezpośredni dostęp do bazy - kod NIE trafia do przeglądarki
const users = await db.user.findMany();
const stats = await calculateStats(); // Ciężkie obliczenia na serwerze
return (
<div>
<h1>Dashboard</h1>
{/* Server Component - zero JS w przeglądarce */}
<UserList users={users} />
{/* Client Component - interaktywny counter */}
<ClientCounter initialCount={stats.totalVisits} />
</div>
);
}
// app/dashboard/ClientCounter.tsx - Client Component
'use client'; // Dyrektywa wymagana dla interaktywności
import { useState } from 'react';
export function ClientCounter({ initialCount }: { initialCount: number }) {
const [count, setCount] = useState(initialCount);
return (
<button onClick={() => setCount(count + 1)}>
Kliknięcia: {count}
</button>
);
}
graph LR
A[Server Component] -->|może importować| B[Client Component]
B -->|NIE może importować| A
A -->|zero JS| C[Browser]
B -->|wysyła JS| C
A -->|dostęp| D[Database/API]
A -->|dostęp| E[Filesystem]
B -->|brak dostępu| D
B -->|dostęp| F[Browser APIs]
A -->|brak dostępu| F
Materiały
↑ Powrót na góręKiedy używać dyrektywy "use client" a kiedy "use server"?
Odpowiedź w 30 sekund:
'use client' używaj dla komponentów wymagających interaktywności (hooks, event handlers, browser APIs). 'use server' oznacza Server Actions - funkcje wykonywane na serwerze, używane w formularzach i mutacjach danych. Domyślnie w App Router wszystko to Server Component, więc 'use client' dodajesz tylko gdy potrzebna interaktywność.
Odpowiedź w 2 minuty:
'use client' to dyrektywa oznaczająca Client Component boundary. Używaj gdy: (1) potrzebujesz React hooks (useState, useEffect, useContext), (2) obsługujesz zdarzenia użytkownika (onClick, onChange), (3) korzystasz z browser APIs (localStorage, window, document), (4) używasz third-party libraries wymagających przeglądarki, (5) tworzysz interaktywne UI (formularze, animacje, modals). Ważne: umieszczaj 'use client' możliwie najniżej w drzewie komponentów - nie oznaczaj całej strony jako client jeśli tylko mały komponent wymaga interaktywności.
'use server' oznacza Server Actions - funkcje asynchroniczne wykonywane wyłącznie na serwerze. Używaj gdy: (1) obsługujesz mutacje danych (POST, PUT, DELETE), (2) integrujesz z bazami danych, (3) wykonujesz wrażliwe operacje (np. z secret keys), (4) implementujesz form submissions, (5) revalidujesz cache. Server Actions mogą być wywoływane z Client Components przez form actions lub bezpośrednie wywołania, ale zawsze wykonują się na serwerze - nigdy nie eksponują kodu/secrets przeglądarce.
Strategia: rozpoczynaj od Server Components (domyślne), dodawaj 'use client' tylko gdy naprawdę potrzebne, wynoś interaktywne części do małych wydzielonych komponentów. Server Actions ('use server') używaj dla wszelkich operacji modyfikujących dane zamiast tradycyjnych API routes - są bezpieczniejsze i prostsze w obsłudze.
Przykład kodu:
// app/products/page.tsx - Server Component (domyślnie, bez dyrektyw)
import { ProductFilter } from './ProductFilter'; // Client Component
import { db } from '@/lib/db';
export default async function ProductsPage() {
const products = await db.product.findMany(); // Bezpośredni dostęp do DB
return (
<div>
<ProductFilter /> {/* Mały Client Component */}
<ProductList products={products} /> {/* Server Component */}
</div>
);
}
// app/products/ProductFilter.tsx
'use client'; // Potrzebne: useState, onChange
import { useState } from 'react';
export function ProductFilter() {
const [category, setCategory] = useState('all');
return (
<select onChange={(e) => setCategory(e.target.value)}>
<option value="all">Wszystkie</option>
<option value="electronics">Elektronika</option>
</select>
);
}
// app/actions.ts - Server Actions
'use server'; // Cały plik to Server Actions
import { db } from '@/lib/db';
import { revalidatePath } from 'next/cache';
export async function createProduct(formData: FormData) {
// Wykonuje się NA SERWERZE, nawet gdy wywoływane z Client Component
const name = formData.get('name') as string;
await db.product.create({
data: { name, price: 100 }
});
revalidatePath('/products'); // Odśwież cache
}
// app/products/NewProductForm.tsx
'use client'; // Formularz wymaga interaktywności
import { createProduct } from '../actions';
export function NewProductForm() {
return (
<form action={createProduct}>
{/* createProduct wykonuje się na serwerze */}
<input name="name" required />
<button type="submit">Dodaj produkt</button>
</form>
);
}
flowchart TD
A{Potrzebujesz interaktywności?} -->|Tak| B['use client']
A -->|Nie| C[Server Component<br/>domyślnie]
B --> D{Co dokładnie?}
D -->|hooks, events, browser APIs| E[Client Component]
F{Mutacje danych?} -->|Tak| G['use server'<br/>Server Action]
F -->|Nie| C
G --> H[Bezpieczne operacje<br/>na serwerze]
style B fill:#ff9999
style G fill:#99ccff
style C fill:#99ff99
Materiały
↑ Powrót na góręJak działa hydracja w Next.js i jakie są jej implikacje dla wydajności?
Odpowiedź w 30 sekund: Hydracja to proces, w którym React "ożywia" HTML wygenerowany na serwerze, dodając interaktywność i event listeners. W Next.js serwer wysyła gotowy HTML (szybkie First Contentful Paint), następnie JavaScript "hydratuje" go czyniąc interaktywnym. Błędy hydracji występują gdy HTML serwera nie zgadza się z renderem klienta.
Odpowiedź w 2 minuty: Hydracja w Next.js to kluczowy proces łączący Server-Side Rendering z interaktywnością React. Działa w trzech krokach: (1) Serwer renderuje komponenty React do HTML i wysyła go do przeglądarki - użytkownik widzi content natychmiast (FCP, First Contentful Paint). (2) Przeglądarka pobiera JavaScript bundle aplikacji. (3) React wykonuje "hydrację" - przechodzi przez istniejący DOM, dopasowuje go do Virtual DOM i attachuje event handlers, czyniąc stronę interaktywną (TTI, Time to Interactive).
Implikacje wydajnościowe są znaczące: po stronie pozytywów - użytkownik widzi content natychmiast zanim JavaScript się załaduje (lepsze UX, SEO), HTML jest funkcjonalny nawet bez JS (progressive enhancement). Po stronie negatywów - okres między FCP a TTI gdzie strona wygląda interaktywnie ale nie reaguje na kliknięcia ("uncanny valley"), podwójna praca - rendering na serwerze i ponownie w przeglądarce (CPU overhead), duże JavaScript bundles opóźniają TTI.
Next.js 13+ App Router wprowadza Selective Hydration i Streaming - komponenty mogą być hydratowane progresywnie w miarę potrzeby, Server Components w ogóle nie wymagają hydracji (zero JS), co drastycznie redukuje TTI. Partial Prerendering (PPR) pozwala mieszać statyczne i dynamiczne części bez hydracji całej strony.
Przykład kodu:
// Serwer generuje HTML
// app/page.tsx - Server Component
export default async function HomePage() {
const data = await fetch('https://api.example.com/data');
return (
<div>
<h1>Witaj!</h1>
{/* Ten HTML jest wysyłany natychmiast */}
<StaticContent data={data} />
{/* Ten komponent wymaga hydracji */}
<InteractiveButton />
</div>
);
}
// Client Component - wymaga hydracji
'use client';
import { useState } from 'react';
export function InteractiveButton() {
const [count, setCount] = useState(0);
// PRZED hydracją: button jest w DOM ale onClick nie działa
// PO hydracji: onClick jest aktywny
return (
<button onClick={() => setCount(count + 1)}>
Kliknięć: {count}
</button>
);
}
// Częsty błąd hydracji - różnica między serverem a klientem
export function ProblematicComponent() {
// ❌ ZŁE - Date.now() da różne wartości na serwerze i kliencie
return <div>Timestamp: {Date.now()}</div>;
// ✅ DOBRE - useEffect wykonuje się tylko w przeglądarce
const [timestamp, setTimestamp] = useState<number | null>(null);
useEffect(() => {
setTimestamp(Date.now());
}, []);
return <div>Timestamp: {timestamp ?? 'Loading...'}</div>;
}
sequenceDiagram
participant User
participant Browser
participant Server
participant React
User->>Browser: Otwiera stronę
Browser->>Server: Request
Server->>Server: SSR - renderuje HTML
Server->>Browser: HTML + CSS (FCP)
Note over Browser: Użytkownik widzi content<br/>ale brak interaktywności
Browser->>Server: Pobiera JS bundle
Server->>Browser: JavaScript
Browser->>React: Uruchamia React
React->>React: Hydracja - attachuje event handlers
Note over Browser: TTI - strona interaktywna
Note over User,React: Czas FCP → TTI = "Uncanny Valley"
Materiały
↑ Powrót na góręNext.js - Routing
Jak zaimplementować middleware w Next.js i jakie ma zastosowania?
Odpowiedź w 30 sekund:
Middleware w Next.js to funkcja uruchamiana przed zakończeniem requestu, zdefiniowana w pliku middleware.ts w głównym katalogu projektu. Pozwala na modyfikację odpowiedzi, przekierowania, przepisywanie URL, dodawanie nagłówków czy autoryzację. Działa na Edge Runtime, zapewniając niskie opóźnienia.
Odpowiedź w 2 minuty:
Middleware w Next.js to kod uruchamiany przed przetworzeniem requestu, działający na poziomie Edge Runtime (bliżej użytkownika końcowego). Definiuje się go przez eksport funkcji middleware z pliku middleware.ts umieszczonego w katalogu głównym projektu (obok app/ lub src/). Middleware otrzymuje obiekt NextRequest i może zwrócić NextResponse, modyfikując zachowanie aplikacji przed renderowaniem strony.
Główne zastosowania middleware to: autentykacja i autoryzacja (sprawdzanie tokenów, przekierowanie niezalogowanych użytkowników), internacjonalizacja (wykrywanie języka i przekierowanie), A/B testing i feature flags, logowanie i analityka, manipulacja nagłówkami (CORS, security headers), bot detection i rate limiting, oraz przepisywanie i przekierowania URL.
Middleware może działać na wszystkich trasach lub być ograniczone do określonych ścieżek przez konfigurację matcher lub warunkową logikę w funkcji. Działa przed cache Next.js, co oznacza, że może wpływać na to, czy i jak strony są cache'owane. Jest wykonywany na każdym requescie do pasujących tras, więc powinien być szybki i lekki.
Ważne ograniczenie: middleware działa na Edge Runtime, co oznacza, że nie wszystkie Node.js API są dostępne. Nie można używać natywnych modułów Node.js, systemu plików czy niektórych pakagów npm. Middleware jest idealny do szybkich operacji jak przekierowania, modyfikacja nagłówków czy proste sprawdzenia autentykacji, ale cięższe operacje powinny być w Server Components lub API Routes.
Przykład kodu:
// middleware.ts (w głównym katalogu projektu)
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
// Podstawowy middleware
export function middleware(request: NextRequest) {
// Logowanie requestu
console.log('Request URL:', request.url);
console.log('Request method:', request.method);
// Kontynuuj normalnie
return NextResponse.next();
}
// Konfiguracja - określa, na które trasy middleware reaguje
export const config = {
matcher: [
// Dopasuj wszystkie trasy poza static files, api, _next
'/((?!api|_next/static|_next/image|favicon.ico).*)',
],
};
// Middleware z autentykacją
// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export function middleware(request: NextRequest) {
// Sprawdź, czy użytkownik jest zalogowany (token w cookies)
const token = request.cookies.get('auth-token');
const { pathname } = request.nextUrl;
// Publiczne trasy dostępne dla wszystkich
const publicPaths = ['/', '/login', '/register', '/about'];
const isPublicPath = publicPaths.includes(pathname);
// Jeśli brak tokenu i trasa chroniona, przekieruj do logowania
if (!token && !isPublicPath) {
const loginUrl = new URL('/login', request.url);
loginUrl.searchParams.set('redirect', pathname); // Zachowaj docelowy URL
return NextResponse.redirect(loginUrl);
}
// Jeśli zalogowany próbuje dostać się do /login, przekieruj do dashboard
if (token && pathname === '/login') {
return NextResponse.redirect(new URL('/dashboard', request.url));
}
return NextResponse.next();
}
export const config = {
matcher: [
'/((?!api|_next/static|_next/image|favicon.ico).*)',
],
};
// Middleware z internacjonalizacją (i18n)
// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
const locales = ['en', 'pl', 'de'];
const defaultLocale = 'en';
function getLocale(request: NextRequest): string {
// 1. Sprawdź cookie
const cookieLocale = request.cookies.get('locale')?.value;
if (cookieLocale && locales.includes(cookieLocale)) {
return cookieLocale;
}
// 2. Sprawdź Accept-Language header
const acceptLanguage = request.headers.get('accept-language');
if (acceptLanguage) {
const browserLocale = acceptLanguage.split(',')[0].split('-')[0];
if (locales.includes(browserLocale)) {
return browserLocale;
}
}
return defaultLocale;
}
export function middleware(request: NextRequest) {
const { pathname } = request.nextUrl;
// Sprawdź czy ścieżka już zawiera locale
const pathnameHasLocale = locales.some(
locale => pathname.startsWith(`/${locale}/`) || pathname === `/${locale}`
);
if (pathnameHasLocale) return NextResponse.next();
// Dodaj locale do URL
const locale = getLocale(request);
request.nextUrl.pathname = `/${locale}${pathname}`;
return NextResponse.redirect(request.nextUrl);
}
export const config = {
matcher: ['/((?!api|_next|.*\\..*).*)'],
};
// A/B Testing middleware
// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export function middleware(request: NextRequest) {
const { pathname } = request.nextUrl;
// Tylko dla strony głównej
if (pathname === '/') {
// Sprawdź czy użytkownik już ma przypisany wariant
let variant = request.cookies.get('ab-test-variant')?.value;
if (!variant) {
// Losowo przypisz wariant (50/50)
variant = Math.random() < 0.5 ? 'A' : 'B';
}
// Przepisz URL na odpowiedni wariant
const response = variant === 'B'
? NextResponse.rewrite(new URL('/variant-b', request.url))
: NextResponse.next();
// Ustaw cookie z wariantem
response.cookies.set('ab-test-variant', variant, {
maxAge: 60 * 60 * 24 * 30, // 30 dni
});
return response;
}
return NextResponse.next();
}
// Dodawanie security headers
// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export function middleware(request: NextRequest) {
const response = NextResponse.next();
// Dodaj security headers
response.headers.set('X-Frame-Options', 'DENY');
response.headers.set('X-Content-Type-Options', 'nosniff');
response.headers.set('Referrer-Policy', 'strict-origin-when-cross-origin');
response.headers.set(
'Content-Security-Policy',
"default-src 'self'; script-src 'self' 'unsafe-inline'"
);
return response;
}
// Rate limiting (prosty przykład)
// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
// W produkcji użyj Redis lub innego store
const rateLimitMap = new Map<string, { count: number; resetTime: number }>();
export function middleware(request: NextRequest) {
const ip = request.ip || 'unknown';
const now = Date.now();
const limit = 100; // Maksymalnie 100 requestów
const window = 60 * 1000; // W ciągu 1 minuty
const clientData = rateLimitMap.get(ip);
if (!clientData || now > clientData.resetTime) {
// Pierwszy request lub okno się zresetowało
rateLimitMap.set(ip, {
count: 1,
resetTime: now + window,
});
return NextResponse.next();
}
if (clientData.count >= limit) {
// Przekroczono limit
return new NextResponse('Too Many Requests', {
status: 429,
headers: {
'Retry-After': String(Math.ceil((clientData.resetTime - now) / 1000)),
},
});
}
// Zwiększ licznik
clientData.count += 1;
return NextResponse.next();
}
// Zaawansowany matcher config
export const config = {
matcher: [
// Dopasuj określone trasy
'/dashboard/:path*',
'/api/:path*',
// Wyklucz static assets
'/((?!_next/static|_next/image|favicon.ico).*)',
],
};
// Conditional middleware z wieloma funkcjami
// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
function authMiddleware(request: NextRequest) {
// Logika autentykacji
const token = request.cookies.get('token');
if (!token) {
return NextResponse.redirect(new URL('/login', request.url));
}
}
function loggingMiddleware(request: NextRequest) {
// Logowanie
console.log(`${request.method} ${request.url}`);
}
export function middleware(request: NextRequest) {
const { pathname } = request.nextUrl;
// Zawsze loguj
loggingMiddleware(request);
// Autentykacja tylko dla /dashboard
if (pathname.startsWith('/dashboard')) {
const authResponse = authMiddleware(request);
if (authResponse) return authResponse;
}
return NextResponse.next();
}
Materiały
↑ Powrót na góręJak działa system routingu oparty na plikach w App Router?
Odpowiedź w 30 sekund:
App Router w Next.js 13+ wykorzystuje strukturę folderów w katalogu app/ do definiowania tras. Każdy folder reprezentuje segment URL, a specjalne pliki jak page.tsx, layout.tsx czy route.ts określają zachowanie danej trasy. System automatycznie tworzy routing na podstawie hierarchii katalogów.
Odpowiedź w 2 minuty:
App Router to nowy system routingu wprowadzony w Next.js 13, który zastępuje poprzedni Pages Router. Opiera się na konwencji, gdzie struktura folderów w katalogu app/ bezpośrednio odpowiada strukturze URL aplikacji. Każdy folder w hierarchii reprezentuje segment ścieżki URL, tworząc intuicyjny i łatwy do zrozumienia system organizacji kodu.
Kluczowe pliki w systemie App Router to: page.tsx (definiuje UI dla konkretnej trasy i czyni ją publicznie dostępną), layout.tsx (wspólny szablon dla wielu stron), loading.tsx (UI stanu ładowania), error.tsx (obsługa błędów), oraz route.ts (API endpoints). System obsługuje także zagnieżdżone layouty, równoległe renderowanie i zaawansowane wzorce routingu.
App Router wykorzystuje React Server Components jako domyślne, co pozwala na renderowanie komponentów po stronie serwera, zmniejszając rozmiar bundle'a JavaScript wysyłanego do klienta. Nawigacja między trasami wykorzystuje preloadowanie i cache'owanie, zapewniając płynne przejścia podobne do SPA, zachowując jednocześnie korzyści renderowania po stronie serwera.
Routing jest natychmiastowy dzięki cache'owaniu po stronie klienta i prefetching - Next.js automatycznie wczytuje kod tras widocznych w viewport. Dodatkowo, stan aplikacji jest zachowywany podczas nawigacji, a odświeżane są tylko zmieniające się segmenty.
Przykład kodu:
// Struktura folderów w app/
// app/
// ├── page.tsx -> /
// ├── about/
// │ └── page.tsx -> /about
// ├── blog/
// │ ├── page.tsx -> /blog
// │ ├── layout.tsx -> Layout dla /blog/*
// │ └── [slug]/
// │ └── page.tsx -> /blog/:slug
// └── dashboard/
// ├── layout.tsx
// ├── page.tsx -> /dashboard
// └── settings/
// └── page.tsx -> /dashboard/settings
// app/page.tsx - Strona główna (/)
export default function HomePage() {
return <h1>Strona główna</h1>;
}
// app/blog/page.tsx - Lista postów (/blog)
export default function BlogPage() {
return <h1>Blog</h1>;
}
// app/blog/[slug]/page.tsx - Pojedynczy post (/blog/moj-post)
export default function BlogPost({ params }: { params: { slug: string } }) {
return <h1>Post: {params.slug}</h1>;
}
// app/blog/layout.tsx - Wspólny layout dla wszystkich stron /blog/*
export default function BlogLayout({ children }: { children: React.ReactNode }) {
return (
<div>
<nav>Blog Navigation</nav>
{children}
</div>
);
}
Materiały
↑ Powrót na góręCzym są segmenty dynamiczne i jak je definiować w Next.js?
Odpowiedź w 30 sekund:
Segmenty dynamiczne to parametry w URL, które pozwalają tworzyć trasy z zmiennymi wartościami. Definiuje się je poprzez umieszczenie nazwy folderu w nawiasach kwadratowych, np. [id] lub [slug]. Wartości tych parametrów są dostępne w komponencie przez obiekt params.
Odpowiedź w 2 minuty:
Segmenty dynamiczne umożliwiają tworzenie elastycznych tras, które mogą obsługiwać różne wartości parametrów URL. Zamiast tworzyć osobną trasę dla każdego produktu, użytkownika czy posta, można utworzyć jedną trasę dynamiczną, która obsłuży wszystkie przypadki. W App Router definiuje się je przez nazwanie folderu w konwencji [parametr].
Next.js obsługuje kilka typów segmentów dynamicznych: pojedyncze segmenty ([id]), catch-all segmenty ([...slug]), które przechwytują wszystkie następujące segmenty URL, oraz opcjonalne catch-all segmenty ([[...slug]]), które działają jak catch-all, ale trasa działa także bez parametru. Wartości parametrów są automatycznie przekazywane do komponentów przez właściwość params.
Segmenty dynamiczne są szczególnie przydatne przy tworzeniu stron produktów, profili użytkowników, postów blogowych czy dowolnych treści generowanych dynamicznie. Można je łączyć ze statycznym generowaniem stron (SSG) używając funkcji generateStaticParams, co pozwala na pre-renderowanie określonych tras w czasie budowania aplikacji.
Next.js parsuje parametry URL i udostępnia je jako obiekt. Można mieć wiele segmentów dynamicznych w jednej ścieżce, a także łączyć segmenty statyczne z dynamicznymi, tworząc zaawansowane wzorce routingu.
Przykład kodu:
// Pojedynczy segment dynamiczny
// app/products/[id]/page.tsx -> /products/123
export default function ProductPage({ params }: { params: { id: string } }) {
return <h1>Produkt ID: {params.id}</h1>;
}
// Wiele segmentów dynamicznych
// app/shop/[category]/[product]/page.tsx -> /shop/electronics/laptop
export default function ProductDetailPage({
params
}: {
params: { category: string; product: string }
}) {
return (
<div>
<h1>Kategoria: {params.category}</h1>
<h2>Produkt: {params.product}</h2>
</div>
);
}
// Catch-all segment (przechwytuje wszystkie segmenty)
// app/docs/[...slug]/page.tsx
// Obsługuje: /docs/a, /docs/a/b, /docs/a/b/c, itd.
// NIE obsługuje: /docs
export default function DocsPage({ params }: { params: { slug: string[] } }) {
// params.slug będzie tablicą: ['a', 'b', 'c']
return <h1>Dokumentacja: {params.slug.join('/')}</h1>;
}
// Opcjonalny catch-all segment
// app/blog/[[...slug]]/page.tsx
// Obsługuje: /blog, /blog/2023, /blog/2023/12, /blog/2023/12/post
export default function BlogPage({
params
}: {
params: { slug?: string[] }
}) {
if (!params.slug) {
return <h1>Wszystkie posty</h1>;
}
return <h1>Blog: {params.slug.join('/')}</h1>;
}
// Generowanie statycznych ścieżek w czasie budowania
// app/products/[id]/page.tsx
export async function generateStaticParams() {
// Pobierz listę produktów z API lub bazy danych
const products = await fetch('https://api.example.com/products').then(res => res.json());
// Zwróć tablicę obiektów z parametrami
return products.map((product: any) => ({
id: product.id.toString(),
}));
}
// TypeScript: Definiowanie typu dla props
type PageProps = {
params: { id: string };
searchParams: { [key: string]: string | string[] | undefined };
};
export default async function Page({ params, searchParams }: PageProps) {
// params.id - parametr z URL
// searchParams - query parameters (?sort=asc)
return <div>Produkt: {params.id}</div>;
}
Materiały
↑ Powrót na góręJak zaimplementować zagnieżdżone layouty w App Router?
Odpowiedź w 30 sekund:
Zagnieżdżone layouty implementuje się przez tworzenie plików layout.tsx na różnych poziomach hierarchii folderów. Każdy layout opakowuje swoje komponenty potomne, a layouty są automatycznie komponowane - zewnętrzny layout zawiera wewnętrzny, tworząc hierarchię szablonów.
Odpowiedź w 2 minuty:
Zagnieżdżone layouty to potężna funkcja App Router, która pozwala tworzyć hierarchiczne struktury szablonów. Każdy folder w hierarchii może mieć własny plik layout.tsx, który definiuje UI wspólny dla wszystkich tras w tym folderze i jego podfolderach. Layouty są automatycznie zagnieżdżane - layout nadrzędny owija layout potomny, który z kolei owija komponent strony.
Główna zaleta zagnieżdżonych layoutów to możliwość zachowania stanu między nawigacją - layout nie jest re-renderowany, gdy użytkownik przechodzi między stronami w tym samym layoutcie. To oznacza, że stan komponentów, pozycja scroll czy dane wejściowe są zachowywane. Ponadto, tylko zmieniające się części UI są aktualizowane, co znacznie poprawia wydajność.
Layouty mogą zawierać wspólną logikę, nawigację, sidebary, nagłówki czy stopki specyficzne dla danej sekcji aplikacji. Można też definiować różne metadane dla różnych sekcji strony. Root layout (app/layout.tsx) jest wymagany i musi zawierać tagi <html> i <body>.
W praktyce można mieć layout dla całej aplikacji (autentykacja, główna nawigacja), layout dla dashboardu (sidebar, breadcrumbs), layout dla sekcji bloga (kategorie, tagi) i tak dalej. Każdy poziom dodaje swoje elementy UI, tworząc kompletną strukturę strony.
Przykład kodu:
// app/layout.tsx - Root layout (wymagany)
// Opakowuje całą aplikację
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="pl">
<body>
<header>
<nav>Główna nawigacja</nav>
</header>
{children}
<footer>Stopka</footer>
</body>
</html>
);
}
// app/dashboard/layout.tsx - Layout dla sekcji dashboard
// Opakowuje wszystkie trasy /dashboard/*
export default function DashboardLayout({ children }: { children: React.ReactNode }) {
return (
<div className="dashboard-container">
<aside className="sidebar">
<nav>
<a href="/dashboard">Przegląd</a>
<a href="/dashboard/analytics">Analityka</a>
<a href="/dashboard/settings">Ustawienia</a>
</nav>
</aside>
<main className="content">
{children}
</main>
</div>
);
}
// app/dashboard/analytics/layout.tsx - Zagnieżdżony layout dla analityki
// Opakowuje wszystkie trasy /dashboard/analytics/*
export default function AnalyticsLayout({ children }: { children: React.ReactNode }) {
return (
<div className="analytics-container">
<div className="analytics-header">
<h1>Analityka</h1>
<nav className="tabs">
<a href="/dashboard/analytics/overview">Przegląd</a>
<a href="/dashboard/analytics/reports">Raporty</a>
<a href="/dashboard/analytics/exports">Eksporty</a>
</nav>
</div>
<div className="analytics-content">
{children}
</div>
</div>
);
}
// app/dashboard/analytics/reports/page.tsx - Strona
// Ostateczna struktura DOM:
// RootLayout -> DashboardLayout -> AnalyticsLayout -> ReportsPage
export default function ReportsPage() {
return <div>Zawartość raportów</div>;
}
// Struktura wizualna dla /dashboard/analytics/reports:
// ┌─────────────────────────────────────┐
// │ Header + Nav (RootLayout) │
// ├─────────────┬───────────────────────┤
// │ Sidebar │ Analytics Header │
// │ (Dashboard │ (AnalyticsLayout) │
// │ Layout) ├───────────────────────┤
// │ │ Reports Content │
// │ │ (ReportsPage) │
// │ │ │
// └─────────────┴───────────────────────┘
// │ Footer (RootLayout) │
// └─────────────────────────────────────┘
// Przykład z metadanymi i stanami
// app/blog/layout.tsx
import { Metadata } from 'next';
export const metadata: Metadata = {
title: {
default: 'Blog',
template: '%s | Blog', // Strony potomne mogą nadpisać
},
};
export default function BlogLayout({ children }: { children: React.ReactNode }) {
// Ten stan będzie zachowany podczas nawigacji między postami
const [sidebarOpen, setSidebarOpen] = React.useState(true);
return (
<div className="blog-layout">
<button onClick={() => setSidebarOpen(!sidebarOpen)}>
Toggle Sidebar
</button>
{sidebarOpen && (
<aside>
<h3>Kategorie</h3>
{/* Kategorie blogowe */}
</aside>
)}
<main>{children}</main>
</div>
);
}
// Przykład z Template (alternatywa dla Layout)
// Template RE-RENDERUJE się przy każdej nawigacji
// app/dashboard/template.tsx
export default function DashboardTemplate({ children }: { children: React.ReactNode }) {
// Ta animacja będzie uruchamiana przy każdej zmianie strony
return <div className="fade-in">{children}</div>;
}
Materiały
↑ Powrót na góręPobieranie Danych w Next.js
Jak obsługiwać równoległe zapytania o dane w Server Components?
Odpowiedź w 30 sekund:
W Server Components używaj Promise.all() lub Promise.allSettled() do wykonywania równoległych zapytań, co znacznie redukuje całkowity czas ładowania. Next.js automatycznie deduplikuje identyczne zapytania fetch i wykonuje je równolegle gdy tylko jest to możliwe. Możesz też użyć wzorca preload do inicjowania zapytań wcześniej i użycia wyników później.
Odpowiedź w 2 minuty:
Równoległe pobieranie danych jest kluczowe dla wydajności aplikacji Next.js. W Server Components, zamiast czekać na zakończenie każdego zapytania sekwencyjnie (co tworzy "waterfall" effect), możesz inicjować wszystkie zapytania jednocześnie używając Promise.all(). Next.js automatycznie wykona je równolegle, co drastycznie redukuje całkowity czas ładowania - np. trzy zapytania po 1 sekundzie każde wykonają się w ~1 sekundę zamiast 3 sekund.
Next.js automatycznie deduplikuje identyczne zapytania fetch w ramach jednego cyklu renderowania (Request Memoization), więc jeśli ten sam URL jest pobierany w różnych komponentach, faktycznie wykona się tylko jedno zapytanie. To pozwala na kolokalność danych - każdy komponent może fetch'ować własne dane bez obaw o duplikację.
Dla bardziej zaawansowanych przypadków, możesz użyć wzorca "preload" - stwórz funkcję która inicjuje zapytanie i zapisuje Promise, a następnie wywołaj ją wcześnie (np. przed renderowaniem) i użyj await później gdy dane są potrzebne. To pozwala na jeszcze wcześniejsze rozpoczęcie pobierania danych.
Warto używać Promise.allSettled() zamiast Promise.all() gdy niektóre zapytania mogą zawieść - pozwala to na częściowe renderowanie strony nawet jeśli część danych nie jest dostępna. Możesz też komponować zagnieżdżone komponenty tak, aby każdy start'ował własne zapytania równolegle, a Next.js automatycznie optymalizuje streaming HTML w miarę jak dane stają się dostępne.
Przykład kodu:
// 1. Podstawowe równoległe zapytania z Promise.all()
async function DashboardPage() {
// ŹLE: Sekwencyjne zapytania - każde czeka na poprzednie
// const user = await fetch('/api/user').then(r => r.json());
// const posts = await fetch('/api/posts').then(r => r.json());
// const stats = await fetch('/api/stats').then(r => r.json());
// Czas: ~3 sekundy (1s + 1s + 1s)
// DOBRZE: Równoległe zapytania
const [user, posts, stats] = await Promise.all([
fetch('/api/user').then(r => r.json()),
fetch('/api/posts').then(r => r.json()),
fetch('/api/stats').then(r => r.json()),
]);
// Czas: ~1 sekunda (wszystkie równolegle)
return (
<div>
<UserProfile user={user} />
<PostsList posts={posts} />
<Statistics stats={stats} />
</div>
);
}
// 2. Obsługa błędów z Promise.allSettled()
async function ProductPage({ params }: { params: { id: string } }) {
const results = await Promise.allSettled([
fetch(`/api/products/${params.id}`).then(r => r.json()),
fetch(`/api/products/${params.id}/reviews`).then(r => r.json()),
fetch(`/api/products/${params.id}/related`).then(r => r.json()),
]);
// Wyciągnij dane lub użyj fallback jeśli zapytanie się nie powiodło
const product = results[0].status === 'fulfilled' ? results[0].value : null;
const reviews = results[1].status === 'fulfilled' ? results[1].value : [];
const related = results[2].status === 'fulfilled' ? results[2].value : [];
if (!product) {
return <div>Nie można załadować produktu</div>;
}
return (
<div>
<ProductDetails product={product} />
{reviews.length > 0 && <ReviewsList reviews={reviews} />}
{related.length > 0 && <RelatedProducts products={related} />}
</div>
);
}
// 3. Wzorzec preload - wcześniejsze rozpoczęcie pobierania
// lib/data.ts
const preloadedData = new Map();
export function preloadUser(id: string) {
// Rozpocznij zapytanie i zapisz Promise
if (!preloadedData.has(`user-${id}`)) {
const promise = fetch(`/api/users/${id}`).then(r => r.json());
preloadedData.set(`user-${id}`, promise);
}
}
export async function getUser(id: string) {
// Użyj preloaded Promise jeśli istnieje
const promise = preloadedData.get(`user-${id}`) ||
fetch(`/api/users/${id}`).then(r => r.json());
return promise;
}
// app/users/[id]/page.tsx
import { preloadUser, getUser } from '@/lib/data';
export async function generateMetadata({ params }: { params: { id: string } }) {
// Preload w generateMetadata
preloadUser(params.id);
const user = await getUser(params.id);
return { title: user.name };
}
export default async function UserPage({ params }: { params: { id: string } }) {
// Zapytanie już się rozpoczęło w generateMetadata!
const user = await getUser(params.id);
return <UserProfile user={user} />;
}
// 4. Komponenty zagnieżdżone z równoległym fetchowaniem
async function ParentComponent() {
// Każdy child rozpocznie własne zapytania równolegle
return (
<div>
<Suspense fallback={<Skeleton />}>
<UserInfo /> {/* Fetch użytkownika */}
</Suspense>
<Suspense fallback={<Skeleton />}>
<RecentPosts /> {/* Fetch postów */}
</Suspense>
<Suspense fallback={<Skeleton />}>
<ActivityFeed /> {/* Fetch aktywności */}
</Suspense>
</div>
);
}
// Komponenty fetchują równolegle dzięki Suspense
async function UserInfo() {
const user = await fetch('/api/user').then(r => r.json());
return <div>{user.name}</div>;
}
async function RecentPosts() {
const posts = await fetch('/api/posts').then(r => r.json());
return <PostsList posts={posts} />;
}
async function ActivityFeed() {
const activity = await fetch('/api/activity').then(r => r.json());
return <Activity data={activity} />;
}
// 5. Zaawansowany przykład z zależnościami i równoległością
async function ComplexPage({ params }: { params: { id: string } }) {
// Krok 1: Pobierz dane podstawowe równolegle
const [user, settings] = await Promise.all([
fetch(`/api/users/${params.id}`).then(r => r.json()),
fetch(`/api/settings`).then(r => r.json()),
]);
// Krok 2: Na podstawie user, pobierz powiązane dane równolegle
const [posts, followers, following] = await Promise.all([
fetch(`/api/users/${user.id}/posts`).then(r => r.json()),
fetch(`/api/users/${user.id}/followers`).then(r => r.json()),
fetch(`/api/users/${user.id}/following`).then(r => r.json()),
]);
return (
<div>
<UserHeader user={user} settings={settings} />
<UserStats
postsCount={posts.length}
followersCount={followers.length}
followingCount={following.length}
/>
<UserContent posts={posts} />
</div>
);
}
// 6. Automatyczna deduplikacja Next.js
async function ProductCard({ id }: { id: string }) {
// To zapytanie będzie deduplikowane jeśli ten sam produkt
// jest renderowany wielokrotnie na stronie
const product = await fetch(`/api/products/${id}`).then(r => r.json());
return <div>{product.name}</div>;
}
async function ProductList() {
const productIds = ['1', '2', '3', '1', '2']; // Duplikaty!
// Pomimo duplikatów, każde unikalne ID będzie fetch'owane tylko raz
return (
<div>
{productIds.map(id => (
<ProductCard key={id} id={id} />
))}
</div>
);
}
Materiały
↑ Powrót na góręMetadata i SEO w Next.js
31. Jak zaimplementować canonical URLs i hreflang w Next.js?
Odpowiedź w 30 sekund:
Canonical URLs i hreflang definiuje się w obiekcie metadata używając właściwości alternates. Dla canonical URL używamy alternates.canonical, a dla wersji językowych alternates.languages. Next.js automatycznie generuje odpowiednie tagi <link rel="canonical"> i <link rel="alternate" hreflang="..."> w sekcji <head>.
Odpowiedź w 2 minuty:
Implementacja canonical URLs i hreflang w Next.js App Router odbywa się poprzez konfigurację metadata, co zapewnia typebezpieczeństwo i automatyczne generowanie poprawnych tagów HTML. Canonical URL służy do wskazania preferowanej wersji strony (przydatne przy duplikacji treści, parametrach URL czy paginacji) i definiuje się go w alternates.canonical. Next.js automatycznie dodaje tag <link rel="canonical" href="...">.
Hreflang to mechanizm informujący wyszukiwarki o wersjach językowych i regionalnych strony. Definiuje się go w alternates.languages jako obiekt, gdzie klucze to kody językowe (np. 'en-US', 'pl-PL', 'x-default') a wartości to URL-e. Next.js generuje odpowiednie tagi <link rel="alternate" hreflang="..." href="..."> dla każdej wersji. Kluczowe jest używanie konsystentnych URL-i i implementacja dwukierunkowa - każda wersja językowa powinna wskazywać na wszystkie inne wersje.
System wspiera również alternates dla różnych typów mediów (types) oraz URLs dla aplikacji mobilnych (ios, android). Można łączyć canonical z hreflang - canonical wskazuje preferowaną wersję w danym języku, a hreflang wszystkie dostępne języki. Dla dynamicznych stron używamy generateMetadata do konstruowania odpowiednich URL-i na podstawie parametrów trasy.
Przykład kodu:
// app/layout.tsx - Globalne canonical i hreflang
import { Metadata } from 'next';
const BASE_URL = 'https://example.com';
export const metadata: Metadata = {
alternates: {
canonical: BASE_URL,
languages: {
'en-US': `${BASE_URL}/en`,
'pl-PL': `${BASE_URL}/pl`,
'de-DE': `${BASE_URL}/de`,
'x-default': `${BASE_URL}/en` // Domyślna wersja językowa
}
}
};
// app/[lang]/page.tsx - Dynamiczny canonical i hreflang dla stron językowych
import { Metadata } from 'next';
const BASE_URL = 'https://example.com';
const languages = ['en', 'pl', 'de', 'fr', 'es'];
interface PageProps {
params: { lang: string };
}
export async function generateStaticParams() {
return languages.map((lang) => ({ lang }));
}
export async function generateMetadata({ params }: PageProps): Promise<Metadata> {
const { lang } = params;
// Mapa kodów języków do pełnych locale codes
const localeMap: Record<string, string> = {
en: 'en-US',
pl: 'pl-PL',
de: 'de-DE',
fr: 'fr-FR',
es: 'es-ES'
};
// Generuj obiekty hreflang dla wszystkich języków
const languageAlternates = languages.reduce((acc, l) => {
acc[localeMap[l]] = `${BASE_URL}/${l}`;
return acc;
}, {} as Record<string, string>);
// Dodaj x-default
languageAlternates['x-default'] = `${BASE_URL}/en`;
return {
alternates: {
canonical: `${BASE_URL}/${lang}`,
languages: languageAlternates
}
};
}
// app/[lang]/blog/[slug]/page.tsx - Canonical i hreflang dla dynamicznych postów
import { Metadata } from 'next';
const BASE_URL = 'https://example.com';
interface BlogPostProps {
params: {
lang: string;
slug: string;
};
}
export async function generateMetadata({ params }: BlogPostProps): Promise<Metadata> {
const { lang, slug } = params;
// Pobierz dostępne tłumaczenia tego posta
const post = await fetch(`https://api.example.com/posts/${slug}?lang=${lang}`)
.then((res) => res.json());
// Buduj hreflang na podstawie dostępnych tłumaczeń
const languageAlternates: Record<string, string> = {};
if (post.translations) {
post.translations.forEach((translation: any) => {
languageAlternates[translation.locale] =
`${BASE_URL}/${translation.lang}/blog/${translation.slug}`;
});
}
// Dodaj x-default (zazwyczaj wersja angielska)
const defaultTranslation = post.translations?.find((t: any) => t.lang === 'en');
if (defaultTranslation) {
languageAlternates['x-default'] =
`${BASE_URL}/en/blog/${defaultTranslation.slug}`;
}
return {
title: post.title,
description: post.excerpt,
alternates: {
// Canonical wskazuje na bieżącą wersję językową
canonical: `${BASE_URL}/${lang}/blog/${slug}`,
// Hreflang wskazuje wszystkie dostępne wersje
languages: languageAlternates
},
openGraph: {
url: `${BASE_URL}/${lang}/blog/${slug}`
}
};
}
// app/products/page.tsx - Canonical dla stron z paginacją
interface ProductsPageProps {
searchParams: { page?: string; sort?: string };
}
export async function generateMetadata({
searchParams
}: ProductsPageProps): Promise<Metadata> {
const page = searchParams.page || '1';
const BASE_URL = 'https://example.com';
// Canonical zawsze wskazuje na "czystą" wersję bez parametrów sortowania
// ale z numerem strony dla paginacji
const canonicalUrl = page === '1'
? `${BASE_URL}/products`
: `${BASE_URL}/products?page=${page}`;
return {
alternates: {
canonical: canonicalUrl
},
robots: {
// Opcjonalnie: no-index dla stron > 1
index: page === '1',
follow: true
}
};
}
// middleware.ts - Automatyczne przekierowanie na właściwą wersję językową
import { NextRequest, NextResponse } from 'next/server';
const locales = ['en', 'pl', 'de', 'fr'];
const defaultLocale = 'en';
export function middleware(request: NextRequest) {
const { pathname } = request.nextUrl;
// Sprawdź czy URL zawiera już locale
const pathnameHasLocale = locales.some(
(locale) => pathname.startsWith(`/${locale}/`) || pathname === `/${locale}`
);
if (pathnameHasLocale) return;
// Wykryj preferowany język z Accept-Language header
const acceptLanguage = request.headers.get('accept-language');
const preferredLocale = acceptLanguage
?.split(',')[0]
?.split('-')[0];
const locale = locales.includes(preferredLocale || '')
? preferredLocale
: defaultLocale;
// Przekieruj na wersję z locale
request.nextUrl.pathname = `/${locale}${pathname}`;
return NextResponse.redirect(request.nextUrl);
}
export const config = {
matcher: [
// Pomiń pliki wewnętrzne Next.js i statyczne
'/((?!_next|api|favicon.ico|.*\\..*).*)'
]
};
// Przykład wygenerowanych tagów HTML:
/*
<head>
<!-- Canonical URL -->
<link rel="canonical" href="https://example.com/pl/blog/nextjs-tutorial" />
<!-- Hreflang tags -->
<link rel="alternate" hreflang="en-US" href="https://example.com/en/blog/nextjs-tutorial" />
<link rel="alternate" hreflang="pl-PL" href="https://example.com/pl/blog/nextjs-tutorial" />
<link rel="alternate" hreflang="de-DE" href="https://example.com/de/blog/nextjs-tutorial" />
<link rel="alternate" hreflang="x-default" href="https://example.com/en/blog/nextjs-tutorial" />
<!-- Open Graph URL -->
<meta property="og:url" content="https://example.com/pl/blog/nextjs-tutorial" />
</head>
*/
Materiały
- Next.js Metadata alternates Documentation
- Google Hreflang Guidelines
- Canonical URL Best Practices
- Next.js Internationalization
Renderowanie w Next.js
13. Jakie strategie renderowania oferuje Next.js (SSR, SSG, ISR)?
Odpowiedź w 30 sekund: Next.js oferuje cztery główne strategie renderowania: Static Site Generation (SSG) - pre-renderowanie w czasie build, Server-Side Rendering (SSR) - renderowanie na każde żądanie, Incremental Static Regeneration (ISR) - aktualizacja statycznych stron w tle, oraz Client-Side Rendering (CSR) - renderowanie w przeglądarce. Każda strategia ma swoje zastosowania w zależności od wymagań dotyczących wydajności i świeżości danych.
Odpowiedź w 2 minuty: Next.js zapewnia elastyczny system renderowania, pozwalający wybrać najbardziej odpowiednią strategię dla każdej strony:
Static Site Generation (SSG) generuje HTML w czasie build. Strony są serwowane jako statyczne pliki, co zapewnia najlepszą wydajność. Idealny dla treści, które rzadko się zmieniają, jak blogi czy strony landingowe. W App Router używamy tego domyślnie, a w Pages Router przez getStaticProps.
Server-Side Rendering (SSR) renderuje HTML na każde żądanie użytkownika. Zapewnia zawsze aktualne dane, ale jest wolniejszy niż SSG. Używany dla spersonalizowanych treści lub szybko zmieniających się danych. W App Router osiągamy to przez fetch z cache: 'no-store', w Pages Router przez getServerSideProps.
Incremental Static Regeneration (ISR) łączy zalety SSG i SSR - strona jest statyczna, ale aktualizowana w tle w określonych odstępach czasu. Idealny dla treści, które zmieniają się regularnie, ale nie wymagają aktualizacji w czasie rzeczywistym.
Client-Side Rendering (CSR) renderuje zawartość w przeglądarce przy użyciu JavaScript. Używany dla interaktywnych części aplikacji lub dashboardów wymagających danych użytkownika.
Przykład kodu:
// App Router - różne strategie renderowania
// 1. SSG (domyślnie) - dane cache'owane
async function BlogPost({ params }) {
const post = await fetch(`https://api.example.com/posts/${params.id}`)
.then(res => res.json());
return <article>{post.title}</article>;
}
// 2. SSR - dane zawsze świeże
async function Dashboard() {
const data = await fetch('https://api.example.com/user', {
cache: 'no-store' // Wymusza SSR
}).then(res => res.json());
return <div>{data.name}</div>;
}
// 3. ISR - rewalidacja co 60 sekund
async function Products() {
const products = await fetch('https://api.example.com/products', {
next: { revalidate: 60 } // ISR z rewalidacją
}).then(res => res.json());
return <div>{products.map(p => p.name)}</div>;
}
// 4. CSR - renderowanie po stronie klienta
'use client';
import { useState, useEffect } from 'react';
function UserProfile() {
const [user, setUser] = useState(null);
useEffect(() => {
fetch('/api/user')
.then(res => res.json())
.then(setUser);
}, []);
return user ? <div>{user.name}</div> : <div>Ładowanie...</div>;
}
// Pages Router - klasyczne podejście
// SSG z getStaticProps
export async function getStaticProps() {
const data = await fetch('https://api.example.com/data').then(r => r.json());
return {
props: { data },
// revalidate: 60 // Dodanie tego tworzy ISR
};
}
// SSR z getServerSideProps
export async function getServerSideProps(context) {
const { req } = context;
const data = await fetch('https://api.example.com/user', {
headers: { cookie: req.headers.cookie }
}).then(r => r.json());
return {
props: { data }
};
}
// ISR - SSG + revalidate
export async function getStaticProps() {
const posts = await fetch('https://api.example.com/posts').then(r => r.json());
return {
props: { posts },
revalidate: 3600 // Rewalidacja co godzinę
};
}
Materiały
↑ Powrót na góręNext.js - Optymalizacja
Jak działa komponent next/image i jakie oferuje optymalizacje?
Odpowiedź w 30 sekund:
Komponent next/image to zaawansowany wrapper na znacznik <img>, który automatycznie optymalizuje obrazy poprzez lazy loading, generowanie responsywnych rozmiarów, konwersję do nowoczesnych formatów (WebP, AVIF) oraz optymalizację rozmiaru. Obrazy są serwowane w odpowiednich wymiarach dla każdego urządzenia, co znacząco poprawia wydajność i Core Web Vitals.
Odpowiedź w 2 minuty:
Komponent next/image automatyzuje najlepsze praktyki związane z optymalizacją obrazów. Domyślnie włącza lazy loading - obrazy ładują się tylko gdy zbliżają się do viewport, co oszczędza pasmo i przyspiesza początkowe ładowanie strony. System automatycznie generuje wiele wersji obrazu w różnych rozmiarach (srcset) dostosowanych do urządzeń użytkowników.
Next.js konwertuje obrazy do nowoczesnych formatów jak WebP czy AVIF (jeśli przeglądarka wspiera), które oferują lepszą kompresję niż tradycyjne JPEG czy PNG. Optymalizacja dzieje się on-demand przy pierwszym żądaniu obrazu, a następnie wynik jest cache'owany. Komponent automatycznie rezerwuje miejsce na obraz (jeśli podane wymiary), eliminując Cumulative Layout Shift (CLS).
Możesz używać obrazów z lokalnego systemu plików (z katalogu public/) lub z zewnętrznych źródeł. Dla zewnętrznych domen musisz je skonfigurować w next.config.js ze względów bezpieczeństwa. Komponent obsługuje różne tryby wypełnienia (fill, responsive, intrinsic) i priorytety ładowania dla obrazów above-the-fold.
Dodatkowe optymalizacje obejmują automatyczne wykrywanie rozmiaru obrazu dla lokalnych plików, blur placeholder podczas ładowania oraz inteligentne zarządzanie qualitym kompresji. To wszystko działa out-of-the-box bez dodatkowej konfiguracji, co czyni go kluczowym narzędziem do poprawy LCP (Largest Contentful Paint).
Przykład kodu:
import Image from 'next/image';
// Podstawowe użycie z lokalnym obrazem
export default function Hero() {
return (
<div className="hero">
{/* Obraz z katalogu public/ - automatyczna optymalizacja */}
<Image
src="/hero-image.jpg"
alt="Strona główna"
width={1200}
height={600}
priority // Ładuj natychmiast dla obrazów above-the-fold
placeholder="blur"
blurDataURL="data:image/jpeg;base64,..." // Opcjonalny blur podczas ładowania
/>
</div>
);
}
// Responsywny obraz wypełniający kontener
function ResponsiveImage() {
return (
<div style={{ position: 'relative', width: '100%', height: '400px' }}>
<Image
src="/landscape.jpg"
alt="Krajobraz"
fill
style={{ objectFit: 'cover' }}
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
/>
</div>
);
}
// Obraz z zewnętrznego źródła
function ExternalImage() {
return (
<Image
src="https://example.com/photo.jpg"
alt="Zdjęcie zewnętrzne"
width={800}
height={600}
quality={85} // Kontrola jakości kompresji (1-100)
/>
);
}
// Konfiguracja w next.config.js dla zewnętrznych obrazów
module.exports = {
images: {
remotePatterns: [
{
protocol: 'https',
hostname: 'example.com',
port: '',
pathname: '/images/**',
},
],
formats: ['image/avif', 'image/webp'], // Preferowane formaty
deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840],
imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],
},
};
// Lista produktów z optymalizowanymi miniaturami
function ProductGrid({ products }) {
return (
<div className="grid">
{products.map((product) => (
<div key={product.id} className="product-card">
<Image
src={product.imageUrl}
alt={product.name}
width={300}
height={300}
loading="lazy" // Domyślne, można wyłączyć przez priority
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 300px"
/>
<h3>{product.name}</h3>
</div>
))}
</div>
);
}
Materiały:
↑ Powrót na góręNext.js - API i Backend
Jak tworzyć Route Handlers (API routes) w App Router?
Odpowiedź w 30 sekund:
Route Handlers w App Router tworzy się przez utworzenie pliku route.ts lub route.js w katalogu app/. Każdy taki plik eksportuje funkcje nazwane według metod HTTP (GET, POST, PUT, DELETE). Route Handlers pozwalają na tworzenie endpointów API bez tworzenia osobnego serwera.
Odpowiedź w 2 minuty:
Route Handlers to nowy sposób tworzenia API routes w Next.js 13+ App Router, zastępujący poprzedni system z katalogu pages/api. Plik route.ts umieszczony w strukturze app/ automatycznie staje się endpointem API - na przykład app/api/users/route.ts tworzy endpoint /api/users.
W pliku route handler eksportujemy funkcje nazwane według metod HTTP: GET, POST, PUT, DELETE, PATCH, HEAD, i OPTIONS. Każda funkcja otrzymuje obiekt Request i opcjonalny kontekst z parametrami. Funkcje te zwracają obiekt Response lub używają helpera NextResponse do tworzenia odpowiedzi z odpowiednimi nagłówkami i statusami.
Route Handlers działają w środowisku Edge Runtime lub Node.js Runtime (konfigurowane przez export const runtime). Mogą być statycznie generowane podczas build (dla GET bez dynamicznych danych) lub renderowane dynamicznie. Wspierają streaming responses, mogą odczytywać headers, cookies, i są w pełni zgodne ze standardem Web Request/Response API.
Route Handlers nie mogą współistnieć z plikiem page.tsx w tym samym segmencie route - jeśli istnieje app/api/users/route.ts, nie może istnieć app/api/users/page.tsx. Są idealnym miejscem do tworzenia API endpoints, webhook handlers, czy integracji z zewnętrznymi serwisami.
Przykład kodu:
// app/api/users/route.ts
import { NextRequest, NextResponse } from 'next/server';
// GET /api/users
export async function GET(request: NextRequest) {
const searchParams = request.nextUrl.searchParams;
const limit = searchParams.get('limit') || '10';
try {
const users = await fetchUsers(Number(limit));
return NextResponse.json(
{ users, count: users.length },
{
status: 200,
headers: {
'Cache-Control': 'max-age=60, s-maxage=3600'
}
}
);
} catch (error) {
return NextResponse.json(
{ error: 'Błąd pobierania użytkowników' },
{ status: 500 }
);
}
}
// POST /api/users
export async function POST(request: NextRequest) {
try {
const body = await request.json();
const newUser = await createUser(body);
return NextResponse.json(
newUser,
{ status: 201 }
);
} catch (error) {
return NextResponse.json(
{ error: 'Błąd tworzenia użytkownika' },
{ status: 400 }
);
}
}
// Helper functions
async function fetchUsers(limit: number) {
// Logika pobierania użytkowników z bazy danych
return [];
}
async function createUser(data: any) {
// Logika tworzenia użytkownika
return { id: 1, ...data };
}
// app/api/users/[id]/route.ts - Dynamic route handler
import { NextRequest, NextResponse } from 'next/server';
interface RouteContext {
params: { id: string };
}
// GET /api/users/[id]
export async function GET(
request: NextRequest,
{ params }: RouteContext
) {
const userId = params.id;
const user = await fetchUserById(userId);
if (!user) {
return NextResponse.json(
{ error: 'Użytkownik nie znaleziony' },
{ status: 404 }
);
}
return NextResponse.json(user);
}
// DELETE /api/users/[id]
export async function DELETE(
request: NextRequest,
{ params }: RouteContext
) {
const userId = params.id;
await deleteUser(userId);
return new Response(null, { status: 204 });
}
async function fetchUserById(id: string) {
// Logika pobierania użytkownika
return null;
}
async function deleteUser(id: string) {
// Logika usuwania użytkownika
}
// app/api/upload/route.ts - Obsługa plików
import { NextRequest, NextResponse } from 'next/server';
import { writeFile } from 'fs/promises';
import path from 'path';
export async function POST(request: NextRequest) {
const formData = await request.formData();
const file = formData.get('file') as File;
if (!file) {
return NextResponse.json(
{ error: 'Brak pliku' },
{ status: 400 }
);
}
const bytes = await file.arrayBuffer();
const buffer = Buffer.from(bytes);
// Zapis pliku
const filePath = path.join(process.cwd(), 'public/uploads', file.name);
await writeFile(filePath, buffer);
return NextResponse.json({
message: 'Plik przesłany',
filename: file.name
});
}
Materiały:
↑ Powrót na góręNext.js - Konfiguracja i Deployment
Jakie są kluczowe opcje w pliku next.config.js?
Odpowiedź w 30 sekund:
Plik next.config.js to główny plik konfiguracyjny Next.js, który pozwala dostosować działanie frameworka. Kluczowe opcje to: reactStrictMode, images (optymalizacja obrazów), env (zmienne środowiskowe), redirects/rewrites, headers, webpack, oraz experimental dla nowych funkcji.
Odpowiedź w 2 minuty:
Plik next.config.js umożliwia zaawansowaną konfigurację aplikacji Next.js. Opcja reactStrictMode: true włącza tryb strict React, wykrywając potencjalne problemy w kodzie. Sekcja images konfiguruje Next.js Image Optimization, definiując dozwolone domeny (domains), rozmiary (deviceSizes, imageSizes) i formaty obrazów. Opcja i18n obsługuje wielojęzyczność, określając dostępne języki i domyślny locale.
Funkcje redirects(), rewrites() i headers() pozwalają na dynamiczną konfigurację przekierowań, przepisywania URL-i i nagłówków HTTP. Opcja webpack umożliwia dostosowanie konfiguracji Webpack, np. dodanie aliasów czy modyfikację loaderów. Właściwość env definiuje zmienne środowiskowe dostępne w kodzie klienta (prefiks NEXT_PUBLIC_ jest bardziej zalecaną metodą).
Sekcja experimental zawiera funkcje w fazie beta, jak appDir (App Router w Next.js 13+), serverActions, czy optimizeCss. Opcje compress, poweredByHeader, generateEtags kontrolują optymalizacje i bezpieczeństwo. Właściwość output: 'standalone' generuje zoptymalizowaną wersję do deploymentu w kontenerach Docker.
Warto również poznać opcje basePath (dla aplikacji w podfolderze), assetPrefix (CDN dla statycznych zasobów), trailingSlash (format URL-i), oraz pageExtensions (niestandardowe rozszerzenia plików dla stron). Każda z tych opcji pozwala dostosować Next.js do specyficznych wymagań projektu.
Przykład kodu:
// next.config.js - kompletna konfiguracja
/** @type {import('next').NextConfig} */
const nextConfig = {
// Włącz tryb strict React
reactStrictMode: true,
// Konfiguracja optymalizacji obrazów
images: {
domains: ['cdn.example.com', 'images.unsplash.com'],
deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840],
imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],
formats: ['image/webp', 'image/avif'],
minimumCacheTTL: 60,
dangerouslyAllowSVG: true,
contentDispositionType: 'attachment',
},
// Wielojęzyczność
i18n: {
locales: ['pl', 'en', 'de'],
defaultLocale: 'pl',
localeDetection: true,
},
// Przekierowania HTTP
async redirects() {
return [
{
source: '/stary-blog/:slug',
destination: '/blog/:slug',
permanent: true, // 301 redirect
},
{
source: '/api/old-endpoint',
destination: '/api/v2/endpoint',
permanent: false, // 302 redirect
},
];
},
// Przepisywanie URL-i (proxy)
async rewrites() {
return [
{
source: '/api/external/:path*',
destination: 'https://external-api.com/:path*',
},
{
source: '/blog/:slug',
destination: '/posts/:slug',
},
];
},
// Niestandardowe nagłówki HTTP
async headers() {
return [
{
source: '/api/:path*',
headers: [
{
key: 'Access-Control-Allow-Origin',
value: '*',
},
{
key: 'Access-Control-Allow-Methods',
value: 'GET, POST, PUT, DELETE, OPTIONS',
},
],
},
{
source: '/:path*',
headers: [
{
key: 'X-Content-Type-Options',
value: 'nosniff',
},
{
key: 'X-Frame-Options',
value: 'DENY',
},
{
key: 'X-XSS-Protection',
value: '1; mode=block',
},
],
},
];
},
// Zmienne środowiskowe (przestarzałe, użyj NEXT_PUBLIC_)
env: {
CUSTOM_KEY: 'wartość',
},
// Konfiguracja Webpack
webpack: (config, { buildId, dev, isServer, defaultLoaders, webpack }) => {
// Dodaj aliasy
config.resolve.alias = {
...config.resolve.alias,
'@components': path.resolve(__dirname, 'components'),
'@utils': path.resolve(__dirname, 'utils'),
};
// Dodaj plugin
config.plugins.push(
new webpack.DefinePlugin({
'process.env.BUILD_ID': JSON.stringify(buildId),
})
);
return config;
},
// Funkcje eksperymentalne
experimental: {
appDir: true, // App Router (Next.js 13+)
serverActions: true, // Server Actions
optimizeCss: true, // Optymalizacja CSS
scrollRestoration: true, // Przywracanie pozycji scroll
},
// Deployment w kontenerze
output: 'standalone',
// Aplikacja w podfolderze
basePath: '/moja-aplikacja',
// CDN dla zasobów statycznych
assetPrefix: process.env.NODE_ENV === 'production'
? 'https://cdn.example.com'
: '',
// URL-e z końcowym slashem
trailingSlash: true,
// Wyłącz nagłówek X-Powered-By
poweredByHeader: false,
// Kompresja gzip
compress: true,
// Generowanie ETag
generateEtags: true,
// Niestandardowe rozszerzenia plików
pageExtensions: ['tsx', 'ts', 'jsx', 'js', 'mdx'],
// TypeScript - ścisła weryfikacja
typescript: {
ignoreBuildErrors: false,
},
// ESLint podczas budowania
eslint: {
ignoreDuringBuilds: false,
dirs: ['pages', 'components', 'lib', 'app'],
},
// Konfiguracja kompilera SWC
swcMinify: true,
compiler: {
removeConsole: process.env.NODE_ENV === 'production',
styledComponents: true,
emotion: true,
},
};
module.exports = nextConfig;
Materiały
↑ Powrót na górę