CodeWorlds

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.png

Konfiguracja 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

  1. Favicon Generator (https://realfavicongenerator.net/) - generuje pełny zestaw ikon z jednego obrazu
  2. 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 --favicon

Dynamiczne 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

  1. Przygotuj pełen zestaw ikon - uwzględnij wszystkie wymagane rozmiary dla różnych platform i urządzeń
  2. Optymalizuj obrazy - kompresuj i optymalizuj wszystkie ikony i obrazy Open Graph
  3. Testuj na różnych platformach - sprawdź, jak Twoje metadane wyglądają na Facebooku, Twitterze, WhatsApp i innych platformach
  4. Używaj znaczących nazw plików - nazwij swoje pliki w sposób opisowy (np. product-og-image.jpg zamiast image1.jpg)
  5. Dodaj strukturyzowane dane - używaj JSON-LD dla lepszego SEO
  6. Dostosuj metadane do każdej podstrony - generuj dynamiczne metadane dla każdej strony
  7. Pamiętaj o alternatywnych tekstach - zawsze dodawaj atrybut alt do obrazów
  8. Uwzględnij różne tryby (jasny/ciemny) - dostosuj kolory motywu do preferencji użytkownika
  9. Dostarczaj ikony w formacie SVG gdy to możliwe - dla lepszej skalowalności

Debugowanie metadanych

Narzędzia do testowania metadanych

  1. Facebook Sharing Debugger - https://developers.facebook.com/tools/debug/
  2. Twitter Card Validator - https://cards-dev.twitter.com/validator
  3. LinkedIn Post Inspector - https://www.linkedin.com/post-inspector/
  4. 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:

  1. Używaj API metadanych Next.js 16 - upraszcza zarządzanie wszystkimi metadanymi
  2. Przygotuj kompletny zestaw ikon - dla różnych platform i urządzeń
  3. Konfiguruj Open Graph i Twitter Cards - dla lepszej widoczności w mediach społecznościowych
  4. Dodaj strukturyzowane dane - dla lepszego SEO i bogatszych wyników wyszukiwania
  5. Generuj dynamiczne metadane - dostosowane do każdej podstrony
  6. 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. 1. Dark mode w aplikacjach najlepiej implementować używając:

  2. 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

Przydatne artykuły