In Metropolis Quantum, the city's visual identity is carefully designed so that every element - from logos on government buildings, through district signage, to city applications - creates a consistent image. This visual identity helps residents and visitors recognize official city elements, builds trust, and creates a professional image.
In the world of web applications, icons, favicon, and visual metadata play a similar role. They are key to your application's recognizability, influence users' first impressions, and make it easier to identify your site among many open browser tabs. Next.js 15 offers extensive tools for managing these elements, which we will learn about in this chapter.
Favicon (short for "favorite icon") is a small icon displayed in the browser tab, bookmarks list, and browsing history. It is the first visual element that users associate with your application, so it is worth ensuring its quality and recognizability.
In modern web applications, we need not only a basic favicon but an entire set of icons in different sizes that will be used in different contexts:
Visual metadata is information that determines how your page is presented when shared on social media, messaging apps, or search results. It includes:
Next.js 15 introduces a simplified way of configuring favicon and icons through the Metadata API. Let's start with basic configuration.
First, place icon files in the
public directory:1public/
2├── favicon.ico # Klasyczny favicon (16x16 lub 32x32)
3├── icon.png # Main icon (can be used in metadata API)
4├── apple-icon.png # Icon for Apple devices
5└── icons/ # Directory with different icon sizes
6 ├── icon-16x16.png
7 ├── icon-32x32.png
8 ├── icon-192x192.png
9 └── icon-512x512.pngThe simplest way is to place a
favicon.ico file in the public directory. Next.js will automatically recognize this file and use it as the 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}In this case, you don't need to do anything else - the browser will automatically fetch
/favicon.ico.Next.js 15 offers advanced icon configuration through the Metadata API:
1// app/layout.tsx
2import { Metadata } from 'next';
3
4export const metadata: Metadata = {
5 title: 'Metropolis Quantum',
6 description: 'Official portal of the city of the future',
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}For professional applications, it is worth preparing a complete set of icons in different sizes and formats.
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};The above configuration will generate the following tags in the
<head> section of the HTML document: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" />Open Graph and Twitter Cards metadata determine how your page looks when shared on social platforms. Next.js 15 offers built-in support for this metadata.
1// app/layout.tsx
2import { Metadata } from 'next';
3
4export const metadata: Metadata = {
5 title: 'Metropolis Quantum',
6 description: 'Official portal of the city of the future',
7 openGraph: {
8 title: 'Metropolis Quantum - City of the Future',
9 description: 'Discover tomorrow's technology in the city of 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 of the city of the future',
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 - City of the Future',
8 description: 'Discover tomorrow's technology in the city of 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};After deployment, you can test your metadata using:
In Next.js 15, you can generate metadata dynamically based on path, URL parameters, or external data.
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 // Optionally you can merge with parent metadata
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// Function fetching district data
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}A web application manifest is a JSON file that provides information about your application for browsers and mobile devices. It is crucial for Progressive Web Apps (PWA).
Place the
manifest.json file in the public directory:1{
2 "name": "Metropolis Quantum",
3 "short_name": "MetroQ",
4 "description": "Official portal of the city of the future",
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 // Or define the manifest directly:
11 /*
12 manifest: {
13 name: 'Metropolis Quantum',
14 short_name: 'MetroQ',
15 description: 'Official portal of the city of the future',
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 // Theme color for mobile browsers
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', // Alternative title for 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 // More sizes...
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};Manually creating all icon sizes can be tedious. Fortunately, there are tools that automate this process.
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 --faviconSometimes it is worth dynamically generating Open Graph images for different subpages. In Next.js 15, we can do this using 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') || 'City of the future';
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}Structured data helps search engines better understand your page's content and can influence how search results are displayed.
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 {/* Rest of content */}
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: 'Store with the latest technologies of the future',
11 keywords: ['technology', 'future', 'metropolis', 'quantum', 'store'],
12 authors: [{ name: 'Metropolis Quantum', url: 'https://metropolisquantum.pl' }],
13 creator: 'Metropolis Quantum Team',
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 - Technology of the future today',
34 description: 'Store with the latest technologies of the future from 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: 'Technology of the future today',
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 {/* Rest of product page content */}
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: 'The latest communicator using quantum technology for instant communication over any distance.',
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 instead of image1.jpg)alt attribute to imagesYou can check the generated metadata directly in your page's HTML source. Check the
<head> section in the browser's developer tools.Icons, favicon, and visual metadata are a key element of your Next.js 15 application's visual identity. Just like in Metropolis Quantum, where every visual element builds a consistent image of the city, in your application carefully designed icons and metadata build a professional image and increase recognizability.
Key takeaways:
In the next chapter, we will focus on animations and transitions in the user interface, which add dynamism and interactivity to Next.js 15 applications, just like holographic effects and smooth animations bring the architecture of Metropolis Quantum to life.