W Metropolis Quantum, każda dzielnica ma swoją centralną bazę zasobów - miejsce, gdzie przechowywane są elementy dostępne dla wszystkich mieszkańców: mapy, broszury informacyjne, elementy identyfikacji wizualnej czy interaktywne holoekrany z planami miasta. Te zasoby są niezmienne i dostępne publicznie, zapewniając spójność i ciągłość doświadczenia w całym mieście.
W aplikacjach Next.js, rolę tej centralnej bazy pełni katalog
public. To właśnie tutaj umieszczamy wszystkie zasoby statyczne, które powinny być dostępne bez przetwarzania przez system budowania Next.js. W tym rozdziale przyjrzymy się, jak efektywnie zarządzać tymi zasobami, by nasza aplikacja była zarówno wydajna, jak i dobrze zorganizowana.Katalog
public to specjalny folder w aplikacji Next.js, który spełnia następujące funkcje:public są serwowane z głównego katalogu domenyKatalog ten jest idealny do przechowywania takich zasobów jak:
Dobrze zorganizowany katalog
public ułatwia zarządzanie zasobami. Oto przykładowa struktura:1public/
2├── favicon.ico # Ikona strony
3├── favicon-16x16.png # Alternatywne rozmiary favicon
4├── favicon-32x32.png
5├── apple-touch-icon.png # Ikona dla urządzeń Apple
6├── manifest.json # Manifest PWA
7├── robots.txt # Instrukcje dla botów
8├── sitemap.xml # Mapa strony dla wyszukiwarek
9├── fonts/ # Katalog z czcionkami
10│ ├── QuantumSans-Regular.woff2
11│ └── QuantumSans-Bold.woff2
12├── images/ # Katalog z obrazami
13│ ├── logo.png
14│ ├── hero-banner.jpg
15│ ├── icons/ # Podkatalog z ikonami
16│ │ ├── arrow-right.svg
17│ │ └── search.svg
18│ └── backgrounds/ # Podkatalog z tłami
19│ ├── quantum-pattern.svg
20│ └── districts/ # Dalsze podkatalogi dla organizacji
21│ ├── downtown.jpg
22│ └── quantum-district.jpg
23├── videos/ # Katalog z filmami
24│ └── intro.mp4
25└── data/ # Katalog z danymi statycznymi
26 ├── districts.json
27 └── services.jsonDostęp do zasobów z katalogu
public jest prosty i intuicyjny. Pamiętaj, że:/)1// Przykład użycia obrazu z katalogu public
2export default function Logo() {
3 return (
4 <img
5 src="/images/logo.png"
6 alt="Metropolis Quantum Logo"
7 width={200}
8 height={50}
9 />
10 );
11}1/* W pliku CSS */
2.hero-section {
3 background-image: url('/images/backgrounds/quantum-pattern.svg');
4 background-repeat: repeat;
5}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: [
9 { url: '/favicon-16x16.png', sizes: '16x16', type: 'image/png' },
10 { url: '/favicon-32x32.png', sizes: '32x32', type: 'image/png' },
11 ],
12 apple: [
13 { url: '/apple-touch-icon.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}Mimo że Next.js dostarcza zoptymalizowany komponent
Image z pakietu next/image, czasem potrzebujemy użyć obrazów bezpośrednio z katalogu public. Komponent Image można używać z obrazami z katalogu public, zachowując korzyści z optymalizacji:1import Image from 'next/image';
2
3export default function HeroBanner() {
4 return (
5 <div className="hero-banner">
6 <Image
7 src="/images/hero-banner.jpg"
8 alt="Witaj w Metropolis Quantum"
9 width={1200}
10 height={600}
11 priority
12 />
13 </div>
14 );
15}Gdy URL obrazu musi być znany podczas budowy:
Gdy obraz jest bardzo duży i rzadko używany:
Gdy chcesz mieć bezpośredni link do pliku:
Pliki SVG można umieszczać w katalogu
public i używać ich jako zwykłych obrazów:1export default function Icon() {
2 return (
3 <img src="/images/icons/search.svg" alt="Szukaj" width={24} height={24} />
4 );
5}Alternatywnie, SVG można importować bezpośrednio w kodzie (nie przez katalog
public), co pozwala na manipulację ich właściwościami:1import SearchIcon from '@/public/images/icons/search.svg';
2
3export default function SearchButton() {
4 return (
5 <button className="search-button">
6 <SearchIcon className="icon" width={24} height={24} />
7 Szukaj
8 </button>
9 );
10}Uwaga: Aby powyższy przykład działał, potrzebujesz dodatkowej konfiguracji, np. za pomocą
.@svgr/webpack
Jak widzieliśmy w poprzednim rozdziale, pliki czcionek można przechowywać w katalogu
public i używać za pomocą next/font/local:1import localFont from 'next/font/local';
2
3export const quantumSans = localFont({
4 src: [
5 {
6 path: '../public/fonts/QuantumSans-Regular.woff2',
7 weight: '400',
8 style: 'normal',
9 },
10 {
11 path: '../public/fonts/QuantumSans-Bold.woff2',
12 weight: '700',
13 style: 'normal',
14 },
15 ],
16});Pliki multimedialne również mogą być przechowywane w katalogu
public:1export default function IntroVideo() {
2 return (
3 <video
4 controls
5 width="100%"
6 poster="/images/video-poster.jpg"
7 >
8 <source src="/videos/intro.mp4" type="video/mp4" />
9 Twoja przeglądarka nie obsługuje odtwarzacza wideo.
10 </video>
11 );
12}JSON przechowywane w
public można ładować za pomocą fetch:1// app/districts/page.tsx
2export default async function DistrictsPage() {
3 // Fetch danych z pliku JSON
4 const response = await fetch('http://localhost:3000/data/districts.json');
5 const districts = await response.json();
6
7 return (
8 <div>
9 <h1>Dzielnice Metropolis Quantum</h1>
10 <ul>
11 {districts.map((district) => (
12 <li key={district.id}>{district.name}</li>
13 ))}
14 </ul>
15 </div>
16 );
17}Wskazówka: W rzeczywistych aplikacjach lepszym rozwiązaniem jest często importowanie danych JSON bezpośrednio w kodzie (np.
) lub przechowywanie ich w bazie danych.import districts from '@/data/districts.json'
Utrzymuj logiczną strukturę katalogów, która odzwierciedla przeznaczenie zasobów:
1public/
2├── branding/ # Elementy związane z marką
3├── icons/ # Ikony systemowe
4├── images/ # Zdjęcia i grafiki
5├── videos/ # Pliki wideo
6├── fonts/ # Czcionki
7└── data/ # Dane statyczne (JSON, CSV)Stosuj spójne konwencje nazewnictwa:
hero-banner.jpg) zamiast spacji (hero banner.jpg)logo-sm.png, logo-lg.png)icon-home.svg, icon-user.svg)Przed umieszczeniem zasobów w katalogu
public, optymalizuj je:Next.js 15 automatycznie dodaje nagłówki cache do zasobów statycznych. Możesz to dodatkowo skonfigurować w pliku
next.config.js:1// next.config.js
2module.exports = {
3 async headers() {
4 return [
5 {
6 source: '/fonts/:path*',
7 headers: [
8 {
9 key: 'Cache-Control',
10 value: 'public, max-age=31536000, immutable',
11 },
12 ],
13 },
14 {
15 source: '/images/:path*',
16 headers: [
17 {
18 key: 'Cache-Control',
19 value: 'public, max-age=86400, stale-while-revalidate=31536000',
20 },
21 ],
22 },
23 ];
24 },
25};Jeśli katalog
public staje się zbyt duży, może to wpłynąć na czas deploymentu i zarządzanie zasobami.Rozwiązanie:
Rozwiązanie:
/images/logo.png, nie images/logo.png)Rozwiązanie:
public1// components/Icon.tsx
2type IconName = 'home' | 'search' | 'user' | 'settings' | 'menu';
3
4interface IconProps {
5 name: IconName;
6 size?: 'sm' | 'md' | 'lg';
7 color?: string;
8 className?: string;
9}
10
11const sizeMap = {
12 sm: { width: 16, height: 16 },
13 md: { width: 24, height: 24 },
14 lg: { width: 32, height: 32 },
15};
16
17export function Icon({
18 name,
19 size = 'md',
20 color = 'currentColor',
21 className = '',
22}: IconProps) {
23 const dimensions = sizeMap[size];
24
25 return (
26 <img
27 src={`/icons/${name}.svg`}
28 alt={`${name} icon`}
29 className={`icon icon-${name} ${className}`}
30 {...dimensions}
31 style={{ filter: `${color !== 'currentColor' ? `color(${color})` : ''}` }}
32 />
33 );
34}Użycie:
1import { Icon } from '@/components/Icon';
2
3export default function Navbar() {
4 return (
5 <nav className="navbar">
6 <Icon name="home" size="sm" />
7 <Icon name="search" color="#3a86ff" />
8 <Icon name="user" className="user-icon" />
9 </nav>
10 );
11}1// components/DistrictImage.tsx
2import Image from 'next/image';
3
4interface DistrictImageProps {
5 districtId: string;
6 alt: string;
7 className?: string;
8}
9
10export function DistrictImage({ districtId, alt, className = '' }: DistrictImageProps) {
11 return (
12 <div className={`district-image-container ${className}`}>
13 <Image
14 src={`/images/backgrounds/districts/${districtId}.jpg`}
15 alt={alt}
16 fill
17 className="district-image"
18 sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
19 />
20 </div>
21 );
22}Użycie:
1import { DistrictImage } from '@/components/DistrictImage';
2
3export default function DistrictCard({ district }) {
4 return (
5 <div className="district-card">
6 <DistrictImage
7 districtId={district.id}
8 alt={`Dzielnica ${district.name}`}
9 />
10 <h3>{district.name}</h3>
11 <p>{district.description}</p>
12 </div>
13 );
14}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 districtId = (await params).id;
10 const district = await fetchDistrictData(districtId);
11
12 return {
13 title: `${district.name} | Metropolis Quantum`,
14 description: district.description,
15 openGraph: {
16 images: [{
17 url: `/images/backgrounds/districts/${districtId}.jpg`,
18 width: 1200,
19 height: 630,
20 alt: district.name,
21 }],
22 type: 'website',
23 },
24 twitter: {
25 card: 'summary_large_image',
26 images: [`/images/backgrounds/districts/${districtId}.jpg`],
27 },
28 };
29}
30
31export default async function DistrictPage({ params }: Props) {
32 const district = await fetchDistrictData((await params).id);
33
34 return (
35 <div className="district-page">
36 <h1>{district.name}</h1>
37 {/* Reszta zawartości strony */}
38 </div>
39 );
40}
41
42// Funkcja pobierająca dane dzielnicy
43async function fetchDistrictData(id: string) {
44 // W rzeczywistej aplikacji: pobieranie danych z API lub bazy
45 const response = await fetch(`http://localhost:3000/data/districts/${id}.json`);
46 return response.json();
47}Dla większych aplikacji warto rozważyć przeniesienie zasobów z katalogu
public do zewnętrznego CDN (Content Delivery Network), co zapewni szybsze ładowanie na całym świecie.1// next.config.js
2module.exports = {
3 assetPrefix: process.env.NODE_ENV === 'production' ? 'https://cdn.twojadomena.com' : '',
4};1// utils/assetPath.ts
2export function getAssetPath(path: string): string {
3 const cdnUrl = process.env.NEXT_PUBLIC_CDN_URL || '';
4 return `${cdnUrl}${path}`;
5}Użycie:
1import { getAssetPath } from '@/utils/assetPath';
2
3export default function Logo() {
4 return (
5 <img
6 src={getAssetPath('/images/logo.png')}
7 alt="Metropolis Quantum Logo"
8 width={200}
9 height={50}
10 />
11 );
12}Możesz dynamicznie importować zasoby z katalogu
public podczas budowy:1// utils/getDistrictData.ts
2import fs from 'fs';
3import path from 'path';
4
5export function getDistrictData() {
6 const filePath = path.join(process.cwd(), 'public', 'data', 'districts.json');
7 const fileContents = fs.readFileSync(filePath, 'utf8');
8 return JSON.parse(fileContents);
9}Możesz generować zasoby statyczne podczas budowy aplikacji:
1// scripts/generate-sitemap.js
2const fs = require('fs');
3const path = require('path');
4
5// Funkcja generująca sitemap.xml
6function generateSitemap(routes) {
7 const sitemap = `<?xml version="1.0" encoding="UTF-8"?>
8 <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
9 ${routes.map(route => `
10 <url>
11 <loc>https://metropolisquantum.pl${route}</loc>
12 <lastmod>${new Date().toISOString()}</lastmod>
13 </url>
14 `).join('')}
15 </urlset>
16 `;
17
18 fs.writeFileSync(
19 path.join(process.cwd(), 'public', 'sitemap.xml'),
20 sitemap
21 );
22}
23
24// Lista tras aplikacji
25const routes = [
26 '/',
27 '/districts',
28 '/about',
29 '/contact',
30 // inne trasy
31];
32
33generateSitemap(routes);Następnie możesz dodać ten skrypt do procesu budowania w
package.json:1{
2 "scripts": {
3 "build": "node scripts/generate-sitemap.js && next build"
4 }
5}Katalog
public jest dostępny publicznie, więc nigdy nie należy przechowywać w nim:Możesz chronić swoje zasoby przed hotlinkingiem (używaniem ich na innych stronach) za pomocą nagłówków HTTP:
1// next.config.js
2module.exports = {
3 async headers() {
4 return [
5 {
6 source: '/images/:path*',
7 headers: [
8 {
9 key: 'Referrer-Policy',
10 value: 'same-origin',
11 },
12 ],
13 },
14 ];
15 },
16};Warto skonfigurować CSP, aby chronić swoją aplikację:
1// next.config.js
2module.exports = {
3 async headers() {
4 return [
5 {
6 source: '/:path*',
7 headers: [
8 {
9 key: 'Content-Security-Policy',
10 value: "default-src 'self'; img-src 'self' https://cdn.twojadomena.com; font-src 'self' https://fonts.gstatic.com",
11 },
12 ],
13 },
14 ];
15 },
16};Katalog
public w Next.js 15 jest potężnym narzędziem do zarządzania zasobami statycznymi w aplikacji. Odpowiednia organizacja, konwencje nazewnictwa i optymalizacja zasobów mogą znacząco poprawić wydajność i utrzymywalność projektu.Kluczowe punkty:
public jest dostępna publiczniePodobnie jak w centralnej bazie zasobów Metropolis Quantum, dobrze zarządzany katalog
public zapewnia sprawny dostęp do wszystkich niezbędnych elementów, utrzymując porządek i wydajność całego systemu.W następnym rozdziale przyjrzymy się ikonom, favicon i metadanym wizualnym, które są kluczowymi elementami tożsamości wizualnej Twojej aplikacji Next.js 15.