We use cookies to enhance your experience on the site
CodeWorlds

Managing Static Assets in the Public Directory in Next.js 15

In Metropolis Quantum, each district has its own central resource base - a place where elements available to all residents are stored: maps, information brochures, visual identity elements, and interactive holoscreens with city plans. These resources are immutable and publicly available, ensuring consistency and continuity of experience throughout the city.

In Next.js applications, the

public
directory plays the role of this central base. This is where we place all static assets that should be available without processing by the Next.js build system. In this chapter, we will look at how to efficiently manage these assets so that our application is both performant and well-organized.

What is the Public Directory in Next.js 15?

The

public
directory is a special folder in a Next.js application that serves the following functions:

  1. Serves static files directly - files placed in
    public
    are served from the root domain directory
  2. Does not process content - files do not go through the build system (unlike resources imported directly in components)
  3. Provides immutable URLs - file names do not change through the build process (no hashes are added to names)

This directory is ideal for storing resources such as:

  • Obrazy logo i ikony
  • Pliki favicon i manifest.json
  • Pliki robots.txt i sitemap.xml
  • Czcionki
  • Dokumenty (PDF, DOCX)
  • Multimedia resources (videos, sounds)
  • Dane statyczne (JSON, CSV)

Struktura katalogu public

A well-organized

public
directory makes resource management easier. Here is an example structure:

1public/
2β”œβ”€β”€ favicon.ico                  # Ikona strony
3β”œβ”€β”€ favicon-16x16.png            # Alternatywne rozmiary favicon
4β”œβ”€β”€ favicon-32x32.png
5β”œβ”€β”€ apple-touch-icon.png         # Icon for Apple devices
6β”œβ”€β”€ manifest.json                # Manifest PWA
7β”œβ”€β”€ robots.txt                   # Instructions for bots
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/             # Subdirectory with backgrounds
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

Accessing Assets from the Public Directory

Accessing assets from the

public
directory is simple and intuitive. Remember that:

  1. Paths are relative to the root domain directory
  2. You must prefix file names with a forward slash (
    /
    )

W komponentach React

1// Example of using an image from the public directory
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: 'Official portal of the city of the future',
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}

The Image Component and the Public Directory

Although Next.js provides an optimized

Image
component from the
next/image
package, sometimes we need to use images directly from the
public
directory. The
Image
component can be used with images from the
public
directory while retaining optimization benefits:

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}

When to Use Images from Public Instead of Imports?

  1. When the image URL must be known at build time:

    • Obrazy w metadanych (Open Graph, Twitter)
    • CSS backgrounds
  2. When the image is very large and rarely used:

    • Large graphics that appear only in specific contexts
  3. When you want a direct link to the file:

    • Zasoby do pobrania
    • Resources shared with other applications

Handling Different File Types

SVG

SVG files can be placed in the

public
directory and used as regular images:

1export default function Icon() {
2  return (
3    <img src="/images/icons/search.svg" alt="Szukaj" width={24} height={24} />
4  );
5}

Alternatively, SVGs can be imported directly in code (not through the

public
directory), which allows manipulation of their properties:

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}

Note: For the above example to work, you need additional configuration, e.g., using

@svgr/webpack
.

Czcionki

As we saw in the previous chapter, font files can be stored in the

public
directory and used with
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

Multimedia files can also be stored in the

public
directory:

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      Your browser does not support the video player.
10    </video>
11  );
12}

Pliki JSON

JSON stored in

public
can be loaded using 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}

Tip: In real applications, a better solution is often to import JSON data directly in code (e.g.,

import districts from '@/data/districts.json'
) or store it in a database.

Organizacja i najlepsze praktyki

1. Directory Structure

Maintain a logical directory structure that reflects the purpose of assets:

1public/
2β”œβ”€β”€ branding/       # Brand-related elements
3β”œβ”€β”€ icons/          # Ikony systemowe
4β”œβ”€β”€ images/         # Photos and graphics
5β”œβ”€β”€ videos/         # Pliki wideo
6β”œβ”€β”€ fonts/          # Czcionki
7└── data/           # Dane statyczne (JSON, CSV)

2. Konwencje nazewnictwa

Use consistent naming conventions:

  • Use kebab-case (
    hero-banner.jpg
    ) instead of spaces (
    hero banner.jpg
    )
  • Add size suffixes for different variants (
    logo-sm.png
    ,
    logo-lg.png
    )
  • Use prefixes for grouping (
    icon-home.svg
    ,
    icon-user.svg
    )

3. Asset Optimization

Before placing assets in the

public
directory, optimize them:

  • Images: Compress using tools like TinyPNG, ImageOptim
  • SVG: Minimize using SVGO
  • Video: Compress to proper formats and sizes
  • Fonts: Use WOFF2 as the preferred format

4. Cachowanie

Next.js 15 automatically adds cache headers to static assets. You can additionally configure this in the

next.config.js
file:

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

Problems and Solutions

Problem 1: Public Directory Too Large

If the

public
directory becomes too large, it can affect deployment time and resource management.

Solution:

  • Move rarely used, large assets to CDN (Content Delivery Network) services
  • Apply lazy loading strategies for assets
  • Optymalizuj wszystkie zasoby przed dodaniem ich do katalogu

Problem 2: Image URL Issues

Solution:

  • Always use forward slashes at the beginning of paths (
    /images/logo.png
    , not
    images/logo.png
    )
  • Avoid spaces and special characters in file names
  • Check letter case in file names (Linux systems are case-sensitive)

Problem 3: Inconsistency in Asset Management

Solution:

  • Create and follow naming conventions for the entire team
  • Document the
    public
    directory structure
  • Consider creating abstract components for frequently used assets

Practical Examples

Example 1: Icon System from the Public Directory

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}

Usage:

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}

Example 2: Dynamic Photo Loading System from the Public Directory

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}

Usage:

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}

Example 3: SEO Configuration with Public Assets

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      {/* Rest of page content */}
38    </div>
39  );
40}
41
42// Function fetching district data
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

For larger applications, it is worth considering moving assets from the

public
directory to an external CDN (Content Delivery Network), which will ensure faster loading worldwide.

Base Path Configuration in Next.js

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

Component for Managing Asset Paths

1// utils/assetPath.ts
2export function getAssetPath(path: string): string {
3  const cdnUrl = process.env.NEXT_PUBLIC_CDN_URL || '';
4  return `${cdnUrl}${path}`;
5}

Usage:

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. Dynamic Asset Imports

You can dynamically import assets from the

public
directory during build:

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. Generating Assets During Build

You can generate static assets during application build:

1// scripts/generate-sitemap.js
2const fs = require('fs');
3const path = require('path');
4
5// Function generating 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);

Then you can add this script to the build process in

package.json
:

1{
2  "scripts": {
3    "build": "node scripts/generate-sitemap.js && next build"
4  }
5}

Security and Notes About Public Assets

1. Unikaj przechowywania poufnych informacji

The

public
directory is publicly accessible, so you should never store in it:

  • Kluczy API
  • Poufnych danych
  • Configuration files with sensitive information

2. Ochrona przed hotlinkiem

You can protect your assets from hotlinking (being used on other sites) using HTTP headers:

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)

It is worth configuring CSP to protect your application:

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

Summary

The

public
directory in Next.js 15 is a powerful tool for managing static assets in the application. Proper organization, naming conventions, and asset optimization can significantly improve performance and project maintainability.

Key points:

  1. Structure and organization - maintain a logical directory structure and consistent naming conventions
  2. Optimization - compress and optimize assets before placing them in the directory
  3. Abstraction - create abstract components for managing access to assets
  4. Security - remember that the
    public
    directory content is publicly accessible
  5. Scaling - for larger applications consider using a CDN

Just like in the central resource base of Metropolis Quantum, a well-managed

public
directory ensures efficient access to all necessary elements, maintaining order and performance of the entire system.

In the next chapter, we will look at icons, favicon, and visual metadata, which are key elements of your Next.js 15 application's visual identity.

Go to CodeWorlds→