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.The
public directory is a special folder in a Next.js application that serves the following functions:public are served from the root domain directoryThis directory is ideal for storing resources such as:
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.jsonAccessing assets from the
public directory is simple and intuitive. Remember that:/)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}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: '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}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 the image URL must be known at build time:
When the image is very large and rarely used:
When you want a direct link to the file:
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
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});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}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.,
) or store it in a database.import districts from '@/data/districts.json'
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)Use consistent naming conventions:
hero-banner.jpg) instead of spaces (hero banner.jpg)logo-sm.png, logo-lg.png)icon-home.svg, icon-user.svg)Before placing assets in the
public directory, optimize them: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};If the
public directory becomes too large, it can affect deployment time and resource management.Solution:
Solution:
/images/logo.png, not images/logo.png)Solution:
public directory structure1// 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}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}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}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.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}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}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}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}The
public directory is publicly accessible, so you should never store in it: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};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};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:
public directory content is publicly accessibleJust 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.