Ikony, favicon i metadane wizualne w Next.js 16
Kurs Next.js · Moduł 3: Stylowanie i optymalizacja wizualna
W Metropolis Quantum, identyfikacja wizualna miasta jest starannie zaprojektowana, by każdy element - od logo na budynkach administracji miejskiej, przez oznakowanie dzielnic, aż po aplikacje miejskie - tworzył spójny wizerunek. Ta identyfikacja wizualna pomaga mieszkańcom i gościom rozpoznawać oficjalne elementy miasta, buduje zaufanie i profesjonalny wizerunek.
W świecie aplikacji internetowych, ikony, favicon i metadane wizualne pełnią podobną rolę. Są one kluczowe dla rozpoznawalności Twojej aplikacji, wpływają na pierwsze wrażenie użytkowników i ułatwiają identyfikację Twojej strony wśród wielu otwartych kart przeglądarki. Next.js 16 oferuje rozbudowane narzędzia do zarządzania tymi elementami, które poznamy w tym rozdziale.
Znaczenie ikon i metadanych wizualnych
Favicon i ikony aplikacji
Favicon (skrót od "favorite icon") to mała ikona wyświetlana w zakładce przeglądarki, na liście zakładek i w historii przeglądania. Jest pierwszym elementem wizualnym, który użytkownicy kojarzą z Twoją aplikacją, dlatego warto zadbać o jej jakość i rozpoznawalność.
W nowoczesnych aplikacjach webowych potrzebujemy nie tylko podstawowego favicon, ale całego zestawu ikon w różnych rozmiarach, które będą używane w różnych kontekstach:
- Ikony zakładek przeglądarki
- Ikony aplikacji na urządzeniach mobilnych
- Ikony aplikacji na pulpicie (PWA)
- Ikony w wynikach wyszukiwania
- Ikony udostępniania w mediach społecznościowych
Metadane wizualne
Metadane wizualne to informacje, które określają, jak Twoja strona jest prezentowana, gdy jest udostępniana w mediach społecznościowych, komunikatorach czy wynikach wyszukiwania. Obejmują one:
- Tytuł strony
- Opis strony
- Zdjęcia/grafiki podglądowe
- Kolory motywu
- Informacje o aplikacji (dla PWA)
Podstawowa konfiguracja favicon w Next.js 16
Next.js 16 oferuje uproszczony sposób konfiguracji favicon i ikon przez API metadanych, dostępne od Next.js 13.2. Zacznijmy od podstawowej konfiguracji.
Umieszczenie ikon w katalogu public
Najpierw umieść pliki ikon w katalogu public:
1public/
2├── favicon.ico # Klasyczny favicon (16x16 lub 32x32)
3├── icon.png # Ikona główna (można użyć w metadata API)
4├── apple-icon.png # Ikona dla urządzeń Apple
5└── icons/ # Katalog z różnymi rozmiarami ikon
6 ├── icon-16x16.png
7 ├── icon-32x32.png
8 ├── icon-192x192.png
9 └── icon-512x512.pngKonfiguracja podstawowego favicon
Najprostszy sposób to umieszczenie pliku favicon.ico w katalogu public. Next.js automatycznie rozpozna ten plik i użyje go jako favicon:
1// app/layout.tsx
2export default function RootLayout({
3 children,
4}: {
5 children: React.ReactNode;
6}) {
7 return (
8 <html lang="pl">
9 <body>{children}</body>
10 </html>
11 );
12}W tym przypadku nie musisz robić nic więcej - przeglądarka automatycznie pobierze /favicon.ico.
Konfiguracja przez Metadata API
Next.js 16 oferuje zaawansowaną konfigurację ikon przez Metadata API:
1// app/layout.tsx
2import { Metadata } from 'next';
3
4export const metadata: Metadata = {
5 title: 'Metropolis Quantum',
6 description: 'Oficjalny portal miasta przyszłości',
7 icons: {
8 icon: '/icon.png', // lub array: [{ url: '/icon-16x16.png', sizes: '16x16' }, ...]
9 shortcut: '/shortcut-icon.png',
10 apple: '/apple-icon.png', // lub array: [{ url: '/apple-icon.png' }, ...]
11 other: {
12 rel: 'apple-touch-icon-precomposed',
13 url: '/apple-touch-icon-precomposed.png',
14 },
15 },
16};
17
18export default function RootLayout({
19 children,
20}: {
21 children: React.ReactNode;
22}) {
23 return (
24 <html lang="pl">
25 <body>{children}</body>
26 </html>
27 );
28}Zaawansowana konfiguracja ikon
Dla profesjonalnych aplikacji warto przygotować kompletny zestaw ikon w różnych rozmiarach i formatach.
Rozszerzona konfiguracja ikon
1// app/layout.tsx
2import { Metadata } from 'next';
3
4export const metadata: Metadata = {
5 icons: {
6 icon: [
7 { url: '/icons/icon-16x16.png', sizes: '16x16', type: 'image/png' },
8 { url: '/icons/icon-32x32.png', sizes: '32x32', type: 'image/png' },
9 { url: '/icons/icon-192x192.png', sizes: '192x192', type: 'image/png' },
10 { url: '/icons/icon-512x512.png', sizes: '512x512', type: 'image/png' },
11 ],
12 shortcut: '/shortcut-icon.png',
13 apple: [
14 { url: '/icons/apple-icon-57x57.png', sizes: '57x57', type: 'image/png' },
15 { url: '/icons/apple-icon-72x72.png', sizes: '72x72', type: 'image/png' },
16 { url: '/icons/apple-icon-114x114.png', sizes: '114x114', type: 'image/png' },
17 { url: '/icons/apple-icon-180x180.png', sizes: '180x180', type: 'image/png' },
18 ],
19 },
20};Rezultat w HTML
Powyższa konfiguracja wygeneruje następujące tagi w sekcji <head> dokumentu HTML:
1<link rel="icon" href="/icons/icon-16x16.png" sizes="16x16" type="image/png" />
2<link rel="icon" href="/icons/icon-32x32.png" sizes="32x32" type="image/png" />
3<link rel="icon" href="/icons/icon-192x192.png" sizes="192x192" type="image/png" />
4<link rel="icon" href="/icons/icon-512x512.png" sizes="512x512" type="image/png" />
5<link rel="shortcut icon" href="/shortcut-icon.png" />
6<link rel="apple-touch-icon" href="/icons/apple-icon-57x57.png" sizes="57x57" type="image/png" />
7<link rel="apple-touch-icon" href="/icons/apple-icon-72x72.png" sizes="72x72" type="image/png" />
8<link rel="apple-touch-icon" href="/icons/apple-icon-114x114.png" sizes="114x114" type="image/png" />
9<link rel="apple-touch-icon" href="/icons/apple-icon-180x180.png" sizes="180x180" type="image/png" />Metadane Open Graph i Twitter Cards
Metadane Open Graph i Twitter Cards określają, jak Twoja strona wygląda, gdy jest udostępniana na platformach społecznościowych. Next.js 16 oferuje wbudowane wsparcie dla tych metadanych.
Konfiguracja Open Graph
1// app/layout.tsx
2import { Metadata } from 'next';
3
4export const metadata: Metadata = {
5 title: 'Metropolis Quantum',
6 description: 'Oficjalny portal miasta przyszłości',
7 openGraph: {
8 title: 'Metropolis Quantum - Miasto Przyszłości',
9 description: 'Odkryj technologię jutra w mieście Metropolis Quantum',
10 url: 'https://metropolisquantum.pl',
11 siteName: 'Metropolis Quantum',
12 images: [
13 {
14 url: '/images/og-image.jpg', // 1200x630 px rekomendowany rozmiar
15 width: 1200,
16 height: 630,
17 alt: 'Metropolis Quantum - Panorama miasta przyszłości',
18 },
19 ],
20 locale: 'pl_PL',
21 type: 'website',
22 },
23};Konfiguracja Twitter Cards
1// app/layout.tsx
2import { Metadata } from 'next';
3
4export const metadata: Metadata = {
5 twitter: {
6 card: 'summary_large_image',
7 title: 'Metropolis Quantum - Miasto Przyszłości',
8 description: 'Odkryj technologię jutra w mieście Metropolis Quantum',
9 siteId: '1467726470533754880', // ID konta Twitter
10 creator: '@MetropolisQ',
11 creatorId: '1467726470533754880',
12 images: ['/images/twitter-image.jpg'], // 800x418 px rekomendowany rozmiar
13 },
14};Testowanie metadanych Open Graph i Twitter Cards
Po wdrożeniu możesz przetestować swoje metadane za pomocą:
Dynamiczne metadane
W Next.js 16 możesz generować metadane dynamicznie, na podstawie ścieżki, parametrów URL lub danych zewnętrznych.
Dynamiczne metadane dla podstron
1// app/districts/[id]/page.tsx
2import { Metadata, ResolvingMetadata } from 'next';
3
4interface Props {
5 params: Promise<{ id: string }>;
6 searchParams: Promise<{ [key: string]: string | string[] | undefined }>;
7}
8
9export async function generateMetadata(
10 { params, searchParams }: Props,
11 parent: ResolvingMetadata
12): Promise<Metadata> {
13 // Pobierz dane o dzielnicy
14 const districtId = (await params).id;
15 const district = await getDistrictData(districtId);
16
17 // Opcjonalnie możesz połączyć z nadrzędnymi metadanymi
18 const previousImages = (await parent).openGraph?.images || [];
19
20 return {
21 title: `${district.name} | Metropolis Quantum`,
22 description: district.description,
23 openGraph: {
24 images: [
25 {
26 url: `/images/districts/${districtId}.jpg`,
27 width: 1200,
28 height: 630,
29 alt: district.name,
30 },
31 ...previousImages,
32 ],
33 },
34 twitter: {
35 images: [`/images/districts/${districtId}-twitter.jpg`],
36 },
37 };
38}
39
40export default async function DistrictPage({ params }: Props) {
41 // Renderowanie strony
42}
43
44// Funkcja pobierająca dane o dzielnicy
45async function getDistrictData(id: string) {
46 // W rzeczywistej aplikacji: fetch z API lub bazy danych
47 return {
48 name: 'Dystrykt Kwantowy',
49 description: 'Najbardziej zaawansowana technologicznie dzielnica Metropolis Quantum.',
50 };
51}Manifest aplikacji webowej (Web App Manifest)
Manifest aplikacji webowej to plik JSON, który dostarcza informacji o Twojej aplikacji dla przeglądarek i urządzeń mobilnych. Jest kluczowy dla Progressive Web Apps (PWA).
Tworzenie pliku manifest.json
Umieść plik manifest.json w katalogu public:
1{
2 "name": "Metropolis Quantum",
3 "short_name": "MetroQ",
4 "description": "Oficjalny portal miasta przyszłości",
5 "start_url": "/",
6 "display": "standalone",
7 "background_color": "#121212",
8 "theme_color": "#3a86ff",
9 "icons": [
10 {
11 "src": "/icons/icon-192x192.png",
12 "sizes": "192x192",
13 "type": "image/png",
14 "purpose": "any maskable"
15 },
16 {
17 "src": "/icons/icon-512x512.png",
18 "sizes": "512x512",
19 "type": "image/png"
20 }
21 ]
22}Konfiguracja manifestu w Next.js
1// app/layout.tsx
2import { Metadata } from 'next';
3
4export const metadata: Metadata = {
5 // Inne metadane...
6
7 // Manifest dla PWA
8 manifest: '/manifest.json',
9
10 // Lub zdefiniuj manifest bezpośrednio:
11 /*
12 manifest: {
13 name: 'Metropolis Quantum',
14 short_name: 'MetroQ',
15 description: 'Oficjalny portal miasta przyszłości',
16 start_url: '/',
17 display: 'standalone',
18 background_color: '#121212',
19 theme_color: '#3a86ff',
20 icons: [
21 {
22 src: '/icons/icon-192x192.png',
23 sizes: '192x192',
24 type: 'image/png',
25 purpose: 'any maskable'
26 },
27 {
28 src: '/icons/icon-512x512.png',
29 sizes: '512x512',
30 type: 'image/png'
31 }
32 ]
33 }
34 */
35};Inne metadane wizualne i funkcjonalne
Motyw kolorystyczny
Kolor motywu dla przeglądarek mobilnych ustawiasz w osobnym eksporcie viewport. Pole themeColor w obiekcie metadata jest przestarzałe od Next.js 14:
1// app/layout.tsx
2import type { Viewport } from 'next';
3
4export const viewport: Viewport = {
5 // Kolor motywu dla przeglądarek mobilnych
6 themeColor: [
7 { media: '(prefers-color-scheme: light)', color: '#3a86ff' },
8 { media: '(prefers-color-scheme: dark)', color: '#1e429f' },
9 ],
10
11 // Lub pojedynczy kolor:
12 // themeColor: '#3a86ff',
13};Obsługa Apple-specific
1// app/layout.tsx
2import { Metadata } from 'next';
3
4export const metadata: Metadata = {
5 // Inne metadane...
6
7 // Ustawienia dla Safari na iOS
8 appleWebApp: {
9 title: 'Metropolis Quantum', // Alternatywny tytuł dla iOS
10 statusBarStyle: 'black-translucent',
11 startupImage: [
12 {
13 url: '/startup/apple-startup-2048x2732.png',
14 media: '(device-width: 1024px) and (device-height: 1366px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)',
15 },
16 {
17 url: '/startup/apple-startup-1668x2388.png',
18 media: '(device-width: 834px) and (device-height: 1194px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)',
19 },
20 // Więcej rozmiarów...
21 ],
22 },
23};Aplikacja mobilna alternatywna
1// app/layout.tsx
2import { Metadata } from 'next';
3
4export const metadata: Metadata = {
5 // Inne metadane...
6
7 // Linki do aplikacji mobilnych
8 alternates: {
9 'android-app': 'android-app://com.metropolisquantum/https/metropolisquantum.pl',
10 'ios-app': 'ios-app://1234567890/https/metropolisquantum.pl',
11 },
12};Generowanie ikon i metadanych
Ręczne tworzenie wszystkich rozmiarów ikon może być żmudne. Na szczęście istnieją narzędzia, które automatyzują ten proces.
Narzędzia do generowania ikon
- Favicon Generator (https://realfavicongenerator.net/) - generuje pełny zestaw ikon z jednego obrazu
- PWA Asset Generator (https://github.com/onderceylan/pwa-asset-generator) - narzędzie wiersza poleceń do generowania ikon PWA
Przykład użycia PWA Asset Generator
1# Instalacja
2npm i -g pwa-asset-generator
3
4# Generowanie ikon
5npx pwa-asset-generator ./logo.svg ./public/icons --manifest ./public/manifest.json --icon-only --faviconDynamiczne generowanie obrazów Open Graph
Czasem warto dynamicznie generować obrazy Open Graph dla różnych podstron. W Next.js 16 możemy to zrobić za pomocą Route Handlers albo pliku opengraph-image.tsx umieszczonego obok strony.
Implementacja Route Handler dla dynamicznych obrazów OG
1// app/api/og/route.tsx
2import { ImageResponse } from 'next/og';
3import { NextRequest } from 'next/server';
4
5// Bez eksportu runtime = 'edge': w Next.js 16 ImageResponse działa w domyślnym środowisku Node.js,
6// a Edge Runtime jest w dokumentacji oznaczony jako przestarzały
7
8export async function GET(request: NextRequest) {
9 const { searchParams } = new URL(request.url);
10
11 // Pobierz parametry z URL
12 const title = searchParams.get('title') || 'Metropolis Quantum';
13 const description = searchParams.get('description') || 'Miasto przyszłości';
14
15 // Opcjonalnie: pobierz dodatkowe dane z bazy lub API
16 // const data = await fetchData();
17
18 // Wygeneruj obraz
19 return new ImageResponse(
20 (
21 <div
22 style={{
23 display: 'flex',
24 fontSize: 40,
25 color: 'white',
26 background: 'linear-gradient(to bottom, #3a86ff, #1e429f)',
27 width: '100%',
28 height: '100%',
29 padding: '50px 200px',
30 textAlign: 'center',
31 justifyContent: 'center',
32 alignItems: 'center',
33 flexDirection: 'column',
34 }}
35 >
36 <img
37 src={`${request.nextUrl.origin}/logo.png`}
38 alt="Logo"
39 width="200"
40 height="200"
41 />
42 <h1 style={{ fontSize: 70 }}>{title}</h1>
43 <p style={{ fontSize: 40 }}>{description}</p>
44 </div>
45 ),
46 {
47 width: 1200,
48 height: 630,
49 }
50 );
51}Użycie dynamicznego obrazu OG w metadanych
1// app/districts/[id]/page.tsx
2import { Metadata } from 'next';
3
4interface Props {
5 params: Promise<{ id: string }>;
6}
7
8export async function generateMetadata({ params }: Props): Promise<Metadata> {
9 const district = await getDistrictData((await params).id);
10
11 const ogImageUrl = new URL(`/api/og`, 'https://metropolisquantum.pl');
12 ogImageUrl.searchParams.append('title', district.name);
13 ogImageUrl.searchParams.append('description', district.description);
14
15 return {
16 title: `${district.name} | Metropolis Quantum`,
17 description: district.description,
18 openGraph: {
19 images: [ogImageUrl.toString()],
20 },
21 twitter: {
22 images: [ogImageUrl.toString()],
23 },
24 };
25}Strukturyzowane dane dla SEO
Strukturyzowane dane pomagają wyszukiwarkom lepiej rozumieć zawartość Twojej strony i mogą wpłynąć na sposób wyświetlania wyników wyszukiwania.
Implementacja JSON-LD
1// app/layout.tsx
2import { Metadata } from 'next';
3
4export const metadata: Metadata = {
5 // Inne metadane...
6};
7
8export default function RootLayout({
9 children,
10}: {
11 children: React.ReactNode;
12}) {
13 return (
14 <html lang="pl">
15 <head>
16 <script
17 type="application/ld+json"
18 dangerouslySetInnerHTML={{
19 __html: JSON.stringify({
20 '@context': 'https://schema.org',
21 '@type': 'Organization',
22 name: 'Metropolis Quantum',
23 url: 'https://metropolisquantum.pl',
24 logo: 'https://metropolisquantum.pl/logo.png',
25 contactPoint: {
26 '@type': 'ContactPoint',
27 telephone: '+48-123-456-789',
28 contactType: 'customer service',
29 },
30 sameAs: [
31 'https://facebook.com/metropolisquantum',
32 'https://twitter.com/metropolisq',
33 'https://instagram.com/metropolisquantum',
34 ],
35 }),
36 }}
37 />
38 </head>
39 <body>{children}</body>
40 </html>
41 );
42}Dynamiczne JSON-LD dla podstron
1// components/JsonLd.tsx
2interface JsonLdProps {
3 data: any;
4}
5
6export function JsonLd({ data }: JsonLdProps) {
7 return (
8 <script
9 type="application/ld+json"
10 dangerouslySetInnerHTML={{ __html: JSON.stringify(data) }}
11 />
12 );
13}1// app/districts/[id]/page.tsx
2import { JsonLd } from '@/components/JsonLd';
3
4export default async function DistrictPage({ params }: { params: Promise<{ id: string }> }) {
5 const district = await getDistrictData((await params).id);
6
7 const jsonLdData = {
8 '@context': 'https://schema.org',
9 '@type': 'Place',
10 name: district.name,
11 description: district.description,
12 address: {
13 '@type': 'PostalAddress',
14 addressLocality: 'Metropolis Quantum',
15 addressRegion: 'MQ',
16 postalCode: '00-000',
17 addressCountry: 'PL',
18 },
19 geo: {
20 '@type': 'GeoCoordinates',
21 latitude: district.coordinates.lat,
22 longitude: district.coordinates.lng,
23 },
24 image: `https://metropolisquantum.pl/images/districts/${(await params).id}.jpg`,
25 };
26
27 return (
28 <div>
29 <JsonLd data={jsonLdData} />
30 <h1>{district.name}</h1>
31 {/* Reszta zawartości */}
32 </div>
33 );
34}Przykłady kompletnej implementacji
Metadane dla aplikacji e-commerce
1// app/layout.tsx
2import type { Metadata, Viewport } from 'next';
3
4export const metadata: Metadata = {
5 metadataBase: new URL('https://metropolisquantum.pl'),
6 title: {
7 default: 'Metropolis Quantum Store',
8 template: '%s | Metropolis Quantum Store',
9 },
10 description: 'Sklep z najnowszymi technologiami przyszłości',
11 keywords: ['technologia', 'przyszłość', 'metropolis', 'quantum', 'sklep'],
12 authors: [{ name: 'Metropolis Quantum', url: 'https://metropolisquantum.pl' }],
13 creator: 'Zespół Metropolis Quantum',
14 publisher: 'Metropolis Quantum Corporation',
15
16 // Ikony
17 icons: {
18 icon: [
19 { url: '/icons/icon-16x16.png', sizes: '16x16', type: 'image/png' },
20 { url: '/icons/icon-32x32.png', sizes: '32x32', type: 'image/png' },
21 { url: '/icons/icon-192x192.png', sizes: '192x192', type: 'image/png' },
22 { url: '/icons/icon-512x512.png', sizes: '512x512', type: 'image/png' },
23 ],
24 apple: [
25 { url: '/icons/apple-icon.png' },
26 ],
27 },
28
29 // Open Graph
30 openGraph: {
31 type: 'website',
32 siteName: 'Metropolis Quantum Store',
33 title: 'Metropolis Quantum Store - Technologia przyszłości już dziś',
34 description: 'Sklep z najnowszymi technologiami przyszłości z Metropolis Quantum',
35 url: 'https://store.metropolisquantum.pl',
36 images: [
37 {
38 url: '/images/og-default.jpg',
39 width: 1200,
40 height: 630,
41 alt: 'Metropolis Quantum Store',
42 },
43 ],
44 locale: 'pl_PL',
45 },
46
47 // Twitter
48 twitter: {
49 card: 'summary_large_image',
50 title: 'Metropolis Quantum Store',
51 description: 'Technologia przyszłości już dziś',
52 images: ['/images/twitter-image.jpg'],
53 creator: '@MetropolisQ',
54 },
55
56 // Robots
57 robots: {
58 index: true,
59 follow: true,
60 googleBot: {
61 index: true,
62 follow: true,
63 'max-video-preview': -1,
64 'max-image-preview': 'large',
65 'max-snippet': -1,
66 },
67 },
68
69 // Manifest PWA
70 manifest: '/manifest.json',
71
72 // Alternatywne aplikacje
73 alternates: {
74 canonical: 'https://store.metropolisquantum.pl',
75 languages: {
76 'en-US': 'https://store.metropolisquantum.pl/en',
77 'de-DE': 'https://store.metropolisquantum.pl/de',
78 },
79 media: {
80 'only screen and (max-width: 640px)': 'https://m.store.metropolisquantum.pl',
81 },
82 types: {
83 'application/rss+xml': 'https://store.metropolisquantum.pl/rss',
84 },
85 },
86
87 // Ustawienia dla aplikacji Apple
88 appleWebApp: {
89 title: 'MQ Store',
90 statusBarStyle: 'black-translucent',
91 capable: true,
92 },
93
94 // Inne
95 formatDetection: {
96 telephone: true,
97 date: false,
98 address: true,
99 email: true,
100 url: true,
101 },
102};
103
104// Kolor motywu - w osobnym eksporcie viewport, bo themeColor w metadata jest przestarzałe
105export const viewport: Viewport = {
106 themeColor: [
107 { media: '(prefers-color-scheme: light)', color: '#3a86ff' },
108 { media: '(prefers-color-scheme: dark)', color: '#1e429f' },
109 ],
110};Metadane dla strony produktu
1// app/products/[id]/page.tsx
2import { Metadata } from 'next';
3
4interface Props {
5 params: Promise<{ id: string }>;
6}
7
8export async function generateMetadata({ params }: Props): Promise<Metadata> {
9 const product = await getProductData((await params).id);
10
11 const price = new Intl.NumberFormat('pl-PL', {
12 style: 'currency',
13 currency: 'PLN',
14 }).format(product.price);
15
16 return {
17 title: product.name,
18 description: product.description,
19 openGraph: {
20 title: product.name,
21 description: product.description,
22 type: 'product',
23 images: [
24 {
25 url: product.images[0].url,
26 width: 800,
27 height: 600,
28 alt: product.name,
29 },
30 ],
31 availability: product.inStock ? 'in stock' : 'out of stock',
32 price: {
33 amount: product.price.toString(),
34 currency: 'PLN',
35 },
36 },
37 };
38}
39
40export default async function ProductPage({ params }: Props) {
41 const product = await getProductData((await params).id);
42
43 const jsonLdData = {
44 '@context': 'https://schema.org',
45 '@type': 'Product',
46 name: product.name,
47 description: product.description,
48 image: product.images.map((img) => img.url),
49 offers: {
50 '@type': 'Offer',
51 price: product.price,
52 priceCurrency: 'PLN',
53 availability: product.inStock
54 ? 'https://schema.org/InStock'
55 : 'https://schema.org/OutOfStock',
56 url: `https://store.metropolisquantum.pl/products/${(await params).id}`,
57 },
58 brand: {
59 '@type': 'Brand',
60 name: 'Metropolis Quantum',
61 },
62 aggregateRating: {
63 '@type': 'AggregateRating',
64 ratingValue: product.rating.average,
65 reviewCount: product.rating.count,
66 },
67 };
68
69 return (
70 <div>
71 <script
72 type="application/ld+json"
73 dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLdData) }}
74 />
75 <h1>{product.name}</h1>
76 {/* Reszta zawartości strony produktu */}
77 </div>
78 );
79}
80
81async function getProductData(id: string) {
82 // W rzeczywistej aplikacji: fetch z API lub bazy danych
83 return {
84 id,
85 name: 'Kwantowy komunikator MQ-7',
86 description: 'Najnowszy komunikator wykorzystujący technologię kwantową do natychmiastowej komunikacji na dowolną odległość.',
87 price: 4999.99,
88 inStock: true,
89 images: [
90 { url: '/images/products/mq-7-main.jpg' },
91 { url: '/images/products/mq-7-side.jpg' },
92 { url: '/images/products/mq-7-back.jpg' },
93 ],
94 rating: {
95 average: 4.8,
96 count: 156,
97 },
98 };
99}Najlepsze praktyki
- Przygotuj pełen zestaw ikon - uwzględnij wszystkie wymagane rozmiary dla różnych platform i urządzeń
- Optymalizuj obrazy - kompresuj i optymalizuj wszystkie ikony i obrazy Open Graph
- Testuj na różnych platformach - sprawdź, jak Twoje metadane wyglądają na Facebooku, Twitterze, WhatsApp i innych platformach
- Używaj znaczących nazw plików - nazwij swoje pliki w sposób opisowy (np.
product-og-image.jpgzamiastimage1.jpg) - Dodaj strukturyzowane dane - używaj JSON-LD dla lepszego SEO
- Dostosuj metadane do każdej podstrony - generuj dynamiczne metadane dla każdej strony
- Pamiętaj o alternatywnych tekstach - zawsze dodawaj atrybut
altdo obrazów - Uwzględnij różne tryby (jasny/ciemny) - dostosuj kolory motywu do preferencji użytkownika
- Dostarczaj ikony w formacie SVG gdy to możliwe - dla lepszej skalowalności
Debugowanie metadanych
Narzędzia do testowania metadanych
- Facebook Sharing Debugger - https://developers.facebook.com/tools/debug/
- Twitter Card Validator - https://cards-dev.twitter.com/validator
- LinkedIn Post Inspector - https://www.linkedin.com/post-inspector/
- Rich Results Test (Google) - https://search.google.com/test/rich-results
Debugowanie w Next.js
Możesz sprawdzić wygenerowane metadane bezpośrednio w źródle HTML Twojej strony. Sprawdź sekcję <head> w narzędziach deweloperskich przeglądarki.
Podsumowanie
Ikony, favicon i metadane wizualne są kluczowym elementem identyfikacji wizualnej Twojej aplikacji Next.js 16. Podobnie jak w Metropolis Quantum, gdzie każdy element wizualny buduje spójny wizerunek miasta, tak w Twojej aplikacji starannie zaprojektowane ikony i metadane budują profesjonalny wizerunek i zwiększają rozpoznawalność.
Najważniejsze punkty:
- Używaj API metadanych Next.js 16 - upraszcza zarządzanie wszystkimi metadanymi
- Przygotuj kompletny zestaw ikon - dla różnych platform i urządzeń
- Konfiguruj Open Graph i Twitter Cards - dla lepszej widoczności w mediach społecznościowych
- Dodaj strukturyzowane dane - dla lepszego SEO i bogatszych wyników wyszukiwania
- Generuj dynamiczne metadane - dostosowane do każdej podstrony
- Testuj swoje metadane - sprawdź, jak wyglądają na różnych platformach
W następnym rozdziale zajmiemy się animacjami i przejściami w interfejsie użytkownika, które dodają dynamiki i interaktywności aplikacjom Next.js 16, podobnie jak holograficzne efekty i płynne animacje dodają życia architekturze Metropolis Quantum.
Kod do tej lekcji: app/page.tsx
1// CSS Animations & Transitions - Cyberpunk Metropolis
2'use client';
3
4export default function AnimationsDemo() {
5 return (
6 <div style={{
7 minHeight: '100vh',
8 background: 'linear-gradient(135deg, #0f0f23, #1a1a2e)',
9 color: '#ffffff',
10 padding: '3rem 2rem'
11 }}>
12 <h1 style={{
13 fontSize: '3rem',
14 textAlign: 'center',
15 background: 'linear-gradient(45deg, #64ffda, #7c4dff)',
16 WebkitBackgroundClip: 'text',
17 WebkitTextFillColor: 'transparent',
18 marginBottom: '3rem',
19 animation: 'fadeIn 1s ease-out'
20 }}>
21 Animations & Transitions
22 </h1>
23
24 {/* Transition Examples */}
25 <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)'}}>
26 <h2 style={{color: '#64ffda', marginBottom: '2rem'}}>CSS Transitions</h2>
27 <div style={{display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(200px, 1fr))', gap: '2rem'}}>
28 <div style={{background: '#64ffda', padding: '2rem', borderRadius: '0.75rem', textAlign: 'center', color: '#0f0f23', fontWeight: 600, transition: 'all 0.3s ease', cursor: 'pointer'}} onMouseEnter={e => e.currentTarget.style.transform = 'translateY(-10px)'} onMouseLeave={e => e.currentTarget.style.transform = 'translateY(0)'}>
29 Hover me!
30 </div>
31 <div style={{background: 'rgba(124,77,255,0.2)', padding: '2rem', borderRadius: '0.75rem', textAlign: 'center', border: '2px solid #7c4dff', color: '#7c4dff', fontWeight: 600, transition: 'all 0.5s cubic-bezier(0.68, -0.55, 0.265, 1.55)', cursor: 'pointer'}} onMouseEnter={e => { e.currentTarget.style.transform = 'scale(1.1) rotate(5deg)'; e.currentTarget.style.background = 'rgba(124,77,255,0.4)'; }} onMouseLeave={e => { e.currentTarget.style.transform = 'scale(1) rotate(0deg)'; e.currentTarget.style.background = 'rgba(124,77,255,0.2)'; }}>
32 Bounce Effect
33 </div>
34 </div>
35 </section>
36
37 {/* Keyframe Animations */}
38 <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)'}}>
39 <h2 style={{color: '#64ffda', marginBottom: '2rem'}}>Keyframe Animations</h2>
40
41 <div style={{display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: '3rem', textAlign: 'center'}}>
42 <div>
43 <div style={{width: '100px', height: '100px', margin: '0 auto 1rem', background: 'linear-gradient(135deg, #64ffda, #448aff)', borderRadius: '50%', animation: 'pulse 2s ease-in-out infinite'}}></div>
44 <p style={{color: '#b0bec5'}}>Pulse</p>
45 </div>
46
47 <div>
48 <div style={{width: '100px', height: '100px', margin: '0 auto 1rem', background: 'linear-gradient(135deg, #7c4dff, #ff0080)', borderRadius: '0.75rem', animation: 'rotate 3s linear infinite'}}></div>
49 <p style={{color: '#b0bec5'}}>Rotate</p>
50 </div>
51
52 <div>
53 <div style={{width: '100px', height: '100px', margin: '0 auto 1rem', background: 'linear-gradient(135deg, #00ff7f, #ffdc00)', borderRadius: '50% 0', animation: 'bounce 1.5s ease-in-out infinite'}}></div>
54 <p style={{color: '#b0bec5'}}>Bounce</p>
55 </div>
56 </div>
57 </section>
58
59 {/* Neon Glow Animation */}
60 <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)'}}>
61 <h2 style={{color: '#64ffda', marginBottom: '2rem'}}>Neon Glow Effect</h2>
62 <div style={{textAlign: 'center', padding: '3rem'}}>
63 <h3 style={{fontSize: '4rem', fontWeight: 800, color: '#64ffda', textShadow: '0 0 10px #64ffda, 0 0 20px #64ffda, 0 0 30px #64ffda', animation: 'neonPulse 2s ease-in-out infinite'}}>
64 QUANTUM
65 </h3>
66 <p style={{color: '#b0bec5', marginTop: '1rem', fontSize: '1.25rem', animation: 'fadeIn 2s ease-out'}}>
67 Cyberpunk 2150
68 </p>
69 </div>
70 </section>
71
72 {/* Loading Animations */}
73 <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)'}}>
74 <h2 style={{color: '#64ffda', marginBottom: '2rem'}}>Loading Animations</h2>
75 <div style={{display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: '3rem', textAlign: 'center'}}>
76 <div>
77 <div style={{width: '50px', height: '50px', margin: '0 auto 1rem', border: '4px solid rgba(100,255,218,0.3)', borderTop: '4px solid #64ffda', borderRadius: '50%', animation: 'spin 1s linear infinite'}}></div>
78 <p style={{color: '#b0bec5'}}>Spinner</p>
79 </div>
80
81 <div>
82 <div style={{display: 'flex', gap: '8px', justifyContent: 'center', marginBottom: '1rem'}}>
83 <div style={{width: '12px', height: '12px', background: '#64ffda', borderRadius: '50%', animation: 'dotPulse 1.4s ease-in-out infinite', animationDelay: '0s'}}></div>
84 <div style={{width: '12px', height: '12px', background: '#7c4dff', borderRadius: '50%', animation: 'dotPulse 1.4s ease-in-out infinite', animationDelay: '0.2s'}}></div>
85 <div style={{width: '12px', height: '12px', background: '#ff0080', borderRadius: '50%', animation: 'dotPulse 1.4s ease-in-out infinite', animationDelay: '0.4s'}}></div>
86 </div>
87 <p style={{color: '#b0bec5'}}>Dots</p>
88 </div>
89
90 <div>
91 <div style={{width: '100%', height: '4px', background: 'rgba(100,255,218,0.2)', borderRadius: '2px', overflow: 'hidden', margin: '0 auto 1rem'}}>
92 <div style={{width: '50%', height: '100%', background: 'linear-gradient(90deg, #64ffda, #7c4dff)', borderRadius: '2px', animation: 'progress 2s ease-in-out infinite'}}></div>
93 </div>
94 <p style={{color: '#b0bec5'}}>Progress</p>
95 </div>
96 </div>
97 </section>
98
99 <style jsx global>{`
100 @keyframes fadeIn {
101 from { opacity: 0; transform: translateY(20px); }
102 to { opacity: 1; transform: translateY(0); }
103 }
104
105 @keyframes pulse {
106 0%, 100% { transform: scale(1); opacity: 1; }
107 50% { transform: scale(1.1); opacity: 0.8; }
108 }
109
110 @keyframes rotate {
111 from { transform: rotate(0deg); }
112 to { transform: rotate(360deg); }
113 }
114
115 @keyframes bounce {
116 0%, 100% { transform: translateY(0); }
117 50% { transform: translateY(-30px); }
118 }
119
120 @keyframes neonPulse {
121 0%, 100% { text-shadow: 0 0 10px #64ffda, 0 0 20px #64ffda, 0 0 30px #64ffda; }
122 50% { text-shadow: 0 0 20px #64ffda, 0 0 40px #64ffda, 0 0 60px #64ffda; }
123 }
124
125 @keyframes spin {
126 to { transform: rotate(360deg); }
127 }
128
129 @keyframes dotPulse {
130 0%, 100% { transform: scale(0.8); opacity: 0.5; }
131 50% { transform: scale(1.2); opacity: 1; }
132 }
133
134 @keyframes progress {
135 0% { transform: translateX(-100%); }
136 100% { transform: translateX(200%); }
137 }
138 `}</style>
139 </div>
140 );
141}Sprawdź się
Odpowiedz na pytania z tej lekcji. Wybierz odpowiedź, a od razu zobaczysz, czy jest poprawna.
1. Dark mode w aplikacjach najlepiej implementować używając:
2. CSS Container Queries pozwalają na:
To 2 z 13 pytań do tej lekcji. Pozostałe rozwiążesz w grze.
Zadania praktyczne w grze
- Układanie w pionie
Ułóż etapy konfiguracji i ładowania czcionek w Next.js (next/font) w prawidłowej kolejności:
- Układanie w pionie
Ułóż kroki implementacji dark mode w aplikacji Next.js w prawidłowej kolejności:
- Układanie w pionie
Ułóż elementy w prawidłowej kolejności: 'use server' → async function → Componen
- Układanie w pionie
Ułóż elementy w prawidłowej kolejności: 'use client' → import { useState } → fun
- Układanie w pionie
Ułóż kroki ręcznej konfiguracji Tailwind CSS 3 w projekcie Next.js