Chapter 14: Next.js Fundamentals 🚀
"Next.js transforms how you build React applications, providing structure, performance optimizations, and developer experience enhancements that make creating web applications faster and more enjoyable."
Next.js is a powerful React framework that brings structure and optimization to your web applications. It simplifies many complex aspects of modern web development, from routing to server-side rendering to image optimization. In this chapter, we'll explore the fundamental building blocks of Next.js applications, focusing on the App Router architecture introduced in Next.js 13.
page.js 📄
The page.js file is the core of Next.js routing. Every page in your application is defined by a page.js file within the appdirectory structure.
Basic Page Component
A page in Next.js is simply a React component exported as the default export from a page.js file:
// app/page.js - Home page
export default function Home() {
return (
<main>
<h1>Welcome to My Next.js App</h1>
<p>This is the home page of our application.</p>
</main>
);
}
When a user visits the root URL (/), this component will be rendered.
Page File Placement and Routing
The location of page.js files in your project structure defines the routes of your application:
app/
├── page.js # → / (home page)
├── about/
│ └── page.js # → /about
├── blog/
│ ├── page.js # → /blog (blog index)
│ └── [slug]/ # Dynamic route
│ └── page.js # → /blog/any-slug
└── shop/
└── [category]/
└── [id]/
└── page.js # → /shop/category-name/product-id
Dynamic Routes
For dynamic segments of a URL, use brackets in the folder name to create a dynamic route:
// app/blog/[slug]/page.js
export default function BlogPost({ params }) {
// params.slug contains the dynamic value
return (
<article>
<h1>Blog Post: {params.slug}</h1>
<p>This is a blog post with dynamic routing.</p>
</article>
);
}
The dynamic parameter (slug in this example) is automatically passed to the page component via the params prop.
Multiple Dynamic Segments
You can have multiple dynamic segments in a path:
// app/products/[category]/[id]/page.js
export default function Product({ params }) {
// Access multiple params
const { category, id } = params;
return (
<div>
<h1>Product: {id}</h1>
<p>Category: {category}</p>
</div>
);
}
Catch-all Segments
To capture all segments after a certain route, use a spread syntax with three dots:
// app/docs/[...slug]/page.js
export default function Docs({ params }) {
// If URL is /docs/a/b/c, params.slug will be ['a', 'b', 'c']
const segments = params.slug;
return (
<div>
<h1>Documentation</h1>
<p>Segments: {segments.join('/')}</p>
</div>
);
}
Optional Catch-all Segments
For routes that may or may not have additional segments:
// app/[[...segments]]/page.js
export default function OptionalCatchAll({ params }) {
// For URL /, params.segments will be undefined
// For URL /a/b, params.segments will be ['a', 'b']
const segments = params.segments || [];
return (
<div>
<h1>Optional Segments Page</h1>
<p>Segments: {segments.join('/')}</p>
</div>
);
}
Server Components in page.js
By default, all page.js files in Next.js App Router are React Server Components. This means you can perform data fetching directly in the component:
// app/users/page.js
async function getUsers() {
const res = await fetch('https://api.example.com/users');
if (!res.ok) throw new Error('Failed to fetch users');
return res.json();
}
export default async function UsersPage() {
// Data fetching in server component
const users = await getUsers();
return (
<div>
<h1>Users</h1>
<ul>
{users.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
</div>
);
}
Since this runs on the server, you can:
- Directly access backend resources (databases, APIs)
- Keep API keys and secrets secure
- Reduce client-side JavaScript
- Improve initial page load performance
Client Components in page.js
If you need interactivity or client-side state, convert to a Client Component by adding the "use client" directive at the top of the file:
"use client";
// app/counter/page.js
import { useState } from 'react';
export default function CounterPage() {
const [count, setCount] = useState(0);
return (
<div>
<h1>Counter</h1>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
}
Static and Dynamic Rendering
Next.js automatically determines whether to statically render a page or render it dynamically based on the data fetching in the page:
// Static rendering by default
export default function StaticPage() {
return <h1>This page is statically rendered at build time</h1>;
}
// Dynamic rendering due to dynamic function
export default function DynamicPage() {
// This makes the page dynamically rendered
const currentTime = new Date().toLocaleTimeString();
return <h1>Current time: {currentTime}</h1>;
}
You can control this behavior with the dynamic export:
// Force dynamic rendering
export const dynamic = 'force-dynamic';
// Force static rendering
export const dynamic = 'force-static';
Metadata
Add SEO metadata directly to your pages:
// app/about/page.js
export const metadata = {
title: 'About Us',
description: 'Learn more about our company and mission',
openGraph: {
title: 'About Our Company',
description: 'Learn about our mission and values',
images: [
{
url: 'https://example.com/og-image.jpg',
width: 1200,
height: 630,
alt: 'About Us'
}
]
}
};
export default function AboutPage() {
return (
<div>
<h1>About Us</h1>
<p>This is the about page of our Next.js application.</p>
</div>
);
}
For dynamic metadata, use generateMetadata:
// app/blog/[slug]/page.js
export async function generateMetadata({ params }) {
// Fetch blog post data
const post = await getPostBySlug(params.slug);
return {
title: post.title,
description: post.excerpt
};
}
export default async function BlogPost({ params }) {
const post = await getPostBySlug(params.slug);
return (
<article>
<h1>{post.title}</h1>
<div dangerouslySetInnerHTML={{ __html: post.content }} />
</article>
);
}
Page Data Fetching
Next.js simplifies data fetching in pages with native fetch support:
// app/products/page.js
async function getProducts() {
// This fetch is automatically deduplicated by Next.js
const res = await fetch('https://api.example.com/products', {
// Optional caching/revalidation options
next: {
revalidate: 3600 // Revalidate every hour
}
});
if (!res.ok) throw new Error('Failed to fetch products');
return res.json();
}
export default async function ProductsPage() {
const products = await getProducts();
return (
<div>
<h1>Products</h1>
<div className="products-grid">
{products.map(product => (
<div key={product.id} className="product-card">
<h2>{product.name}</h2>
<p>${product.price}</p>
</div>
))}
</div>
</div>
);
}
Loading UI with Suspense
Next.js includes built-in loading states for data fetching:
// app/dashboard/loading.js
export default function DashboardLoading() {
return <div className="loading-spinner">Loading dashboard...</div>;
}
// app/dashboard/page.js
export default async function Dashboard() {
// This will show the loading.js UI while data is being fetched
const data = await fetchDashboardData();
return (
<div>
<h1>Dashboard</h1>
{/* Dashboard content */}
</div>
);
}
You can also use React Suspense directly:
// app/profile/page.js
import { Suspense } from 'react';
import UserInfo from './UserInfo';
import UserActivity from './UserActivity';
export default function ProfilePage() {
return (
<div>
<h1>User Profile</h1>
<Suspense fallback={<div>Loading user info...</div>}>
<UserInfo />
</Suspense>
<Suspense fallback={<div>Loading activity...</div>}>
<UserActivity />
</Suspense>
</div>
);
}
Error Handling
Create error UI to handle errors gracefully:
// app/products/error.js
"use client";
import { useEffect } from 'react';
export default function Error({ error, reset }) {
useEffect(() => {
// Log the error to an error reporting service
console.error(error);
}, [error]);
return (
<div className="error-container">
<h2>Something went wrong!</h2>
<p>{error.message || 'An error occurred while loading products'}</p>
<button onClick={() => reset()}>Try again</button>
</div>
);
}
Not Found Page
Create a custom 404 page for routes that don't exist:
// app/not-found.js
export default function NotFound() {
return (
<div className="not-found">
<h1>404 - Page Not Found</h1>
<p>The page you are looking for does not exist.</p>
<a href="/">Go back home</a>
</div>
);
}
You can also trigger this page programmatically:
// app/blog/[slug]/page.js
import { notFound } from 'next/navigation';
export default async function BlogPost({ params }) {
const post = await getPostBySlug(params.slug);
if (!post) {
// This will trigger the closest not-found.js
notFound();
}
return (
<article>
<h1>{post.title}</h1>
<div dangerouslySetInnerHTML={{ __html: post.content }} />
</article>
);
}
layout.js 🏗️
The layout.js file defines shared layouts for your application. It wraps page components and persists across route changes, enabling shared UI elements like headers, sidebars, and footers.
Root Layout
The root layout is required and defines the <html> and <body> tags for the entire application:
// app/layout.js
import './globals.css';
export const metadata = {
title: {
template: '%s | My App',
default: 'My App - A Next.js Application'
},
description: 'A powerful Next.js application',
};
export default function RootLayout({ children }) {
return (
<html lang="en">
<body>
<header>
<nav>{/* Navigation content */}</nav>
</header>
<main>{children}</main>
<footer>
<p>© {new Date().getFullYear()} My App</p>
</footer>
</body>
</html>
);
}
The children prop represents the page component or nested layout that should be rendered inside this layout.
Nested Layouts
You can create nested layouts to share UI across specific routes:
// app/dashboard/layout.js
export default function DashboardLayout({ children }) {
return (
<div className="dashboard-layout">
<aside className="sidebar">
<nav>
<ul>
<li><a href="/dashboard">Overview</a></li>
<li><a href="/dashboard/analytics">Analytics</a></li>
<li><a href="/dashboard/settings">Settings</a></li>
</ul>
</nav>
</aside>
<div className="dashboard-content">
{children}
</div>
</div>
);
}
// app/dashboard/page.js
export default function DashboardPage() {
return (
<div>
<h1>Dashboard Overview</h1>
{/* Dashboard content */}
</div>
);
}
With this structure, any page within the /dashboard route will be wrapped by both the root layout and the dashboard layout.
Layout Nesting Hierarchy
Layouts nest automatically based on the file system:
app/
├── layout.js # Root layout (applies to all pages)
├── page.js # Home page
├── about/
│ └── page.js # About page (uses root layout)
└── dashboard/
├── layout.js # Dashboard layout (nested inside root layout)
├── page.js # Dashboard page (uses both layouts)
└── settings/
└── page.js # Settings page (uses both layouts)
When a user visits /dashboard/settings, the rendering hierarchy will be:
- Root layout (
app/layout.js)
- Dashboard layout (
app/dashboard/layout.js)
- Settings page (
app/dashboard/settings/page.js)
Layout Data Fetching
Like pages, layouts can fetch data:
// app/dashboard/layout.js
async function getUserInfo() {
const res = await fetch('https://api.example.com/user', {
next: { revalidate: 60 } // Revalidate every minute
});
if (!res.ok) throw new Error('Failed to fetch user info');
return res.json();
}
export default async function DashboardLayout({ children }) {
const user = await getUserInfo();
return (
<div className="dashboard-layout">
<aside className="sidebar">
<div className="user-info">
<img src={user.avatar} alt={user.name} />
<p>Welcome, {user.name}</p>
</div>
<nav>{/* Navigation content */}</nav>
</aside>
<div className="dashboard-content">
{children}
</div>
</div>
);
}
Template vs. Layout
Next.js also provides a template.js file that's similar to layouts but with a key difference:
layout.js: Persists across routes, maintains state, not re-rendered
template.js: Creates a new instance for each child, state is not preserved
Use template.js when you need a layout that is re-created on every navigation:
// app/blog/template.js
export default function BlogTemplate({ children }) {
// This component is re-mounted on each navigation
return (
<div className="blog-container">
<div className="blog-content">
{children}
</div>
<aside className="blog-sidebar">
<h3>Recent Posts</h3>
{/* Recent posts */}
</aside>
</div>
);
}
Parallel Routes
Next.js supports parallel routes, allowing multiple pages to be shown simultaneously in the same layout:
// app/dashboard/layout.js
export default function DashboardLayout({ children, analytics, notifications }) {
return (
<div className="dashboard">
<div className="main-content">
{children}
</div>
<div className="side-panels">
<div className="analytics-panel">
{analytics}
</div>
<div className="notifications-panel">
{notifications}
</div>
</div>
</div>
);
}
// File structure:
// app/dashboard/page.js -> children
// app/dashboard/@analytics/page.js -> analytics
// app/dashboard/@notifications/page.js -> notifications
Modal Patterns with Layouts
You can implement modals that persist across page navigation:
// app/layout.js
import { cookies } from 'next/headers';
import WelcomeModal from '@/components/WelcomeModal';
export default function RootLayout({ children }) {
// Check if user has seen the welcome modal
const cookieStore = cookies();
const hasSeenWelcome = cookieStore.has('seen_welcome');
return (
<html lang="en">
<body>
{!hasSeenWelcome && <WelcomeModal />}
{children}
</body>
</html>
);
}
Layout Groups
To create layouts that only apply to specific routes without creating a shared URL segment, use route groups:
app/
├── (marketing)/ # Route group (doesn't affect URL)
│ ├── layout.js # Layout for marketing pages
│ ├── page.js # Home page
│ └── about/
│ └── page.js # About page
└── (dashboard)/
├── layout.js # Layout for dashboard pages
├── dashboard/
│ └── page.js # Dashboard page
└── settings/
└── page.js # Settings page
This way, you can have different layouts for marketing pages and dashboard pages without affecting the URL structure.
<Link/> 🔗
The Link component in Next.js is an extension of the HTML <a> tag, providing client-side navigation without full page refreshes.
Basic Usage
import Link from 'next/link';
export default function Navigation() {
return (
<nav>
<ul>
<li>
<Link href="/">Home</Link>
</li>
<li>
<Link href="/about">About</Link>
</li>
<li>
<Link href="/blog">Blog</Link>
</li>
</ul>
</nav>
);
}
Dynamic Routes
Link to dynamic routes by constructing the href with the dynamic parameters:
import Link from 'next/link';
export default function BlogPostsList({ posts }) {
return (
<ul>
{posts.map(post => (
<li key={post.id}>
<Link href={`/blog/${post.slug}`}>
{post.title}
</Link>
</li>
))}
</ul>
);
}
Object Syntax for Complex URLs
For more complex URLs, you can use an object syntax:
import Link from 'next/link';
export default function ProductLink({ product }) {
return (
<Link
href={{
pathname: '/products/[category]/[id]',
query: { category: product.category, id: product.id }
}}
>
{product.name}
</Link>
);
}
Active Link Styling
To style active links, use the usePathname hook:
"use client";
import Link from 'next/link';
import { usePathname } from 'next/navigation';
export default function Navigation() {
const pathname = usePathname();
const navLinks = [
{ href: '/', label: 'Home' },
{ href: '/about', label: 'About' },
{ href: '/blog', label: 'Blog' }
];
return (
<nav>
<ul className="flex space-x-4">
{navLinks.map(link => {
const isActive = pathname === link.href;
return (
<li key={link.href}>
<Link
href={link.href}
className={`
px-3 py-2 rounded-md
${isActive ? 'bg-blue-500 text-white' : 'text-gray-700 hover:bg-gray-100'}
`}
>
{link.label}
</Link>
</li>
);
})}
</ul>
</nav>
);
}
Prefetching
By default, Next.js automatically prefetches links that are in the viewport, improving the performance of subsequent page navigations. You can disable this behavior:
<Link href="/about" prefetch={false}>
About
</Link>
Scroll Restoration
Next.js automatically restores scroll position when navigating back/forward. You can also programmatically scroll to specific elements:
"use client";
import { useEffect } from 'react';
import { useSearchParams } from 'next/navigation';
export default function BlogPage() {
const searchParams = useSearchParams();
const scrollToId = searchParams.get('scrollTo');
useEffect(() => {
if (scrollToId) {
const element = document.getElementById(scrollToId);
if (element) {
element.scrollIntoView({ behavior: 'smooth' });
}
}
}, [scrollToId]);
return (
<div>
<h1>Blog</h1>
{/* Blog content */}
<div id="comments">
<h2>Comments</h2>
{/* Comments section */}
</div>
</div>
);
}
Navigation Events
Use navigation events to show loading indicators or confirm before navigation:
"use client";
import { useState, useEffect } from 'react';
import { useRouter } from 'next/navigation';
export default function Page() {
const router = useRouter();
const [loading, setLoading] = useState(false);
useEffect(() => {
const handleStart = () => setLoading(true);
const handleComplete = () => setLoading(false);
router.events.on('routeChangeStart', handleStart);
router.events.on('routeChangeComplete', handleComplete);
router.events.on('routeChangeError', handleComplete);
return () => {
router.events.off('routeChangeStart', handleStart);
router.events.off('routeChangeComplete', handleComplete);
router.events.off('routeChangeError', handleComplete);
};
}, [router]);
return (
<div>
{loading && <div className="loading-spinner">Loading...</div>}
<h1>My Page</h1>
{/* Page content */}
</div>
);
}
Programmatic Navigation
While <Link> is preferred for most navigation, you can use the useRouter hook for programmatic navigation:
"use client";
import { useRouter } from 'next/navigation';
export default function LoginForm() {
const router = useRouter();
const handleSubmit = async (e) => {
e.preventDefault();
// Form submission logic
const success = await submitLoginForm();
if (success) {
// Redirect after successful login
router.push('/dashboard');
}
};
return (
<form onSubmit={handleSubmit}>
{/* Form fields */}
<button type="submit">Log In</button>
</form>
);
}
Available navigation methods:
router.push(url): Navigate to a new URL
router.replace(url): Replace the current URL (won't add to history)
router.refresh(): Refresh the current route
router.back(): Navigate to the previous route
router.forward(): Navigate to the next route
<Image/> 📷
Next.js provides an Image component that extends HTML's <img> element with automatic optimizations for performance and user experience.
Basic Usage
import Image from 'next/image';
export default function ProfileCard() {
return (
<div className="profile-card">
<Image
src="/images/profile.jpg"
alt="Profile picture"
width={200}
height={200}
priority
/>
<h2>John Doe</h2>
<p>Front-end Developer</p>
</div>
);
}
Key Features
The Image component provides several advantages:
- Automatic optimization: Resizes and converts images to modern formats (WebP, AVIF)
- Lazy loading: Only loads images when they enter the viewport
- Prevents layout shift: Reserves space for images before they load
- Responsive images: Serves different sized images based on device
- Visual stability: Avoids cumulative layout shift (CLS)
Image Sources
Images can be loaded from two sources:
- Local images (stored in your project):
import Image from 'next/image';
import profilePic from '../public/images/profile.jpg';
export default function Avatar() {
return (
<Image
src={profilePic} // Imported image
alt="Profile picture"
placeholder="blur" // Optional blur-up while loading
/>
);
}
- Remote images (from external URLs):
import Image from 'next/image';
export default function ProductImage({ product }) {
return (
<Image
src={product.imageUrl} // Remote URL
alt={product.name}
width={500}
height={350}
unoptimized={false} // Set to true to disable optimization
/>
);
}
For remote images, you must either:
- Specify
widthandheightprops
- Set
fill={true}and use a parent element withposition: relative
- Add the domain to the allowed list in
next.config.js:
// next.config.js
module.exports = {
images: {
remotePatterns: [
{
protocol: 'https',
hostname: 'example.com',
port: '',
pathname: '/images/**',
},
],
},
}
Image Sizing
There are three approaches to sizing images:
- Static sizing (exact dimensions):
<Image
src="/product.jpg"
alt="Product"
width={400}
height={300}
/>
- Responsive sizing (adapts to parent):
<div className="relative w-full h-40">
<Image
src="/banner.jpg"
alt="Banner"
fill
style={{ objectFit: 'cover' }}
/>
</div>
- Filling a container (with specific sizing):
<div className="grid grid-cols-2 gap-4">
<div className="relative h-60">
<Image
src="/image1.jpg"
alt="Image 1"
fill
sizes="(max-width: 768px) 100vw, 50vw"
style={{ objectFit: 'cover' }}
/>
</div>
<div className="relative h-60">
<Image
src="/image2.jpg"
alt="Image 2"
fill
sizes="(max-width: 768px) 100vw, 50vw"
style={{ objectFit: 'cover' }}
/>
</div>
</div>
Responsive Images with sizes
The sizes prop helps Next.js generate appropriate image sizes for different viewports:
<Image
src="/hero.jpg"
alt="Hero image"
fill
sizes="(max-width: 640px) 100vw,
(max-width: 1024px) 50vw,
33vw"
style={{ objectFit: 'cover' }}
/>
This tells the browser:
- On small screens (under 640px): Image takes 100% of viewport width
- On medium screens (640px-1024px): Image takes 50% of viewport width
- On large screens (above 1024px): Image takes 33% of viewport width
Image Loading Priority
For important images (like hero images) that are visible immediately, use the priority prop:
<Image
src="/hero-banner.jpg"
alt="Hero banner"
width={1200}
height={400}
priority
/>
This disables lazy loading and preloads the image.
Image Placeholders
While images load, you can show placeholders:
// Blur placeholder from local image metadata
import profileImage from '../public/profile.jpg';
<Image
src={profileImage}
alt="Profile"
placeholder="blur" // Uses the image's own blur hash
/>
// Custom color placeholder
<Image
src="/remote-image.jpg"
alt="Remote image"
width={300}
height={200}
placeholder="blur"
blurDataURL="data:image/svg+xml;base64,..." // Base64 encoded SVG or blur hash
/>
Image Styling
Style the Image component using standard CSS or CSS-in-JS:
// With className
<Image
src="/product.jpg"
alt="Product"
width={400}
height={300}
className="rounded-lg shadow-md hover:shadow-xl transition-shadow"
/>
// With inline style
<Image
src="/profile.jpg"
alt="Profile"
width={200}
height={200}
style={{
borderRadius: '50%',
border: '2px solid #0070f3'
}}
/>
Background Images
For background images, you can use fill with positioning:
<div className="relative h-screen">
<Image
src="/background.jpg"
alt=""
fill
style={{
objectFit: 'cover',
objectPosition: 'center',
zIndex: -1
}}
quality={100}
priority
/>
<div className="relative z-10 p-10 text-white">
<h1 className="text-4xl font-bold">Welcome to Our Site</h1>
<p>Content that overlays the background image</p>
</div>
</div>
Image Component Props
Here are the key props for the Image component:
<Image
// Required props
src="/image.jpg" // Image source (URL or imported object)
alt="Description" // Alt text for accessibility
// Size props (one approach required)
width={500} // Width in pixels
height={300} // Height in pixels
fill // Alternative to width/height; fills parent container
// Optional but recommended
sizes="(max-width: 768px) 100vw, 50vw" // Responsive size hints
quality={80} // Image quality (1-100), default is 75
priority={false} // Set true for LCP images above the fold
placeholder="empty" // "empty", "blur", or "data:image/..."
// Advanced options
loading="lazy" // "lazy" or "eager"
onLoad={() => {}} // Callback when image is loaded
onError={() => {}} // Callback when image fails to load
style={{}} // CSS styles
className="" // CSS class
unoptimized={false} // Skip optimization if true
/>
Image Component Best Practices
Here are some best practices for using the Next.js Image component:
- Always provide meaningful alt text for accessibility
- Set
priorityon LCP (Largest Contentful Paint) images to improve performance metrics
- Use appropriate image sizes to avoid unnecessary bandwidth usage
- Provide
sizesattribute for responsive images to help the browser select the right source
- Use modern image formats like WebP and AVIF (Next.js handles this automatically)
- Avoid layout shift by always providing dimensions or using
fillwith a positioned parent
- Optimize for mobile first by ensuring images look good on smaller screens
- Limit the number of high priority images to only what's visible above the fold
Advanced Image Usage Scenarios
Art Direction (Different Images for Different Screen Sizes)
While the Next.js Image component doesn't directly support art direction (showing different images at different breakpoints), you can implement it with CSS:
// Multiple images approach
import { useEffect, useState } from 'react';
import Image from 'next/image';
import mobileBanner from '../public/mobile-banner.jpg';
import desktopBanner from '../public/desktop-banner.jpg';
export function ResponsiveBanner() {
const [isMobile, setIsMobile] = useState(false);
useEffect(() => {
const checkIfMobile = () => {
setIsMobile(window.innerWidth < 768);
};
checkIfMobile();
window.addEventListener('resize', checkIfMobile);
return () => window.removeEventListener('resize', checkIfMobile);
}, []);
return (
<div className="banner relative h-[300px] md:h-[500px]">
<Image
src={isMobile ? mobileBanner : desktopBanner}
alt="Banner"
fill
priority
style={{ objectFit: 'cover' }}
sizes="100vw"
/>
</div>
);
}
Dynamic Images from CMS
When working with images from a CMS or other dynamic source:
// app/products/[id]/page.js
import Image from 'next/image';
async function getProduct(id) {
const res = await fetch(`https://api.example.com/products/${id}`);
if (!res.ok) throw new Error('Failed to fetch product');
return res.json();
}
export default async function ProductPage({ params }) {
const product = await getProduct(params.id);
// Extract image dimensions if available from API response
const { imageUrl, imageWidth, imageHeight } = product;
return (
<div className="product-details">
<div className="relative h-80 w-full md:h-96">
<Image
src={imageUrl}
alt={product.name}
fill
sizes="(max-width: 768px) 100vw, 50vw"
style={{ objectFit: 'contain' }}
/>
</div>
<h1>{product.name}</h1>
<p>{product.description}</p>
<p className="price">${product.price}</p>
</div>
);
}
Image Gallery with Thumbnails
Creating a responsive image gallery:
"use client";
import { useState } from 'react';
import Image from 'next/image';
export default function ImageGallery({ images }) {
const [activeIndex, setActiveIndex] = useState(0);
const activeImage = images[activeIndex];
return (
<div className="gallery">
<div className="main-image relative h-80 mb-4">
<Image
src={activeImage.url}
alt={activeImage.alt || 'Gallery image'}
fill
sizes="(max-width: 768px) 100vw, 600px"
style={{ objectFit: 'contain' }}
priority={activeIndex === 0}
/>
</div>
<div className="thumbnails grid grid-cols-5 gap-2">
{images.map((image, index) => (
<button
key={index}
className={`relative h-16 w-16 cursor-pointer ${
index === activeIndex ? 'ring-2 ring-blue-500' : ''
}`}
onClick={() => setActiveIndex(index)}
>
<Image
src={image.url}
alt={image.alt || `Thumbnail ${index + 1}`}
fill
sizes="64px"
style={{ objectFit: 'cover' }}
/>
</button>
))}
</div>
</div>
);
}
Using External Image Services
If you're using a cloud-based image service like Cloudinary or Imgix:
// next.config.js
module.exports = {
images: {
domains: ['res.cloudinary.com'],
// Or use loader configuration for specialized services
loader: 'cloudinary',
loaderFile: './my-loader.js',
},
};
// my-loader.js (custom loader)
export default function cloudinaryLoader({ src, width, quality }) {
const params = ['f_auto', 'c_limit', `w_${width}`, `q_${quality || 75}`];
return `https://res.cloudinary.com/my-account/image/upload/${params.join(
','
)}/${src}`;
}
Then use it in your components:
<Image
src="my-folder/image.jpg" // Just the image ID/path part
alt="My Image"
width={400}
height={300}
/>
Putting It All Together 🧩
Now let's see how all these components work together to create a complete Next.js application:
Project Structure
A typical Next.js project structure might look like this:
my-nextjs-app/
├── app/
│ ├── favicon.ico
│ ├── globals.css
│ ├── layout.js # Root layout
│ ├── page.js # Home page
│ ├── about/
│ │ └── page.js # About page
│ ├── blog/
│ │ ├── layout.js # Blog layout
│ │ ├── page.js # Blog index
│ │ └── [slug]/
│ │ ├── page.js # Blog post page
│ │ └── error.js # Error handling for blog posts
│ └── products/
│ ├── page.js # Products index
│ └── [id]/
│ └── page.js # Product detail page
├── components/
│ ├── ui/
│ │ ├── Button.jsx
│ │ └── Card.jsx
│ ├── layout/
│ │ ├── Header.jsx
│ │ └── Footer.jsx
│ └── features/
│ ├── ProductCard.jsx
│ └── BlogPostCard.jsx
├── lib/
│ ├── api.js # API utilities
│ └── utils.js # Helper functions
├── public/
│ ├── images/
│ │ └── hero.jpg
│ └── favicon.ico
└── next.config.js
Sample Implementation
Here's how these components might work together:
- Root Layout (app/layout.js):
import { Inter } from 'next/font/google';
import Header from '@/components/layout/Header';
import Footer from '@/components/layout/Footer';
import './globals.css';
const inter = Inter({ subsets: ['latin'] });
export const metadata = {
title: {
template: '%s | My Next.js App',
default: 'My Next.js App',
},
description: 'A modern Next.js application',
};
export default function RootLayout({ children }) {
return (
<html lang="en">
<body className={inter.className}>
<Header />
<main className="min-h-screen px-4 py-8 max-w-5xl mx-auto">
{children}
</main>
<Footer />
</body>
</html>
);
}
- Header Component (components/layout/Header.jsx):
"use client";
import Link from 'next/link';
import Image from 'next/image';
import { usePathname } from 'next/navigation';
import logo from '@/public/images/logo.png';
export default function Header() {
const pathname = usePathname();
const navLinks = [
{ href: '/', label: 'Home' },
{ href: '/about', label: 'About' },
{ href: '/blog', label: 'Blog' },
{ href: '/products', label: 'Products' },
];
return (
<header className="bg-white shadow-sm">
<div className="max-w-5xl mx-auto px-4 py-4 flex justify-between items-center">
<Link href="/" className="flex items-center space-x-2">
<Image
src={logo}
alt="Company Logo"
width={40}
height={40}
priority
/>
<span className="font-bold text-xl">MyApp</span>
</Link>
<nav>
<ul className="flex space-x-6">
{navLinks.map(link => {
const isActive = pathname === link.href ||
(link.href !== '/' && pathname.startsWith(link.href));
return (
<li key={link.href}>
<Link
href={link.href}
className={`transition-colors py-2 border-b-2 ${
isActive
? 'border-blue-500 text-blue-600'
: 'border-transparent hover:text-blue-600'
}`}
>
{link.label}
</Link>
</li>
);
})}
</ul>
</nav>
</div>
</header>
);
}
- Home Page (app/page.js):
import Image from 'next/image';
import Link from 'next/link';
import heroImage from '@/public/images/hero.jpg';
export default function Home() {
return (
<div>
<div className="relative h-96 mb-12">
<Image
src={heroImage}
alt="Hero image"
fill
priority
sizes="(max-width: 768px) 100vw, 1024px"
style={{ objectFit: 'cover' }}
className="rounded-lg"
/>
<div className="absolute inset-0 flex items-center justify-center bg-black bg-opacity-50 rounded-lg">
<div className="text-white text-center px-4">
<h1 className="text-4xl md:text-5xl font-bold mb-4">
Welcome to My Next.js App
</h1>
<p className="text-lg md:text-xl mb-6">
A modern web application built with Next.js
</p>
<Link
href="/products"
className="bg-blue-600 hover:bg-blue-700 text-white py-2 px-6 rounded-lg text-lg transition-colors"
>
Explore Products
</Link>
</div>
</div>
</div>
<div className="grid md:grid-cols-3 gap-8 mb-12">
<div className="bg-white p-6 rounded-lg shadow-md">
<h2 className="text-xl font-bold mb-2">Feature 1</h2>
<p>Description of feature 1 and its benefits.</p>
</div>
<div className="bg-white p-6 rounded-lg shadow-md">
<h2 className="text-xl font-bold mb-2">Feature 2</h2>
<p>Description of feature 2 and its benefits.</p>
</div>
<div className="bg-white p-6 rounded-lg shadow-md">
<h2 className="text-xl font-bold mb-2">Feature 3</h2>
<p>Description of feature 3 and its benefits.</p>
</div>
</div>
<div className="text-center">
<h2 className="text-2xl font-bold mb-4">Ready to get started?</h2>
<Link
href="/about"
className="bg-blue-600 hover:bg-blue-700 text-white py-2 px-6 rounded-lg text-lg transition-colors inline-block"
>
Learn More
</Link>
</div>
</div>
);
}
- Blog Layout (app/blog/layout.js):
export default function BlogLayout({ children }) {
return (
<div className="grid md:grid-cols-12 gap-8">
<div className="md:col-span-9">{children}</div>
<aside className="md:col-span-3">
<div className="bg-white p-6 rounded-lg shadow-md sticky top-8">
<h3 className="text-lg font-bold mb-4">Recent Posts</h3>
<ul className="space-y-2">
<li><a href="/blog/post-1" className="text-blue-600 hover:underline">Post 1</a></li>
<li><a href="/blog/post-2" className="text-blue-600 hover:underline">Post 2</a></li>
<li><a href="/blog/post-3" className="text-blue-600 hover:underline">Post 3</a></li>
</ul>
<h3 className="text-lg font-bold mt-6 mb-4">Categories</h3>
<ul className="space-y-2">
<li><a href="/blog?category=technology" className="text-blue-600 hover:underline">Technology</a></li>
<li><a href="/blog?category=design" className="text-blue-600 hover:underline">Design</a></li>
<li><a href="/blog?category=business" className="text-blue-600 hover:underline">Business</a></li>
</ul>
</div>
</aside>
</div>
);
}
- Blog Post Page (app/blog/[slug]/page.js):
import Image from 'next/image';
import { notFound } from 'next/navigation';
async function getBlogPost(slug) {
// In a real app, fetch from an API
const posts = {
'post-1': {
title: 'Getting Started with Next.js',
date: '2023-01-15',
author: 'Jane Doe',
image: '/images/blog/post-1.jpg',
content: 'This is the content of the first blog post...'
},
'post-2': {
title: 'Next.js 13 Features',
date: '2023-02-22',
author: 'John Smith',
image: '/images/blog/post-2.jpg',
content: 'This is the content of the second blog post...'
}
};
// Simulate API delay
await new Promise(resolve => setTimeout(resolve, 1000));
return posts[slug] || null;
}
export async function generateMetadata({ params }) {
const post = await getBlogPost(params.slug);
if (!post) {
return {
title: 'Post Not Found',
description: 'The blog post you are looking for does not exist.'
};
}
return {
title: post.title,
description: `${post.title} - Written by ${post.author}`
};
}
export default async function BlogPost({ params }) {
const post = await getBlogPost(params.slug);
if (!post) {
notFound();
}
return (
<article className="bg-white rounded-lg shadow-md overflow-hidden">
<div className="relative h-72">
<Image
src={post.image}
alt={post.title}
fill
sizes="(max-width: 768px) 100vw, 800px"
style={{ objectFit: 'cover' }}
priority
/>
</div>
<div className="p-6">
<h1 className="text-3xl font-bold mb-2">{post.title}</h1>
<div className="text-gray-600 mb-6">
<span>By {post.author}</span>
<span className="mx-2">•</span>
<span>{post.date}</span>
</div>
<div className="prose max-w-none">
<p>{post.content}</p>
</div>
</div>
</article>
);
}
- 404 Not Found Page (app/not-found.js):
import Link from 'next/link';
export default function NotFound() {
return (
<div className="min-h-[70vh] flex flex-col items-center justify-center text-center">
<h1 className="text-6xl font-bold text-gray-900">404</h1>
<h2 className="text-3xl font-semibold text-gray-700 mt-4">Page Not Found</h2>
<p className="text-gray-600 mt-2 mb-6">
The page you are looking for does not exist or has been moved.
</p>
<Link
href="/"
className="bg-blue-600 hover:bg-blue-700 text-white py-2 px-6 rounded-lg transition-colors"
>
Go Back Home
</Link>
</div>
);
}
Best Practices for Next.js Applications 🥇
File Organization
- Group by Feature: Organize files by feature rather than file type
- Keep Components Small: Break down large components into smaller, reusable ones
- Use Consistent Naming: Follow a naming convention throughout your project
- Leverage the App Directory: Use the new
appdirectory structure for all new projects
Performance Optimization
- Use Server Components: Leverage Server Components for data fetching and static content
- Image Optimization: Always use the Next.js Image component for images
- Route Prefetching: Use Link components for internal navigation
- Lazy Loading: Use dynamic imports for components not needed immediately
- Optimize LCP: Set the
priorityprop on the largest image in the viewport
- Code Splitting: Let Next.js handle code splitting automatically
SEO
- Use Metadata API: Define metadata in your pages and layouts
- Ensure Semantic HTML: Use proper HTML elements for content structure
- Optimize Images: Include descriptive alt text for all images
- Create a Sitemap: Use Next.js built-in sitemap generation
- Add Structured Data: Include JSON-LD for rich search results
Development Workflow
- Use TypeScript: Add type safety to your application
- Test Components: Implement unit and integration tests
- Consistent Styling: Use a CSS framework like Tailwind CSS
- Error Handling: Implement error boundaries and fallbacks
- Environment Variables: Use
.envfiles for configuration
Practice Exercises 🏋️
- Basic Layout Implementation:
- Create a root layout with header and footer components
- Implement a nested layout for a blog section
- Add navigation links using the Link component
- Style active links based on the current route
- Dynamic Page Challenge:
- Create a products page with dynamic routes
- Implement loading and error states
- Add metadata for SEO
- Optimize images using the Image component
- Image Gallery:
- Build an image gallery component
- Implement responsive images with proper sizing
- Add lazy loading for images below the fold
- Optimize the largest contentful paint image
- Data Fetching:
- Create a page that fetches data from an API
- Implement server-side rendering
- Add client-side interactivity
- Handle loading and error states
Pro Tip: Focus on mastering the App Router architecture in Next.js. While the Pages Router is still supported, the App Router is the future of Next.js and offers more powerful features like Server Components and nested layouts.
What you've learned:
- How to structure pages and layouts in Next.js using the App Router
- Creating dynamic routes with parameters
- Client-side navigation with the Link component
- Image optimization with the Image component
- Best practices for building performant Next.js applications
Coming up next: In Chapter 15, we'll explore Tailwind CSS Utilities, learning how to efficiently style your Next.js applications using utility classes.

