Utilizziamo i cookie per migliorare la tua esperienza sul sito
CodeWorlds

Zarządzanie zasobami statycznymi w katalogu public w Next.js 15

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.

Czym jest katalog public w Next.js 15?

Katalog

public
to specjalny folder w aplikacji Next.js, który spełnia następujące funkcje:

  1. Udostępnia pliki statyczne bezpośrednio - pliki umieszczone w
    public
    są serwowane z głównego katalogu domeny
  2. Nie przetwarza zawartości - pliki nie przechodzą przez system budowania (w przeciwieństwie do zasobów importowanych bezpośrednio w komponentach)
  3. Zapewnia niezmienne URL-e - nazwy plików nie ulegają zmianie przez proces budowania (nie są dodawane hashe do nazw)

Katalog ten jest idealny do przechowywania takich zasobów jak:

  • Obrazy logo i ikony
  • Pliki favicon i manifest.json
  • Pliki robots.txt i sitemap.xml
  • Czcionki
  • Dokumenty (PDF, DOCX)
  • Zasoby multimedialne (filmy, dźwięki)
  • Dane statyczne (JSON, CSV)

Struktura katalogu public

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

Dostęp do zasobów z katalogu public

Dostęp do zasobów z katalogu

public
jest prosty i intuicyjny. Pamiętaj, że:

  1. Ścieżki są względem głównego katalogu domeny
  2. Musisz poprzedzać nazwy plików ukośnikiem (
    /
    )

W komponentach React

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}

W CSS

1/* W pliku CSS */
2.hero-section {
3  background-image: url('/images/backgrounds/quantum-pattern.svg');
4  background-repeat: repeat;
5}

W metadanych

1// app/layout.tsx
2import { Metadata } from 'next';
3
4export const metadata: Metadata = {
5  title: 'Metropolis Quantum',
6  description: 'Oficjalny portal miasta przyszłości',
7  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}

Komponent Image a katalog public

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}

Kiedy używać obrazów z public zamiast importów?

  1. Gdy URL obrazu musi być znany podczas budowy:

    • Obrazy w metadanych (Open Graph, Twitter)
    • Tła CSS
  2. Gdy obraz jest bardzo duży i rzadko używany:

    • Duże grafiki, które pojawiają się tylko w specyficznych kontekstach
  3. Gdy chcesz mieć bezpośredni link do pliku:

    • Zasoby do pobrania
    • Zasoby współdzielone z innymi aplikacjami

Obsługa różnych typów plików

SVG

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
.

Czcionki

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});

Filmy i audio

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}

Pliki JSON

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.

import districts from '@/data/districts.json'
) lub przechowywanie ich w bazie danych.

Organizacja i najlepsze praktyki

1. Struktura katalogów

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)

2. Konwencje nazewnictwa

Stosuj spójne konwencje nazewnictwa:

  • Używaj kebab-case (
    hero-banner.jpg
    ) zamiast spacji (
    hero banner.jpg
    )
  • Dodawaj sufiksy z rozmiarami dla różnych wariantów (
    logo-sm.png
    ,
    logo-lg.png
    )
  • Używaj prefiksów dla grupowania (
    icon-home.svg
    ,
    icon-user.svg
    )

3. Optymalizacja zasobów

Przed umieszczeniem zasobów w katalogu

public
, optymalizuj je:

  • Obrazy: Kompresuj za pomocą narzędzi jak TinyPNG, ImageOptim
  • SVG: Minimalizuj za pomocą SVGO
  • Wideo: Kompresuj do właściwych formatów i rozmiarów
  • Czcionki: Używaj WOFF2 jako preferowanego formatu

4. Cachowanie

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};

Problemy i rozwiązania

Problem 1: Zbyt duży rozmiar katalogu public

Jeśli katalog

public
staje się zbyt duży, może to wpłynąć na czas deploymentu i zarządzanie zasobami.

Rozwiązanie:

  • Przenieś rzadko używane, duże zasoby do usług CDN (Content Delivery Network)
  • Stosuj strategie lazy loading dla zasobów
  • Optymalizuj wszystkie zasoby przed dodaniem ich do katalogu

Problem 2: Problemy z URL obrazów

Rozwiązanie:

  • Zawsze używaj ukośników na początku ścieżek (
    /images/logo.png
    , nie
    images/logo.png
    )
  • Unikaj spacji i znaków specjalnych w nazwach plików
  • Sprawdzaj wielkość liter w nazwach plików (systemy Linux są case-sensitive)

Problem 3: Niespójność w zarządzaniu zasobami

Rozwiązanie:

  • Stwórz i stosuj konwencje nazewnictwa dla całego zespołu
  • Dokumentuj strukturę katalogu
    public
  • Rozważ utworzenie komponentów abstrakcyjnych dla często używanych zasobów

Praktyczne przykłady

Przykład 1: System ikon z katalogu public

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

Przykład 2: System dynamicznego ładowania zdjęć z katalogu public

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}

Przykład 3: Konfiguracja SEO z zasobami z public

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}

Integracja z CDN

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.

Konfiguracja ścieżki bazowej w Next.js

1// next.config.js
2module.exports = {
3  assetPrefix: process.env.NODE_ENV === 'production' ? 'https://cdn.twojadomena.com' : '',
4};

Komponent do zarządzania ścieżkami zasobów

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}

Zaawansowane techniki

1. Dynamiczne importy zasobów

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}

2. Generowanie zasobów podczas budowy

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}

Bezpieczeństwo i uwagi dotyczące zasobów publicznych

1. Unikaj przechowywania poufnych informacji

Katalog

public
jest dostępny publicznie, więc nigdy nie należy przechowywać w nim:

  • Kluczy API
  • Poufnych danych
  • Plików konfiguracyjnych z wrażliwymi informacjami

2. Ochrona przed hotlinkiem

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};

3. Content Security Policy (CSP)

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};

Podsumowanie

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:

  1. Struktura i organizacja - utrzymuj logiczną strukturę katalogów i spójne konwencje nazewnictwa
  2. Optymalizacja - kompresuj i optymalizuj zasoby przed umieszczeniem ich w katalogu
  3. Abstrakcja - twórz komponenty abstrakcyjne do zarządzania dostępem do zasobów
  4. Bezpieczeństwo - pamiętaj, że zawartość katalogu
    public
    jest dostępna publicznie
  5. Skalowanie - dla większych aplikacji rozważ użycie CDN

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

Vai a CodeWorlds