Kurs Next.js · Moduł 3: Stylowanie i optymalizacja wizualna
Zarządzanie zasobami statycznymi w katalogu public w Next.js 16
W tej lekcji12
W Metropolis Quantum, każda dzielnica ma swoją centralną bazę zasobów - miejsce, gdzie przechowywane są elementy dostępne dla wszystkich mieszkańców: mapy, broszury informacyjne, elementy identyfikacji wizualnej czy interaktywne holoekrany z planami miasta. Te zasoby są niezmienne i dostępne publicznie, zapewniając spójność i ciągłość doświadczenia w całym mieście.
W aplikacjach Next.js, rolę tej centralnej bazy pełni katalog public. To właśnie tutaj umieszczamy wszystkie zasoby statyczne, które powinny być dostępne bez przetwarzania przez system budowania Next.js. W tym rozdziale przyjrzymy się, jak efektywnie zarządzać tymi zasobami, by nasza aplikacja była zarówno wydajna, jak i dobrze zorganizowana.
Czym jest katalog public w Next.js 16?
Katalog public to specjalny folder w aplikacji Next.js, który spełnia następujące funkcje:
- Udostępnia pliki statyczne bezpośrednio - pliki umieszczone w
publicsą serwowane z głównego katalogu domeny - Nie przetwarza zawartości - pliki nie przechodzą przez system budowania (w przeciwieństwie do zasobów importowanych bezpośrednio w komponentach)
- Zapewnia niezmienne URL-e - nazwy plików nie ulegają zmianie przez proces budowania (nie są dodawane hashe do nazw)
Katalog ten jest idealny do przechowywania takich zasobów jak:
- Obrazy logo i ikony
- Pliki favicon i manifest.json
- Pliki robots.txt i sitemap.xml
- Czcionki
- Dokumenty (PDF, DOCX)
- Zasoby multimedialne (filmy, dźwięki)
- Dane statyczne (JSON, CSV)
Struktura katalogu public
Dobrze zorganizowany katalog public ułatwia zarządzanie zasobami. Oto przykładowa struktura:
public/
├── favicon.ico # Ikona strony
├── favicon-16x16.png # Alternatywne rozmiary favicon
├── favicon-32x32.png
├── apple-touch-icon.png # Ikona dla urządzeń Apple
├── manifest.json # Manifest PWA
├── robots.txt # Instrukcje dla botów
├── sitemap.xml # Mapa strony dla wyszukiwarek
├── fonts/ # Katalog z czcionkami
│ ├── QuantumSans-Regular.woff2
│ └── QuantumSans-Bold.woff2
├── images/ # Katalog z obrazami
│ ├── logo.png
│ ├── hero-banner.jpg
│ ├── icons/ # Podkatalog z ikonami
│ │ ├── arrow-right.svg
│ │ └── search.svg
│ └── backgrounds/ # Podkatalog z tłami
│ ├── quantum-pattern.svg
│ └── districts/ # Dalsze podkatalogi dla organizacji
│ ├── downtown.jpg
│ └── quantum-district.jpg
├── videos/ # Katalog z filmami
│ └── intro.mp4
└── data/ # Katalog z danymi statycznymi
├── districts.json
└── services.jsonDostęp do zasobów z katalogu public
Dostęp do zasobów z katalogu public jest prosty i intuicyjny. Pamiętaj, że:
- Ścieżki są względem głównego katalogu domeny
- Musisz poprzedzać nazwy plików ukośnikiem (
/)
W komponentach React
// Przykład użycia obrazu z katalogu public
export default function Logo() {
return (
<img
src="/images/logo.png"
alt="Metropolis Quantum Logo"
width={200}
height={50}
/>
);
}W CSS
/* W pliku CSS */
.hero-section {
background-image: url('/images/backgrounds/quantum-pattern.svg');
background-repeat: repeat;
}W metadanych
// app/layout.tsx
import { Metadata } from 'next';
export const metadata: Metadata = {
title: 'Metropolis Quantum',
description: 'Oficjalny portal miasta przyszłości',
icons: {
icon: [
{ url: '/favicon-16x16.png', sizes: '16x16', type: 'image/png' },
{ url: '/favicon-32x32.png', sizes: '32x32', type: 'image/png' },
],
apple: [
{ url: '/apple-touch-icon.png' },
],
},
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="pl">
<body>{children}</body>
</html>
);
}Komponent Image a katalog public
Mimo że Next.js dostarcza zoptymalizowany komponent Image z pakietu next/image, czasem potrzebujemy użyć obrazów bezpośrednio z katalogu public. Komponent Image można używać z obrazami z katalogu public, zachowując korzyści z optymalizacji:
import Image from 'next/image';
export default function HeroBanner() {
return (
<div className="hero-banner">
<Image
src="/images/hero-banner.jpg"
alt="Witaj w Metropolis Quantum"
width={1200}
height={600}
preload
/>
</div>
);
}Kiedy używać obrazów z public zamiast importów?
Gdy URL obrazu musi być znany podczas budowy:
- Obrazy w metadanych (Open Graph, Twitter)
- Tła CSS
Gdy obraz jest bardzo duży i rzadko używany:
- Duże grafiki, które pojawiają się tylko w specyficznych kontekstach
Gdy chcesz mieć bezpośredni link do pliku:
- Zasoby do pobrania
- Zasoby współdzielone z innymi aplikacjami
Obsługa różnych typów plików
SVG
Pliki SVG można umieszczać w katalogu public i używać ich jako zwykłych obrazów:
export default function Icon() {
return (
<img src="/images/icons/search.svg" alt="Szukaj" width={24} height={24} />
);
}Alternatywnie, SVG można importować bezpośrednio w kodzie (nie przez katalog public), co pozwala na manipulację ich właściwościami:
import SearchIcon from '@/public/images/icons/search.svg';
export default function SearchButton() {
return (
<button className="search-button">
<SearchIcon className="icon" width={24} height={24} />
Szukaj
</button>
);
}Uwaga: Aby powyższy przykład działał, potrzebujesz dodatkowej konfiguracji, np. za pomocą
@svgr/webpack.
Czcionki
Jak widzieliśmy w poprzednim rozdziale, pliki czcionek można przechowywać w katalogu public i używać za pomocą next/font/local:
import localFont from 'next/font/local';
export const quantumSans = localFont({
src: [
{
path: '../public/fonts/QuantumSans-Regular.woff2',
weight: '400',
style: 'normal',
},
{
path: '../public/fonts/QuantumSans-Bold.woff2',
weight: '700',
style: 'normal',
},
],
});Filmy i audio
Pliki multimedialne również mogą być przechowywane w katalogu public:
export default function IntroVideo() {
return (
<video
controls
width="100%"
poster="/images/video-poster.jpg"
>
<source src="/videos/intro.mp4" type="video/mp4" />
Twoja przeglądarka nie obsługuje odtwarzacza wideo.
</video>
);
}Pliki JSON
JSON przechowywane w public można ładować za pomocą fetch:
// app/districts/page.tsx
export default async function DistrictsPage() {
// Fetch danych z pliku JSON
const response = await fetch('http://localhost:3000/data/districts.json');
const districts = await response.json();
return (
<div>
<h1>Dzielnice Metropolis Quantum</h1>
<ul>
{districts.map((district) => (
<li key={district.id}>{district.name}</li>
))}
</ul>
</div>
);
}Wskazówka: W rzeczywistych aplikacjach lepszym rozwiązaniem jest często importowanie danych JSON bezpośrednio w kodzie (np.
import districts from '@/data/districts.json') lub przechowywanie ich w bazie danych.
Organizacja i najlepsze praktyki
1. Struktura katalogów
Utrzymuj logiczną strukturę katalogów, która odzwierciedla przeznaczenie zasobów:
public/
├── branding/ # Elementy związane z marką
├── icons/ # Ikony systemowe
├── images/ # Zdjęcia i grafiki
├── videos/ # Pliki wideo
├── fonts/ # Czcionki
└── data/ # Dane statyczne (JSON, CSV)2. Konwencje nazewnictwa
Stosuj spójne konwencje nazewnictwa:
- Używaj kebab-case (
hero-banner.jpg) zamiast spacji (hero banner.jpg) - Dodawaj sufiksy z rozmiarami dla różnych wariantów (
logo-sm.png,logo-lg.png) - Używaj prefiksów dla grupowania (
icon-home.svg,icon-user.svg)
3. Optymalizacja zasobów
Przed umieszczeniem zasobów w katalogu public, optymalizuj je:
- Obrazy: Kompresuj za pomocą narzędzi jak TinyPNG, ImageOptim
- SVG: Minimalizuj za pomocą SVGO
- Wideo: Kompresuj do właściwych formatów i rozmiarów
- Czcionki: Używaj WOFF2 jako preferowanego formatu
4. Cachowanie
Next.js 16 dodaje do plików z katalogu public nagłówek Cache-Control: public, max-age=0, bo takie pliki mogą się zmienić bez zmiany nazwy. Długie, niezmienne cachowanie dostają tylko pliki z /_next/static, które mają w nazwie skrót zawartości. Jeśli wiesz, że Twoje zasoby się nie zmieniają, ustaw własne nagłówki w pliku next.config.js:
// next.config.js
module.exports = {
async headers() {
return [
{
source: '/fonts/:path*',
headers: [
{
key: 'Cache-Control',
value: 'public, max-age=31536000, immutable',
},
],
},
{
source: '/images/:path*',
headers: [
{
key: 'Cache-Control',
value: 'public, max-age=86400, stale-while-revalidate=31536000',
},
],
},
];
},
};Problemy i rozwiązania
Problem 1: Zbyt duży rozmiar katalogu public
Jeśli katalog public staje się zbyt duży, może to wpłynąć na czas deploymentu i zarządzanie zasobami.
Rozwiązanie:
- Przenieś rzadko używane, duże zasoby do usług CDN (Content Delivery Network)
- Stosuj strategie lazy loading dla zasobów
- Optymalizuj wszystkie zasoby przed dodaniem ich do katalogu
Problem 2: Problemy z URL obrazów
Rozwiązanie:
- Zawsze używaj ukośników na początku ścieżek (
/images/logo.png, nieimages/logo.png) - Unikaj spacji i znaków specjalnych w nazwach plików
- Sprawdzaj wielkość liter w nazwach plików (systemy Linux są case-sensitive)
Problem 3: Niespójność w zarządzaniu zasobami
Rozwiązanie:
- Stwórz i stosuj konwencje nazewnictwa dla całego zespołu
- Dokumentuj strukturę katalogu
public - Rozważ utworzenie komponentów abstrakcyjnych dla często używanych zasobów
Praktyczne przykłady
Przykład 1: System ikon z katalogu public
// components/Icon.tsx
type IconName = 'home' | 'search' | 'user' | 'settings' | 'menu';
interface IconProps {
name: IconName;
size?: 'sm' | 'md' | 'lg';
color?: string;
className?: string;
}
const sizeMap = {
sm: { width: 16, height: 16 },
md: { width: 24, height: 24 },
lg: { width: 32, height: 32 },
};
export function Icon({
name,
size = 'md',
color = 'currentColor',
className = '',
}: IconProps) {
const dimensions = sizeMap[size];
return (
<img
src={`/icons/${name}.svg`}
alt={`${name} icon`}
className={`icon icon-${name} ${className}`}
{...dimensions}
style={{ filter: `${color !== 'currentColor' ? `color(${color})` : ''}` }}
/>
);
}Użycie:
import { Icon } from '@/components/Icon';
export default function Navbar() {
return (
<nav className="navbar">
<Icon name="home" size="sm" />
<Icon name="search" color="#3a86ff" />
<Icon name="user" className="user-icon" />
</nav>
);
}Przykład 2: System dynamicznego ładowania zdjęć z katalogu public
// components/DistrictImage.tsx
import Image from 'next/image';
interface DistrictImageProps {
districtId: string;
alt: string;
className?: string;
}
export function DistrictImage({ districtId, alt, className = '' }: DistrictImageProps) {
return (
<div className={`district-image-container ${className}`}>
<Image
src={`/images/backgrounds/districts/${districtId}.jpg`}
alt={alt}
fill
className="district-image"
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
/>
</div>
);
}Użycie:
import { DistrictImage } from '@/components/DistrictImage';
export default function DistrictCard({ district }) {
return (
<div className="district-card">
<DistrictImage
districtId={district.id}
alt={`Dzielnica ${district.name}`}
/>
<h3>{district.name}</h3>
<p>{district.description}</p>
</div>
);
}Przykład 3: Konfiguracja SEO z zasobami z public
// app/districts/[id]/page.tsx
import { Metadata } from 'next';
interface Props {
params: Promise<{ id: string }>;
}
export async function generateMetadata({ params }: Props): Promise<Metadata> {
const districtId = (await params).id;
const district = await fetchDistrictData(districtId);
return {
title: `${district.name} | Metropolis Quantum`,
description: district.description,
openGraph: {
images: [{
url: `/images/backgrounds/districts/${districtId}.jpg`,
width: 1200,
height: 630,
alt: district.name,
}],
type: 'website',
},
twitter: {
card: 'summary_large_image',
images: [`/images/backgrounds/districts/${districtId}.jpg`],
},
};
}
export default async function DistrictPage({ params }: Props) {
const district = await fetchDistrictData((await params).id);
return (
<div className="district-page">
<h1>{district.name}</h1>
{/* Reszta zawartości strony */}
</div>
);
}
// Funkcja pobierająca dane dzielnicy
async function fetchDistrictData(id: string) {
// W rzeczywistej aplikacji: pobieranie danych z API lub bazy
const response = await fetch(`http://localhost:3000/data/districts/${id}.json`);
return response.json();
}Integracja z CDN
Dla większych aplikacji warto rozważyć przeniesienie zasobów z katalogu public do zewnętrznego CDN (Content Delivery Network), co zapewni szybsze ładowanie na całym świecie.
Konfiguracja ścieżki bazowej w Next.js
// next.config.js
module.exports = {
assetPrefix: process.env.NODE_ENV === 'production' ? 'https://cdn.twojadomena.com' : '',
};Komponent do zarządzania ścieżkami zasobów
// utils/assetPath.ts
export function getAssetPath(path: string): string {
const cdnUrl = process.env.NEXT_PUBLIC_CDN_URL || '';
return `${cdnUrl}${path}`;
}Użycie:
import { getAssetPath } from '@/utils/assetPath';
export default function Logo() {
return (
<img
src={getAssetPath('/images/logo.png')}
alt="Metropolis Quantum Logo"
width={200}
height={50}
/>
);
}Zaawansowane techniki
1. Dynamiczne importy zasobów
Możesz dynamicznie importować zasoby z katalogu public podczas budowy:
// utils/getDistrictData.ts
import fs from 'fs';
import path from 'path';
export function getDistrictData() {
const filePath = path.join(process.cwd(), 'public', 'data', 'districts.json');
const fileContents = fs.readFileSync(filePath, 'utf8');
return JSON.parse(fileContents);
}2. Generowanie zasobów podczas budowy
Możesz generować zasoby statyczne podczas budowy aplikacji:
// scripts/generate-sitemap.js
const fs = require('fs');
const path = require('path');
// Funkcja generująca sitemap.xml
function generateSitemap(routes) {
const sitemap = `<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
${routes.map(route => `
<url>
<loc>https://metropolisquantum.pl${route}</loc>
<lastmod>${new Date().toISOString()}</lastmod>
</url>
`).join('')}
</urlset>
`;
fs.writeFileSync(
path.join(process.cwd(), 'public', 'sitemap.xml'),
sitemap
);
}
// Lista tras aplikacji
const routes = [
'/',
'/districts',
'/about',
'/contact',
// inne trasy
];
generateSitemap(routes);Następnie możesz dodać ten skrypt do procesu budowania w package.json:
{
"scripts": {
"build": "node scripts/generate-sitemap.js && next build"
}
}Bezpieczeństwo i uwagi dotyczące zasobów publicznych
1. Unikaj przechowywania poufnych informacji
Katalog public jest dostępny publicznie, więc nigdy nie należy przechowywać w nim:
- Kluczy API
- Poufnych danych
- Plików konfiguracyjnych z wrażliwymi informacjami
2. Ochrona przed hotlinkiem
Możesz chronić swoje zasoby przed hotlinkingiem (używaniem ich na innych stronach) za pomocą nagłówków HTTP:
// next.config.js
module.exports = {
async headers() {
return [
{
source: '/images/:path*',
headers: [
{
key: 'Referrer-Policy',
value: 'same-origin',
},
],
},
];
},
};3. Content Security Policy (CSP)
Warto skonfigurować CSP, aby chronić swoją aplikację:
// next.config.js
module.exports = {
async headers() {
return [
{
source: '/:path*',
headers: [
{
key: 'Content-Security-Policy',
value: "default-src 'self'; img-src 'self' https://cdn.twojadomena.com; font-src 'self' https://fonts.gstatic.com",
},
],
},
];
},
};Podsumowanie
Katalog public w Next.js 16 jest potężnym narzędziem do zarządzania zasobami statycznymi w aplikacji. Odpowiednia organizacja, konwencje nazewnictwa i optymalizacja zasobów mogą znacząco poprawić wydajność i utrzymywalność projektu.
Kluczowe punkty:
- Struktura i organizacja - utrzymuj logiczną strukturę katalogów i spójne konwencje nazewnictwa
- Optymalizacja - kompresuj i optymalizuj zasoby przed umieszczeniem ich w katalogu
- Abstrakcja - twórz komponenty abstrakcyjne do zarządzania dostępem do zasobów
- Bezpieczeństwo - pamiętaj, że zawartość katalogu
publicjest dostępna publicznie - Skalowanie - dla większych aplikacji rozważ użycie CDN
Podobnie jak w centralnej bazie zasobów Metropolis Quantum, dobrze zarządzany katalog public zapewnia sprawny dostęp do wszystkich niezbędnych elementów, utrzymując porządek i wydajność całego systemu.
W następnym rozdziale przyjrzymy się ikonom, favicon i metadanym wizualnym, które są kluczowymi elementami tożsamości wizualnej Twojej aplikacji Next.js 16.
Kod do tej lekcji: app/page.tsx
// CSS Performance Optimization - Cyberpunk Edition
'use client';
export default function PerformanceDemo() {
return (
<div style={{
minHeight: '100vh',
background: 'linear-gradient(135deg, #0f0f23, #1a1a2e)',
color: '#ffffff',
padding: '3rem 2rem'
}}>
<h1 style={{
fontSize: '3rem',
textAlign: 'center',
background: 'linear-gradient(45deg, #64ffda, #7c4dff)',
WebkitBackgroundClip: 'text',
WebkitTextFillColor: 'transparent',
marginBottom: '3rem'
}}>
CSS Performance Optimization
</h1>
{/* Will-change dla animowanych elementów */}
<section style={{
maxWidth: '1200px',
margin: '0 auto 3rem',
background: 'rgba(255,255,255,0.05)',
padding: '2rem',
borderRadius: '1rem',
border: '1px solid rgba(100,255,218,0.2)'
}}>
<h2 style={{color: '#64ffda', marginBottom: '1.5rem'}}>1. will-change Property</h2>
<div style={{
width: '200px',
height: '200px',
background: 'linear-gradient(135deg, #64ffda, #7c4dff)',
borderRadius: '1rem',
willChange: 'transform',
animation: 'float 3s ease-in-out infinite'
}}></div>
<p style={{color: '#b0bec5', marginTop: '1rem'}}>
Ten element używa <code style={{background: 'rgba(0,0,0,0.5)', padding: '0.25rem 0.5rem', borderRadius: '0.25rem', color: '#64ffda'}}>will-change: transform</code> dla lepszej wydajności animacji
</p>
</section>
{/* Transform zamiast left/top */}
<section style={{
maxWidth: '1200px',
margin: '0 auto 3rem',
background: 'rgba(255,255,255,0.05)',
padding: '2rem',
borderRadius: '1rem',
border: '1px solid rgba(100,255,218,0.2)'
}}>
<h2 style={{color: '#64ffda', marginBottom: '1.5rem'}}>2. Transform vs Position</h2>
<div style={{display: 'grid', gridTemplateColumns: 'repeat(2, 1fr)', gap: '2rem'}}>
<div>
<h3 style={{color: '#7c4dff', marginBottom: '1rem'}}>Wolne (position)</h3>
<div style={{
position: 'relative',
width: '150px',
height: '150px',
background: '#ff0080',
borderRadius: '0.75rem'
}}>
<div style={{fontSize: '0.875rem', padding: '1rem', color: '#fff'}}>
Używa position: relative; top/left
</div>
</div>
</div>
<div>
<h3 style={{color: '#7c4dff', marginBottom: '1rem'}}>Szybkie (transform)</h3>
<div style={{
width: '150px',
height: '150px',
background: '#00ff7f',
borderRadius: '0.75rem',
transform: 'translateZ(0)'
}}>
<div style={{fontSize: '0.875rem', padding: '1rem', color: '#0f0f23', fontWeight: 600}}>
Używa transform: translate()
</div>
</div>
</div>
</div>
</section>
{/* Contain property */}
<section style={{
maxWidth: '1200px',
margin: '0 auto 3rem',
background: 'rgba(255,255,255,0.05)',
padding: '2rem',
borderRadius: '1rem',
border: '1px solid rgba(100,255,218,0.2)'
}}>
<h2 style={{color: '#64ffda', marginBottom: '1.5rem'}}>3. CSS Containment</h2>
<div style={{
contain: 'layout style paint',
background: 'rgba(124,77,255,0.2)',
padding: '1.5rem',
borderRadius: '0.75rem',
border: '1px solid rgba(124,77,255,0.5)'
}}>
<p style={{color: '#b0bec5', lineHeight: 1.6}}>
Ten kontener używa <code style={{background: 'rgba(0,0,0,0.5)', padding: '0.25rem 0.5rem', borderRadius: '0.25rem', color: '#7c4dff'}}>contain: layout style paint</code>
<br/><br/>
Izoluje zmiany layoutu tylko do tego kontenera, poprawiając wydajność renderowania.
</p>
</div>
</section>
{/* Content-visibility */}
<section style={{
maxWidth: '1200px',
margin: '0 auto 3rem',
background: 'rgba(255,255,255,0.05)',
padding: '2rem',
borderRadius: '1rem',
border: '1px solid rgba(100,255,218,0.2)'
}}>
<h2 style={{color: '#64ffda', marginBottom: '1.5rem'}}>4. content-visibility</h2>
{[1, 2, 3, 4, 5].map(i => (
<div key={i} style={{
contentVisibility: 'auto',
containIntrinsicSize: '0 200px',
marginBottom: '1.5rem',
background: 'rgba(100,255,218,0.1)',
padding: '1.5rem',
borderRadius: '0.75rem',
border: '1px solid rgba(100,255,218,0.3)'
}}>
<h3 style={{color: '#64ffda', marginBottom: '0.75rem'}}>Section {i}</h3>
<p style={{color: '#b0bec5', lineHeight: 1.6}}>
Ta sekcja używa content-visibility: auto. Przeglądarka renderuje ją tylko wtedy, gdy jest w viewporcie,
co znacząco poprawia wydajność przy dużych listach treści.
</p>
</div>
))}
</section>
{/* Critical CSS */}
<section style={{
maxWidth: '1200px',
margin: '0 auto 3rem',
background: 'rgba(255,255,255,0.05)',
padding: '2rem',
borderRadius: '1rem',
border: '1px solid rgba(100,255,218,0.2)'
}}>
<h2 style={{color: '#64ffda', marginBottom: '1.5rem'}}>5. Critical CSS Best Practices</h2>
<ul style={{color: '#b0bec5', lineHeight: 2, paddingLeft: '1.5rem'}}>
<li>Inline krytyczne style w <head></li>
<li>Lazy-load non-critical CSS</li>
<li>Użyj CSS-in-JS dla komponentów</li>
<li>Minifikuj i kompresuj CSS</li>
<li>Usuń nieużywane style (PurgeCSS)</li>
<li>Użyj CDN dla szybszego dostarczania</li>
</ul>
</section>
<style jsx global>{`
@keyframes float {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-20px); }
}
`}</style>
</div>
);
}Hasła z dokumentacji w tej lekcji
Widzisz błąd w tej lekcji?
Sprawdź się
Odpowiedz na pytania z tej lekcji. Wybierz odpowiedź, a od razu zobaczysz, czy jest poprawna.
1. CSS Custom Properties (variables) definiuje się używając:
2. SASS/SCSS to:
To 2 z 3 pytań do tej lekcji. Pozostałe rozwiążesz w grze.
Zadania praktyczne w grze
- Edytor kodu
Utwórz CSS z uwzględnieniem accessibility: focus styles, prefers-reduced-motion, high contrast
- Klikanie w kolejności
Ułóż deklarację CSS Grid template:
- Układanie w pionie
Ułóż etapy optymalizacji obrazów przez komponent next/image w prawidłowej kolejności: