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 15 oferuje rozbudowane narzędzia do zarządzania tymi elementami, które poznamy w tym rozdziale.
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:
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:
Next.js 15 wprowadza uproszczony sposób konfiguracji favicon i ikon przez API metadanych. Zacznijmy od podstawowej konfiguracji.
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.pngNajprostszy 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.Next.js 15 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}Dla profesjonalnych aplikacji warto przygotować kompletny zestaw ikon w różnych rozmiarach i formatach.
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};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 określają, jak Twoja strona wygląda, gdy jest udostępniana na platformach społecznościowych. Next.js 15 oferuje wbudowane wsparcie dla tych metadanych.
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};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};Po wdrożeniu możesz przetestować swoje metadane za pomocą:
W Next.js 15 możesz generować metadane dynamicznie, na podstawie ścieżki, parametrów URL lub danych zewnętrznych.
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 to plik JSON, który dostarcza informacji o Twojej aplikacji dla przeglądarek i urządzeń mobilnych. Jest kluczowy dla Progressive Web Apps (PWA).
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}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};1// app/layout.tsx
2import { Metadata } from 'next';
3
4export const metadata: Metadata = {
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};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};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};Ręczne tworzenie wszystkich rozmiarów ikon może być żmudne. Na szczęście istnieją narzędzia, które automatyzują ten proces.
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 --faviconCzasem warto dynamicznie generować obrazy Open Graph dla różnych podstron. W Next.js 15 możemy to zrobić za pomocą Route Handlers.
1// app/api/og/route.tsx
2import { ImageResponse } from 'next/og';
3import { NextRequest } from 'next/server';
4
5export const runtime = 'edge';
6
7export async function GET(request: NextRequest) {
8 const { searchParams } = new URL(request.url);
9
10 // Pobierz parametry z URL
11 const title = searchParams.get('title') || 'Metropolis Quantum';
12 const description = searchParams.get('description') || 'Miasto przyszłości';
13
14 // Opcjonalnie: pobierz dodatkowe dane z bazy lub API
15 // const data = await fetchData();
16
17 // Wygeneruj obraz
18 return new ImageResponse(
19 (
20 <div
21 style={{
22 display: 'flex',
23 fontSize: 40,
24 color: 'white',
25 background: 'linear-gradient(to bottom, #3a86ff, #1e429f)',
26 width: '100%',
27 height: '100%',
28 padding: '50px 200px',
29 textAlign: 'center',
30 justifyContent: 'center',
31 alignItems: 'center',
32 flexDirection: 'column',
33 }}
34 >
35 <img
36 src={`${request.nextUrl.origin}/logo.png`}
37 alt="Logo"
38 width="200"
39 height="200"
40 />
41 <h1 style={{ fontSize: 70 }}>{title}</h1>
42 <p style={{ fontSize: 40 }}>{description}</p>
43 </div>
44 ),
45 {
46 width: 1200,
47 height: 630,
48 }
49 );
50}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 pomagają wyszukiwarkom lepiej rozumieć zawartość Twojej strony i mogą wpłynąć na sposób wyświetlania wyników wyszukiwania.
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}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}1// app/layout.tsx
2import { Metadata } 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 // Kolor motywu
70 themeColor: [
71 { media: '(prefers-color-scheme: light)', color: '#3a86ff' },
72 { media: '(prefers-color-scheme: dark)', color: '#1e429f' },
73 ],
74
75 // Manifest PWA
76 manifest: '/manifest.json',
77
78 // Alternatywne aplikacje
79 alternates: {
80 canonical: 'https://store.metropolisquantum.pl',
81 languages: {
82 'en-US': 'https://store.metropolisquantum.pl/en',
83 'de-DE': 'https://store.metropolisquantum.pl/de',
84 },
85 media: {
86 'only screen and (max-width: 640px)': 'https://m.store.metropolisquantum.pl',
87 },
88 types: {
89 'application/rss+xml': 'https://store.metropolisquantum.pl/rss',
90 },
91 },
92
93 // Ustawienia dla aplikacji Apple
94 appleWebApp: {
95 title: 'MQ Store',
96 statusBarStyle: 'black-translucent',
97 capable: true,
98 },
99
100 // Inne
101 formatDetection: {
102 telephone: true,
103 date: false,
104 address: true,
105 email: true,
106 url: true,
107 },
108};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}product-og-image.jpg zamiast image1.jpg)alt do obrazówMożesz sprawdzić wygenerowane metadane bezpośrednio w źródle HTML Twojej strony. Sprawdź sekcję
<head> w narzędziach deweloperskich przeglądarki.Ikony, favicon i metadane wizualne są kluczowym elementem identyfikacji wizualnej Twojej aplikacji Next.js 15. 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:
W następnym rozdziale zajmiemy się animacjami i przejściami w interfejsie użytkownika, które dodają dynamiki i interaktywności aplikacjom Next.js 15, podobnie jak holograficzne efekty i płynne animacje dodają życia architekturze Metropolis Quantum.