Kurs Next.js · Moduł 3: Stylowanie i optymalizacja wizualna

Ikony, favicon i metadane wizualne w Next.js 16

15 min czytania ·
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.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:

// 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

  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

# Instalacja
npm i -g pwa-asset-generator

# Generowanie ikon
npx 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

// 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

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