Kurs Next.js · Moduł 3: Stylowanie i optymalizacja wizualna
Ikony, favicon i metadane wizualne w Next.js 16
W tej lekcji14
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:
public/
├── favicon.ico # Klasyczny favicon (16x16 lub 32x32)
├── icon.png # Ikona główna (można użyć w metadata API)
├── apple-icon.png # Ikona dla urządzeń Apple
└── icons/ # Katalog z różnymi rozmiarami ikon
├── icon-16x16.png
├── icon-32x32.png
├── icon-192x192.png
└── 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:
// app/layout.tsx
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="pl">
<body>{children}</body>
</html>
);
}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:
// app/layout.tsx
import { Metadata } from 'next';
export const metadata: Metadata = {
title: 'Metropolis Quantum',
description: 'Oficjalny portal miasta przyszłości',
icons: {
icon: '/icon.png', // lub array: [{ url: '/icon-16x16.png', sizes: '16x16' }, ...]
shortcut: '/shortcut-icon.png',
apple: '/apple-icon.png', // lub array: [{ url: '/apple-icon.png' }, ...]
other: {
rel: 'apple-touch-icon-precomposed',
url: '/apple-touch-icon-precomposed.png',
},
},
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="pl">
<body>{children}</body>
</html>
);
}Zaawansowana konfiguracja ikon
Dla profesjonalnych aplikacji warto przygotować kompletny zestaw ikon w różnych rozmiarach i formatach.
Rozszerzona konfiguracja ikon
// app/layout.tsx
import { Metadata } from 'next';
export const metadata: Metadata = {
icons: {
icon: [
{ url: '/icons/icon-16x16.png', sizes: '16x16', type: 'image/png' },
{ url: '/icons/icon-32x32.png', sizes: '32x32', type: 'image/png' },
{ url: '/icons/icon-192x192.png', sizes: '192x192', type: 'image/png' },
{ url: '/icons/icon-512x512.png', sizes: '512x512', type: 'image/png' },
],
shortcut: '/shortcut-icon.png',
apple: [
{ url: '/icons/apple-icon-57x57.png', sizes: '57x57', type: 'image/png' },
{ url: '/icons/apple-icon-72x72.png', sizes: '72x72', type: 'image/png' },
{ url: '/icons/apple-icon-114x114.png', sizes: '114x114', type: 'image/png' },
{ url: '/icons/apple-icon-180x180.png', sizes: '180x180', type: 'image/png' },
],
},
};Rezultat w HTML
Powyższa konfiguracja wygeneruje następujące tagi w sekcji <head> dokumentu HTML:
<link rel="icon" href="/icons/icon-16x16.png" sizes="16x16" type="image/png" />
<link rel="icon" href="/icons/icon-32x32.png" sizes="32x32" type="image/png" />
<link rel="icon" href="/icons/icon-192x192.png" sizes="192x192" type="image/png" />
<link rel="icon" href="/icons/icon-512x512.png" sizes="512x512" type="image/png" />
<link rel="shortcut icon" href="/shortcut-icon.png" />
<link rel="apple-touch-icon" href="/icons/apple-icon-57x57.png" sizes="57x57" type="image/png" />
<link rel="apple-touch-icon" href="/icons/apple-icon-72x72.png" sizes="72x72" type="image/png" />
<link rel="apple-touch-icon" href="/icons/apple-icon-114x114.png" sizes="114x114" type="image/png" />
<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
// app/layout.tsx
import { Metadata } from 'next';
export const metadata: Metadata = {
title: 'Metropolis Quantum',
description: 'Oficjalny portal miasta przyszłości',
openGraph: {
title: 'Metropolis Quantum - Miasto Przyszłości',
description: 'Odkryj technologię jutra w mieście Metropolis Quantum',
url: 'https://metropolisquantum.pl',
siteName: 'Metropolis Quantum',
images: [
{
url: '/images/og-image.jpg', // 1200x630 px rekomendowany rozmiar
width: 1200,
height: 630,
alt: 'Metropolis Quantum - Panorama miasta przyszłości',
},
],
locale: 'pl_PL',
type: 'website',
},
};Konfiguracja Twitter Cards
// app/layout.tsx
import { Metadata } from 'next';
export const metadata: Metadata = {
twitter: {
card: 'summary_large_image',
title: 'Metropolis Quantum - Miasto Przyszłości',
description: 'Odkryj technologię jutra w mieście Metropolis Quantum',
siteId: '1467726470533754880', // ID konta Twitter
creator: '@MetropolisQ',
creatorId: '1467726470533754880',
images: ['/images/twitter-image.jpg'], // 800x418 px rekomendowany rozmiar
},
};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
// app/districts/[id]/page.tsx
import { Metadata, ResolvingMetadata } from 'next';
interface Props {
params: Promise<{ id: string }>;
searchParams: Promise<{ [key: string]: string | string[] | undefined }>;
}
export async function generateMetadata(
{ params, searchParams }: Props,
parent: ResolvingMetadata
): Promise<Metadata> {
// Pobierz dane o dzielnicy
const districtId = (await params).id;
const district = await getDistrictData(districtId);
// Opcjonalnie możesz połączyć z nadrzędnymi metadanymi
const previousImages = (await parent).openGraph?.images || [];
return {
title: `${district.name} | Metropolis Quantum`,
description: district.description,
openGraph: {
images: [
{
url: `/images/districts/${districtId}.jpg`,
width: 1200,
height: 630,
alt: district.name,
},
...previousImages,
],
},
twitter: {
images: [`/images/districts/${districtId}-twitter.jpg`],
},
};
}
export default async function DistrictPage({ params }: Props) {
// Renderowanie strony
}
// Funkcja pobierająca dane o dzielnicy
async function getDistrictData(id: string) {
// W rzeczywistej aplikacji: fetch z API lub bazy danych
return {
name: 'Dystrykt Kwantowy',
description: 'Najbardziej zaawansowana technologicznie dzielnica Metropolis Quantum.',
};
}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:
{
"name": "Metropolis Quantum",
"short_name": "MetroQ",
"description": "Oficjalny portal miasta przyszłości",
"start_url": "/",
"display": "standalone",
"background_color": "#121212",
"theme_color": "#3a86ff",
"icons": [
{
"src": "/icons/icon-192x192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "any maskable"
},
{
"src": "/icons/icon-512x512.png",
"sizes": "512x512",
"type": "image/png"
}
]
}Konfiguracja manifestu w Next.js
// app/layout.tsx
import { Metadata } from 'next';
export const metadata: Metadata = {
// Inne metadane...
// Manifest dla PWA
manifest: '/manifest.json',
// Lub zdefiniuj manifest bezpośrednio:
/*
manifest: {
name: 'Metropolis Quantum',
short_name: 'MetroQ',
description: 'Oficjalny portal miasta przyszłości',
start_url: '/',
display: 'standalone',
background_color: '#121212',
theme_color: '#3a86ff',
icons: [
{
src: '/icons/icon-192x192.png',
sizes: '192x192',
type: 'image/png',
purpose: 'any maskable'
},
{
src: '/icons/icon-512x512.png',
sizes: '512x512',
type: 'image/png'
}
]
}
*/
};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:
// app/layout.tsx
import type { Viewport } from 'next';
export const viewport: Viewport = {
// Kolor motywu dla przeglądarek mobilnych
themeColor: [
{ media: '(prefers-color-scheme: light)', color: '#3a86ff' },
{ media: '(prefers-color-scheme: dark)', color: '#1e429f' },
],
// Lub pojedynczy kolor:
// themeColor: '#3a86ff',
};Obsługa Apple-specific
// app/layout.tsx
import { Metadata } from 'next';
export const metadata: Metadata = {
// Inne metadane...
// Ustawienia dla Safari na iOS
appleWebApp: {
title: 'Metropolis Quantum', // Alternatywny tytuł dla iOS
statusBarStyle: 'black-translucent',
startupImage: [
{
url: '/startup/apple-startup-2048x2732.png',
media: '(device-width: 1024px) and (device-height: 1366px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)',
},
{
url: '/startup/apple-startup-1668x2388.png',
media: '(device-width: 834px) and (device-height: 1194px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)',
},
// Więcej rozmiarów...
],
},
};Aplikacja mobilna alternatywna
// app/layout.tsx
import { Metadata } from 'next';
export const metadata: Metadata = {
// Inne metadane...
// Linki do aplikacji mobilnych
alternates: {
'android-app': 'android-app://com.metropolisquantum/https/metropolisquantum.pl',
'ios-app': 'ios-app://1234567890/https/metropolisquantum.pl',
},
};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
# Instalacja
npm i -g pwa-asset-generator
# Generowanie ikon
npx 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
// app/api/og/route.tsx
import { ImageResponse } from 'next/og';
import { NextRequest } from 'next/server';
// Bez eksportu runtime = 'edge': w Next.js 16 ImageResponse działa w domyślnym środowisku Node.js,
// a Edge Runtime jest w dokumentacji oznaczony jako przestarzały
export async function GET(request: NextRequest) {
const { searchParams } = new URL(request.url);
// Pobierz parametry z URL
const title = searchParams.get('title') || 'Metropolis Quantum';
const description = searchParams.get('description') || 'Miasto przyszłości';
// Opcjonalnie: pobierz dodatkowe dane z bazy lub API
// const data = await fetchData();
// Wygeneruj obraz
return new ImageResponse(
(
<div
style={{
display: 'flex',
fontSize: 40,
color: 'white',
background: 'linear-gradient(to bottom, #3a86ff, #1e429f)',
width: '100%',
height: '100%',
padding: '50px 200px',
textAlign: 'center',
justifyContent: 'center',
alignItems: 'center',
flexDirection: 'column',
}}
>
<img
src={`${request.nextUrl.origin}/logo.png`}
alt="Logo"
width="200"
height="200"
/>
<h1 style={{ fontSize: 70 }}>{title}</h1>
<p style={{ fontSize: 40 }}>{description}</p>
</div>
),
{
width: 1200,
height: 630,
}
);
}Użycie dynamicznego obrazu OG w metadanych
// app/districts/[id]/page.tsx
import { Metadata } from 'next';
interface Props {
params: Promise<{ id: string }>;
}
export async function generateMetadata({ params }: Props): Promise<Metadata> {
const district = await getDistrictData((await params).id);
const ogImageUrl = new URL(`/api/og`, 'https://metropolisquantum.pl');
ogImageUrl.searchParams.append('title', district.name);
ogImageUrl.searchParams.append('description', district.description);
return {
title: `${district.name} | Metropolis Quantum`,
description: district.description,
openGraph: {
images: [ogImageUrl.toString()],
},
twitter: {
images: [ogImageUrl.toString()],
},
};
}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
// app/layout.tsx
import { Metadata } from 'next';
export const metadata: Metadata = {
// Inne metadane...
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="pl">
<head>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{
__html: JSON.stringify({
'@context': 'https://schema.org',
'@type': 'Organization',
name: 'Metropolis Quantum',
url: 'https://metropolisquantum.pl',
logo: 'https://metropolisquantum.pl/logo.png',
contactPoint: {
'@type': 'ContactPoint',
telephone: '+48-123-456-789',
contactType: 'customer service',
},
sameAs: [
'https://facebook.com/metropolisquantum',
'https://twitter.com/metropolisq',
'https://instagram.com/metropolisquantum',
],
}),
}}
/>
</head>
<body>{children}</body>
</html>
);
}Dynamiczne JSON-LD dla podstron
// components/JsonLd.tsx
interface JsonLdProps {
data: any;
}
export function JsonLd({ data }: JsonLdProps) {
return (
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(data) }}
/>
);
}// app/districts/[id]/page.tsx
import { JsonLd } from '@/components/JsonLd';
export default async function DistrictPage({ params }: { params: Promise<{ id: string }> }) {
const district = await getDistrictData((await params).id);
const jsonLdData = {
'@context': 'https://schema.org',
'@type': 'Place',
name: district.name,
description: district.description,
address: {
'@type': 'PostalAddress',
addressLocality: 'Metropolis Quantum',
addressRegion: 'MQ',
postalCode: '00-000',
addressCountry: 'PL',
},
geo: {
'@type': 'GeoCoordinates',
latitude: district.coordinates.lat,
longitude: district.coordinates.lng,
},
image: `https://metropolisquantum.pl/images/districts/${(await params).id}.jpg`,
};
return (
<div>
<JsonLd data={jsonLdData} />
<h1>{district.name}</h1>
{/* Reszta zawartości */}
</div>
);
}Przykłady kompletnej implementacji
Metadane dla aplikacji e-commerce
// app/layout.tsx
import type { Metadata, Viewport } from 'next';
export const metadata: Metadata = {
metadataBase: new URL('https://metropolisquantum.pl'),
title: {
default: 'Metropolis Quantum Store',
template: '%s | Metropolis Quantum Store',
},
description: 'Sklep z najnowszymi technologiami przyszłości',
keywords: ['technologia', 'przyszłość', 'metropolis', 'quantum', 'sklep'],
authors: [{ name: 'Metropolis Quantum', url: 'https://metropolisquantum.pl' }],
creator: 'Zespół Metropolis Quantum',
publisher: 'Metropolis Quantum Corporation',
// Ikony
icons: {
icon: [
{ url: '/icons/icon-16x16.png', sizes: '16x16', type: 'image/png' },
{ url: '/icons/icon-32x32.png', sizes: '32x32', type: 'image/png' },
{ url: '/icons/icon-192x192.png', sizes: '192x192', type: 'image/png' },
{ url: '/icons/icon-512x512.png', sizes: '512x512', type: 'image/png' },
],
apple: [
{ url: '/icons/apple-icon.png' },
],
},
// Open Graph
openGraph: {
type: 'website',
siteName: 'Metropolis Quantum Store',
title: 'Metropolis Quantum Store - Technologia przyszłości już dziś',
description: 'Sklep z najnowszymi technologiami przyszłości z Metropolis Quantum',
url: 'https://store.metropolisquantum.pl',
images: [
{
url: '/images/og-default.jpg',
width: 1200,
height: 630,
alt: 'Metropolis Quantum Store',
},
],
locale: 'pl_PL',
},
// Twitter
twitter: {
card: 'summary_large_image',
title: 'Metropolis Quantum Store',
description: 'Technologia przyszłości już dziś',
images: ['/images/twitter-image.jpg'],
creator: '@MetropolisQ',
},
// Robots
robots: {
index: true,
follow: true,
googleBot: {
index: true,
follow: true,
'max-video-preview': -1,
'max-image-preview': 'large',
'max-snippet': -1,
},
},
// Manifest PWA
manifest: '/manifest.json',
// Alternatywne aplikacje
alternates: {
canonical: 'https://store.metropolisquantum.pl',
languages: {
'en-US': 'https://store.metropolisquantum.pl/en',
'de-DE': 'https://store.metropolisquantum.pl/de',
},
media: {
'only screen and (max-width: 640px)': 'https://m.store.metropolisquantum.pl',
},
types: {
'application/rss+xml': 'https://store.metropolisquantum.pl/rss',
},
},
// Ustawienia dla aplikacji Apple
appleWebApp: {
title: 'MQ Store',
statusBarStyle: 'black-translucent',
capable: true,
},
// Inne
formatDetection: {
telephone: true,
date: false,
address: true,
email: true,
url: true,
},
};
// Kolor motywu - w osobnym eksporcie viewport, bo themeColor w metadata jest przestarzałe
export const viewport: Viewport = {
themeColor: [
{ media: '(prefers-color-scheme: light)', color: '#3a86ff' },
{ media: '(prefers-color-scheme: dark)', color: '#1e429f' },
],
};Metadane dla strony produktu
// app/products/[id]/page.tsx
import { Metadata } from 'next';
interface Props {
params: Promise<{ id: string }>;
}
export async function generateMetadata({ params }: Props): Promise<Metadata> {
const product = await getProductData((await params).id);
const price = new Intl.NumberFormat('pl-PL', {
style: 'currency',
currency: 'PLN',
}).format(product.price);
return {
title: product.name,
description: product.description,
openGraph: {
title: product.name,
description: product.description,
type: 'product',
images: [
{
url: product.images[0].url,
width: 800,
height: 600,
alt: product.name,
},
],
availability: product.inStock ? 'in stock' : 'out of stock',
price: {
amount: product.price.toString(),
currency: 'PLN',
},
},
};
}
export default async function ProductPage({ params }: Props) {
const product = await getProductData((await params).id);
const jsonLdData = {
'@context': 'https://schema.org',
'@type': 'Product',
name: product.name,
description: product.description,
image: product.images.map((img) => img.url),
offers: {
'@type': 'Offer',
price: product.price,
priceCurrency: 'PLN',
availability: product.inStock
? 'https://schema.org/InStock'
: 'https://schema.org/OutOfStock',
url: `https://store.metropolisquantum.pl/products/${(await params).id}`,
},
brand: {
'@type': 'Brand',
name: 'Metropolis Quantum',
},
aggregateRating: {
'@type': 'AggregateRating',
ratingValue: product.rating.average,
reviewCount: product.rating.count,
},
};
return (
<div>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLdData) }}
/>
<h1>{product.name}</h1>
{/* Reszta zawartości strony produktu */}
</div>
);
}
async function getProductData(id: string) {
// W rzeczywistej aplikacji: fetch z API lub bazy danych
return {
id,
name: 'Kwantowy komunikator MQ-7',
description: 'Najnowszy komunikator wykorzystujący technologię kwantową do natychmiastowej komunikacji na dowolną odległość.',
price: 4999.99,
inStock: true,
images: [
{ url: '/images/products/mq-7-main.jpg' },
{ url: '/images/products/mq-7-side.jpg' },
{ url: '/images/products/mq-7-back.jpg' },
],
rating: {
average: 4.8,
count: 156,
},
};
}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
// CSS Animations & Transitions - Cyberpunk Metropolis
'use client';
export default function AnimationsDemo() {
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',
animation: 'fadeIn 1s ease-out'
}}>
Animations & Transitions
</h1>
{/* Transition Examples */}
<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: '2rem'}}>CSS Transitions</h2>
<div style={{display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(200px, 1fr))', gap: '2rem'}}>
<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)'}>
Hover me!
</div>
<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)'; }}>
Bounce Effect
</div>
</div>
</section>
{/* Keyframe Animations */}
<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: '2rem'}}>Keyframe Animations</h2>
<div style={{display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: '3rem', textAlign: 'center'}}>
<div>
<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>
<p style={{color: '#b0bec5'}}>Pulse</p>
</div>
<div>
<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>
<p style={{color: '#b0bec5'}}>Rotate</p>
</div>
<div>
<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>
<p style={{color: '#b0bec5'}}>Bounce</p>
</div>
</div>
</section>
{/* Neon Glow Animation */}
<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: '2rem'}}>Neon Glow Effect</h2>
<div style={{textAlign: 'center', padding: '3rem'}}>
<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'}}>
QUANTUM
</h3>
<p style={{color: '#b0bec5', marginTop: '1rem', fontSize: '1.25rem', animation: 'fadeIn 2s ease-out'}}>
Cyberpunk 2150
</p>
</div>
</section>
{/* Loading Animations */}
<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: '2rem'}}>Loading Animations</h2>
<div style={{display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: '3rem', textAlign: 'center'}}>
<div>
<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>
<p style={{color: '#b0bec5'}}>Spinner</p>
</div>
<div>
<div style={{display: 'flex', gap: '8px', justifyContent: 'center', marginBottom: '1rem'}}>
<div style={{width: '12px', height: '12px', background: '#64ffda', borderRadius: '50%', animation: 'dotPulse 1.4s ease-in-out infinite', animationDelay: '0s'}}></div>
<div style={{width: '12px', height: '12px', background: '#7c4dff', borderRadius: '50%', animation: 'dotPulse 1.4s ease-in-out infinite', animationDelay: '0.2s'}}></div>
<div style={{width: '12px', height: '12px', background: '#ff0080', borderRadius: '50%', animation: 'dotPulse 1.4s ease-in-out infinite', animationDelay: '0.4s'}}></div>
</div>
<p style={{color: '#b0bec5'}}>Dots</p>
</div>
<div>
<div style={{width: '100%', height: '4px', background: 'rgba(100,255,218,0.2)', borderRadius: '2px', overflow: 'hidden', margin: '0 auto 1rem'}}>
<div style={{width: '50%', height: '100%', background: 'linear-gradient(90deg, #64ffda, #7c4dff)', borderRadius: '2px', animation: 'progress 2s ease-in-out infinite'}}></div>
</div>
<p style={{color: '#b0bec5'}}>Progress</p>
</div>
</div>
</section>
<style jsx global>{`
@keyframes fadeIn {
from { opacity: 0; transform: translateY(20px); }
to { opacity: 1; transform: translateY(0); }
}
@keyframes pulse {
0%, 100% { transform: scale(1); opacity: 1; }
50% { transform: scale(1.1); opacity: 0.8; }
}
@keyframes rotate {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
@keyframes bounce {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-30px); }
}
@keyframes neonPulse {
0%, 100% { text-shadow: 0 0 10px #64ffda, 0 0 20px #64ffda, 0 0 30px #64ffda; }
50% { text-shadow: 0 0 20px #64ffda, 0 0 40px #64ffda, 0 0 60px #64ffda; }
}
@keyframes spin {
to { transform: rotate(360deg); }
}
@keyframes dotPulse {
0%, 100% { transform: scale(0.8); opacity: 0.5; }
50% { transform: scale(1.2); opacity: 1; }
}
@keyframes progress {
0% { transform: translateX(-100%); }
100% { transform: translateX(200%); }
}
`}</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. 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