Chapter 14: Next.js Fundamentals 🚀

Next.js framework overview showing project structure, file-based routing, API routes, data fetching methods, and key features like code splitting and image optimization.

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:

  1. Root layout (app/layout.js)
  1. Dashboard layout (app/dashboard/layout.js)
  1. 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:

  1. Automatic optimization: Resizes and converts images to modern formats (WebP, AVIF)
  1. Lazy loading: Only loads images when they enter the viewport
  1. Prevents layout shift: Reserves space for images before they load
  1. Responsive images: Serves different sized images based on device
  1. Visual stability: Avoids cumulative layout shift (CLS)

Image Sources

Images can be loaded from two sources:

  1. 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
    />
  );
}
  1. 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 width and height props
  • Set fill={true} and use a parent element with position: 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:

  1. Static sizing (exact dimensions):
<Image
  src="/product.jpg"
  alt="Product"
  width={400}
  height={300}
/>
  1. Responsive sizing (adapts to parent):
<div className="relative w-full h-40">
  <Image
    src="/banner.jpg"
    alt="Banner"
    fill
    style={{ objectFit: 'cover' }}
  />
</div>
  1. 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:

  1. Always provide meaningful alt text for accessibility
  1. Set priority on LCP (Largest Contentful Paint) images to improve performance metrics
  1. Use appropriate image sizes to avoid unnecessary bandwidth usage
  1. Provide sizes attribute for responsive images to help the browser select the right source
  1. Use modern image formats like WebP and AVIF (Next.js handles this automatically)
  1. Avoid layout shift by always providing dimensions or using fill with a positioned parent
  1. Optimize for mobile first by ensuring images look good on smaller screens
  1. 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:

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

  1. Group by Feature: Organize files by feature rather than file type
  1. Keep Components Small: Break down large components into smaller, reusable ones
  1. Use Consistent Naming: Follow a naming convention throughout your project
  1. Leverage the App Directory: Use the new app directory structure for all new projects

Performance Optimization

  1. Use Server Components: Leverage Server Components for data fetching and static content
  1. Image Optimization: Always use the Next.js Image component for images
  1. Route Prefetching: Use Link components for internal navigation
  1. Lazy Loading: Use dynamic imports for components not needed immediately
  1. Optimize LCP: Set the priority prop on the largest image in the viewport
  1. Code Splitting: Let Next.js handle code splitting automatically

SEO

  1. Use Metadata API: Define metadata in your pages and layouts
  1. Ensure Semantic HTML: Use proper HTML elements for content structure
  1. Optimize Images: Include descriptive alt text for all images
  1. Create a Sitemap: Use Next.js built-in sitemap generation
  1. Add Structured Data: Include JSON-LD for rich search results

Development Workflow

  1. Use TypeScript: Add type safety to your application
  1. Test Components: Implement unit and integration tests
  1. Consistent Styling: Use a CSS framework like Tailwind CSS
  1. Error Handling: Implement error boundaries and fallbacks
  1. Environment Variables: Use .env files for configuration

Practice Exercises 🏋️

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

Blog CTA Snippet

Don't Have Time to Build Your SaaS?

Learning to code is powerful — but your time is better spent growing your business. Let our expert team handle your MVP while you focus on what matters.

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top