Optimizing Performance and SEO

6. Optimizing Performance and SEO

In the competitive world of web development, a fast-loading and search-engine-friendly application isn’t just a luxury—it’s a necessity. Next.js is built with performance and SEO in mind, offering powerful features out of the box. This chapter will guide you through leveraging these features, focusing on image optimization, font optimization, and robust metadata management to ensure your applications are both blazing fast and highly discoverable.

6.1 Image Optimization with next/image

Images often account for the largest portion of a webpage’s size, significantly impacting load times. Next.js provides the next/image component, a powerful tool that automatically optimizes images for performance and an improved user experience.

Key Features of next/image:

  • Automatic Image Optimization: Next.js automatically resizes, optimizes, and serves images in modern formats (like WebP or AVIF) when the browser supports them. This dramatically reduces file sizes without compromising visual quality.
  • Lazy Loading by Default: Images outside the viewport are not loaded until they are scrolled into view, reducing initial page load time.
  • Responsive Images: Automatically generates srcset attributes, serving different image sizes for various screen resolutions and device pixel ratios.
  • Prevention of Cumulative Layout Shift (CLS): Requires width and height props (or the fill prop) to reserve space, preventing layout shifts as images load.
  • External Image Sources: Supports optimization for images hosted on external domains with simple configuration.
  • Image Placeholders: Provides a placeholder prop to show a blurred placeholder while the image loads, enhancing perceived performance.

Basic Usage

To use next/image, simply import it and replace your standard <img> tags.

Example:

Let’s use next/image on our home page.

  1. Open src/app/page.tsx.

  2. Add an image from an external source (make sure to configure next.config.mjs for remote patterns if needed).

    // src/app/page.tsx
    import Link from 'next/link';
    import CustomButton from './components/CustomButton';
    import Image from 'next/image'; // Import the Image component
    
    export default function Home() {
      const handleButtonClick = (buttonName: string) => {
        alert(`${buttonName} button clicked!`);
      };
    
      return (
        <main className="min-h-screen flex flex-col items-center justify-center p-6 bg-gradient-to-br from-blue-50 to-indigo-100 text-gray-800">
          <Image
            src="https://images.unsplash.com/photo-1695669748983-63b7e75f4d1c?q=80&w=2940&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D"
            alt="Modern desk setup with laptop and coffee"
            width={700} // Original width of the image
            height={400} // Original height of the image
            priority // Load this image with high priority (for LCP)
            className="rounded-lg shadow-xl mb-8"
            style={{ objectFit: 'cover' }} // Ensure image covers its area
          />
    
          <h1 className="text-5xl font-extrabold text-blue-800 mb-4">Welcome to Next.js!</h1>
          <p className="text-xl text-gray-700 mb-8 max-w-2xl text-center">
            This is the homepage of your Next.js application. <br />
            Let's learn and build amazing things!
          </p>
          <div className="flex space-x-4 mb-12">
            <Link href="/blog" className="text-blue-600 hover:text-blue-800 font-medium text-lg">
              View Blog Posts
            </Link>
            <Link href="/products" className="text-blue-600 hover:text-blue-800 font-medium text-lg">
              Explore Products
            </Link>
            <Link href="/users" className="text-blue-600 hover:text-blue-800 font-medium text-lg">
              See Users
            </Link>
          </div>
    
          <div className="mt-12 pt-8 border-t border-dashed border-gray-300 text-center">
            <h2 className="text-3xl font-bold text-gray-900 mb-6">Check out our custom buttons:</h2>
            <div className="flex justify-center gap-4">
              <CustomButton onClick={() => handleButtonClick('Primary')} variant="primary">
                Primary Action
              </CustomButton>
              <CustomButton onClick={() => handleButtonClick('Secondary')} variant="secondary" size="large">
                Large Secondary Action
              </CustomButton>
            </div>
          </div>
        </main>
      );
    }
    
  3. Configure next.config.mjs for remote images: If you’re using external images (like from Unsplash, as in the example), you must configure remotePatterns in next.config.mjs for security and optimization.

    // next.config.mjs
    /** @type {import('next').NextConfig} */
    const nextConfig = {
      images: {
        remotePatterns: [
          {
            protocol: 'https',
            hostname: 'images.unsplash.com', // Allow images from Unsplash
            port: '',
            pathname: '**',
          },
          // Add other remote hosts here if needed
        ],
      },
    };
    
    export default nextConfig;
    

    You might need to restart your development server (npm run dev) after modifying next.config.mjs.

  4. Save the files and visit http://localhost:3000. Open your browser’s developer tools (Network tab) and observe the image. Next.js will likely have converted it to WebP (or AVIF) and generated multiple srcset entries, showing its optimization in action.

Important next/image Props:

  • src: The path to your image (local or remote URL).
  • alt: Essential for accessibility and SEO. Describes the image content.
  • width and height: Required for preventing CLS. Specifies the intrinsic dimensions.
  • priority: Set to true for images that are “above the fold” (visible on initial load) to prioritize loading and improve Largest Contentful Paint (LCP).
  • fill: When true, the image will fill its parent element (which must have position: relative, absolute, or fixed). Useful for fluid images where explicit width/height might be restrictive.
  • sizes: Provides a comma-separated list of media conditions and image slot widths. Crucial for generating optimal srcset when fill is used or for more complex responsive layouts.

Exercises/Mini-Challenges (Image Optimization):

  1. Add a placeholder to your homepage image:

    • Set placeholder="blur" and add a blurDataURL (you can generate a base64 blurhash or use a small, blurred version of the image for local static images).
    • For external images, you might need a library like plaiceholder (which you’d install) or manually provide a very small base64 image representation for testing. For this exercise, you can set placeholder="empty" to just see the effect of the space being reserved.
  2. Optimize an image in the Blog Page:

    • Find a suitable image (either local in /public or an external one) and add it to src/app/blog/page.tsx using next/image.
    • Ensure it has width, height, and alt attributes.
    • If it’s an image that appears early on the page, consider adding priority.

6.2 Font Optimization with next/font

Web fonts can also significantly impact performance, causing layout shifts (Flash of Unstyled Text - FOUT, or Flash of Invisible Text - FOIT). Next.js’s next/font module automatically optimizes fonts, eliminating external network requests and ensuring efficient loading.

Key Features of next/font:

  • Automatic Self-Hosting: Downloads font files at build time and serves them from your domain. No requests are sent to Google (or other providers) by the browser, improving privacy and performance.
  • Zero Layout Shift: Uses CSS size-adjust to maintain the layout during font loading, preventing CLS.
  • Optimized Loading: Reduces font file sizes through subsetting (only includes characters needed) and uses modern font formats (WOFF2).
  • Google Fonts & Local Fonts Support: Works seamlessly with both popular Google Fonts and your self-hosted local font files.

Using Google Fonts with next/font/google

  1. Open your root layout.tsx file (src/app/layout.tsx).

  2. Import the desired font from next/font/google. We are already using Inter, but let’s add Roboto_Mono for code blocks.

    // src/app/layout.tsx
    import type { Metadata } from "next";
    import { Inter, Roboto_Mono } from "next/font/google"; // Import Roboto_Mono
    import "./globals.css";
    
    const inter = Inter({ subsets: ["latin"], variable: '--font-inter' }); // Add variable
    const roboto_mono = Roboto_Mono({ subsets: ["latin"], variable: '--font-roboto-mono' }); // Define for code
    
    export const metadata: Metadata = {
      title: "My Next.js App",
      description: "Learning Next.js styling",
    };
    
    export default function RootLayout({
      children,
    }: Readonly<{
      children: React.ReactNode;
    }>) {
      return (
        // Apply the font variables to the html element
        <html lang="en" className={`${inter.variable} ${roboto_mono.variable}`}>
          <body className={inter.className}>{children}</body> {/* Inter is the default body font */}
        </html>
      );
    }
    
  3. Configure Tailwind CSS to use the font variable: (If you are using Tailwind)

    • Open tailwind.config.ts.
    • Update the fontFamily extension.
    // tailwind.config.ts
    import type { Config } from 'tailwindcss';
    
    const config: Config = {
      content: [
        './pages/**/*.{js,ts,jsx,tsx,mdx}',
        './components/**/*.{js,ts,jsx,tsx,mdx}',
        './app/**/*.{js,ts,jsx,tsx,mdx}',
        './src/**/*.{js,ts,jsx,tsx,mdx}',
      ],
      theme: {
        extend: {
          backgroundImage: {
            'gradient-radial': 'radial-gradient(var(--tw-gradient-stops))',
            'gradient-conic':
              'conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))',
          },
          fontFamily: { // Add font family configuration
            sans: ['var(--font-inter)', 'sans-serif'],
            mono: ['var(--font-roboto-mono)', 'monospace'],
          },
        },
      },
      plugins: [],
    };
    export default config;
    
  4. Apply the font-mono class to elements where you want to use the monospace font (e.g., <pre> or <code> tags).

    • Let’s add a code example to src/app/page.tsx that uses the font-mono class:
    // src/app/page.tsx (inside the <main> element, perhaps near the bottom)
    // ... existing JSX ...
    
          <div className="mt-12 pt-8 border-t border-dashed border-gray-300 text-center">
            <h2 className="text-3xl font-bold text-gray-900 mb-6">Check out our custom buttons:</h2>
            <div className="flex justify-center gap-4">
              <CustomButton onClick={() => handleButtonClick('Primary')} variant="primary">
                Primary Action
              </CustomButton>
              <CustomButton onClick={() => handleButtonClick('Secondary')} variant="secondary" size="large">
                Large Secondary Action
              </CustomButton>
            </div>
          </div>
    
          <div className="mt-12 pt-8 border-t border-dashed border-gray-300">
            <h2 className="text-2xl font-bold text-gray-900 mb-4">Code Example:</h2>
            <pre className="bg-gray-800 text-white p-4 rounded-md overflow-x-auto font-mono text-left">
              {`// Example of using Roboto Mono for code
    

function greet(name: string) { console.log(`Hello, ${name}!`); }

greet(“World”); `} ); } ```

  1. Save all files. Restart your dev server if you changed tailwind.config.ts. Visit http://localhost:3000. The code block should now use Roboto Mono without causing any layout shifts during load.

Using Local Fonts with next/font/local

For custom fonts not available on Google Fonts, you can use next/font/local.

Example:

  1. Place your font file (e.g., MyCustomFont.woff2) in your public directory or a dedicated src/fonts folder. Let’s assume src/fonts/MyCustomFont.woff2.

  2. Define and import in src/app/layout.tsx:

    // src/app/layout.tsx
    // ... other imports ...
    import localFont from 'next/font/local'; // Import local font
    
    // Define your local font
    const myLocalFont = localFont({
      src: '../fonts/MyCustomFont.woff2', // Path relative to the layout.tsx file
      display: 'swap', // Fallback behavior
      variable: '--font-local-custom', // CSS variable for Tailwind
      // You can also specify multiple sources for different weights/styles:
      // src: [
      //   {
      //     path: '../fonts/MyCustomFont-Regular.woff2',
      //     weight: '400',
      //     style: 'normal',
      //   },
      //   {
      //     path: '../fonts/MyCustomFont-Bold.woff2',
      //     weight: '700',
      //     style: 'normal',
      //   },
      // ],
    });
    
    export const metadata: Metadata = {
      // ...
    };
    
    export default function RootLayout({
      children,
    }: Readonly<{
      children: React.ReactNode;
    }>) {
      return (
        // Add the local font variable to the html tag
        <html lang="en" className={`${inter.variable} ${roboto_mono.variable} ${myLocalFont.variable}`}>
          <body className={inter.className}>{children}</body>
        </html>
      );
    }
    
  3. Update tailwind.config.ts to use the local font variable, just like with Google Fonts.

Exercises/Mini-Challenges (Font Optimization):

  1. Apply Roboto Mono to specific elements:

    • Instead of just the <pre> tag, find another small element on your site (e.g., a specific paragraph or list item) and apply className="font-mono" to it. Observe its styling.
  2. Experiment with display property:

    • In the Inter font definition in src/app/layout.tsx, temporarily change display: 'swap' to display: 'block'. (Note: ‘swap’ is generally preferred for performance).
    • Observe if you can detect any “Flash of Unstyled Text” when refreshing the page. Then change it back.

6.3 Managing Metadata for SEO

Metadata (like titles, descriptions, and Open Graph tags) is crucial for Search Engine Optimization (SEO) and how your content appears on social media. Next.js, especially with the App Router, provides a powerful and declarative API for managing metadata.

Defining Metadata

You can define metadata in two ways:

  1. Static Metadata: Export a metadata object in a layout.tsx or page.tsx file.
  2. Dynamic Metadata: Export an async function generateMetadata() in a layout.tsx or page.tsx file to fetch data and dynamically generate metadata.

Next.js automatically handles merging metadata from parent layouts down to pages, ensuring global defaults can be overridden by more specific page-level metadata.

Example: Static Metadata in Root Layout

You already have static metadata in src/app/layout.tsx:

// src/app/layout.tsx
// ...
export const metadata: Metadata = {
  title: "My Next.js App",
  description: "Learning Next.js styling",
  // You can add more here:
  // keywords: ["Next.js", "React", "Web Development", "SEO"],
  // authors: [{ name: "AI Expert" }],
  // openGraph: {
  //   title: "My Next.js App - The Ultimate Guide",
  //   description: "A comprehensive guide to Next.js for beginners.",
  //   url: "https://yourwebsite.com",
  //   siteName: "My Next.js App",
  //   images: [
  //     {
  //       url: "https://yourwebsite.com/og-image.jpg", // Must be absolute URL
  //       width: 1200,
  //       height: 630,
  //       alt: "My Next.js App logo",
  //     },
  //   ],
  //   locale: "en_US",
  //   type: "website",
  // },
  // twitter: {
  //   card: "summary_large_image",
  //   title: "My Next.js App - The Ultimate Guide",
  //   description: "A comprehensive guide to Next.js for beginners.",
  //   images: ["https://yourwebsite.com/twitter-image.jpg"],
  //   creator: "@yourtwitterhandle",
  // },
};
// ...

Example: Dynamic Metadata for a Blog Post

Let’s add dynamic metadata to our blog post detail page (src/app/blog/[postId]/page.tsx). First, we need to create a [postId] dynamic route similar to how we did for products or users.

  1. Create a dynamic route folder for blog posts: src/app/blog/[postId].

    mkdir src/app/blog/[postId]
    touch src/app/blog/[postId]/page.tsx
    
  2. Add a simple getPostDetails function (simulated) for our blog posts.

    • Create src/lib/blogPosts.ts
    // src/lib/blogPosts.ts
    export interface BlogPost {
      id: string;
      title: string;
      content: string;
      author: string;
      date: string;
      imageUrl?: string;
      excerpt: string;
    }
    
    const posts: BlogPost[] = [
      {
        id: 'nextjs-intro',
        title: 'Getting Started with Next.js',
        content: 'This post covers the basics of setting up a Next.js project and understanding its core features. It\'s a great starting point for beginners.',
        author: 'Jane Doe',
        date: '2025-01-15',
        imageUrl: 'https://images.unsplash.com/photo-1593720213428-fee66e40ce44?q=80&w=2938&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
        excerpt: 'A beginner-friendly guide to Next.js fundamentals.'
      },
      {
        id: 'data-fetching-patterns',
        title: 'Mastering Data Fetching in Next.js',
        content: 'Dive deep into various data fetching strategies in Next.js, including SSR, SSG, and client-side fetching. Learn when to use each for optimal performance.',
        author: 'John Smith',
        date: '2025-03-20',
        imageUrl: 'https://images.unsplash.com/photo-1549490104-5470783a32f0?q=80&w=2940&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
        excerpt: 'Understanding SSR, SSG, and client-side data loading.'
      },
      {
        id: 'styling-nextjs',
        title: 'Beautiful UIs: Styling in Next.js',
        content: 'Explore different styling approaches in Next.js, from global CSS and modules to Tailwind CSS. Build visually appealing and maintainable applications.',
        author: 'Alice Wonderland',
        date: '2025-05-10',
        imageUrl: 'https://images.unsplash.com/photo-1581472723648-5264b3c4f74d?q=80&w=2940&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
        excerpt: 'A guide to CSS Modules, Tailwind, and more.'
      }
    ];
    
    export async function getPostDetails(postId: string): Promise<BlogPost | undefined> {
      await new Promise(resolve => setTimeout(resolve, 500)); // Simulate API call
      return posts.find(post => post.id === postId);
    }
    
    export async function getAllPostIds(): Promise<string[]> {
      await new Promise(resolve => setTimeout(resolve, 100));
      return posts.map(post => post.id);
    }
    
    export async function getAllBlogPosts(): Promise<BlogPost[]> {
      await new Promise(resolve => setTimeout(resolve, 100));
      return posts;
    }
    
  3. Update src/app/blog/page.tsx to list actual blog posts with links:

    // src/app/blog/page.tsx
    import Link from 'next/link';
    import { getAllBlogPosts } from '@/lib/blogPosts'; // Import our new function
    
    interface Post {
      id: string;
      title: string;
      excerpt: string;
    }
    
    export default async function BlogPage() {
      const posts: Post[] = await getAllBlogPosts();
    
      return (
        <main className="container mx-auto p-6 max-w-3xl font-sans text-gray-800">
          <h1 className="text-4xl font-extrabold text-gray-900 mb-6 text-center">Latest Blog Posts</h1>
          <p className="text-lg text-gray-700 mb-8 text-center">Stay updated with our latest articles on Next.js and web development.</p>
    
          <ul className="list-none p-0">
            {posts.map((post) => (
              <li key={post.id} className="mb-6 p-6 bg-white rounded-lg shadow-md hover:shadow-lg transition-shadow duration-200">
                <Link href={`/blog/${post.id}`} className="block text-decoration-none">
                  <h2 className="text-3xl font-bold text-blue-700 hover:text-blue-900 mb-2">
                    {post.title}
                  </h2>
                  <p className="text-gray-600 mb-4">{post.excerpt}</p>
                  <span className="text-blue-600 font-medium hover:underline">Read More &rarr;</span>
                </Link>
              </li>
            ))}
          </ul>
          <div className="mt-8 text-center">
            <Link href="/" className="text-gray-600 hover:text-gray-800 font-medium text-lg">&larr; Back to Home</Link>
          </div>
        </main>
      );
    }
    
  4. Add generateMetadata to src/app/blog/[postId]/page.tsx:

    // src/app/blog/[postId]/page.tsx
    import type { Metadata, ResolvingMetadata } from 'next';
    import { getPostDetails, getAllPostIds } from '@/lib/blogPosts';
    import { notFound } from 'next/navigation';
    import Link from 'next/link';
    import Image from 'next/image';
    
    // Define props for generateMetadata and BlogPost component
    interface BlogPostPageProps {
      params: { postId: string };
    }
    
    // --- Dynamic Metadata Generation ---
    export async function generateMetadata(
      { params }: BlogPostPageProps,
      parent: ResolvingMetadata // Access parent metadata (e.g., from layout.tsx)
    ): Promise<Metadata> {
      const post = await getPostDetails(params.postId);
    
      // If post not found, Next.js will eventually show a 404,
      // but we return default metadata here if data fetching fails for metadata.
      if (!post) {
        return {
          title: 'Post Not Found',
          description: 'The requested blog post could not be found.',
        };
      }
    
      // Optionally, get and merge parent metadata
      const previousImages = (await parent).openGraph?.images || [];
    
      return {
        title: post.title,
        description: post.excerpt,
        keywords: [post.title.split(' ')[0], 'Next.js', 'blog', post.author],
        authors: [{ name: post.author }],
        openGraph: {
          title: post.title,
          description: post.excerpt,
          url: `https://yourwebsite.com/blog/${post.id}`, // Canonical URL
          siteName: 'My Next.js App',
          images: post.imageUrl ? [{ url: post.imageUrl, width: 1200, height: 630, alt: post.title }] : previousImages,
          locale: 'en_US',
          type: 'article',
        },
        twitter: {
          card: 'summary_large_image',
          title: post.title,
          description: post.excerpt,
          images: post.imageUrl ? [post.imageUrl] : previousImages,
          creator: `@${post.author.replace(/\s/g, '')}`, // Simple twitter handle generation
        },
      };
    }
    
    // --- Dynamic Page Content ---
    export default async function BlogPostPage({ params }: BlogPostPageProps) {
      const post = await getPostDetails(params.postId);
    
      if (!post) {
        notFound(); // Renders the nearest not-found.tsx or a default 404
      }
    
      return (
        <main className="container mx-auto p-6 max-w-3xl bg-white shadow-lg rounded-lg mt-10">
          <Link href="/blog" className="text-blue-600 hover:text-blue-800 mb-4 inline-block">&larr; Back to Blog</Link>
          {post.imageUrl && (
            <Image
              src={post.imageUrl}
              alt={post.title}
              width={800}
              height={450}
              priority
              className="rounded-md w-full h-auto object-cover mb-6"
            />
          )}
          <h1 className="text-4xl font-extrabold text-gray-900 mb-4">{post.title}</h1>
          <p className="text-gray-600 text-sm mb-4">
            By {post.author} on {new Date(post.date).toLocaleDateString()}
          </p>
          <div className="prose prose-lg max-w-none text-gray-700">
            <p>{post.content}</p>
            {/* Add more content here */}
          </div>
        </main>
      );
    }
    
    // Optional: generateStaticParams for SSG and dynamic routes
    // This allows Next.js to pre-render the pages at build time.
    export async function generateStaticParams() {
      const postIds = await getAllPostIds();
      return postIds.map((id) => ({
        postId: id,
      }));
    }
    
  5. Save all files. Navigate to http://localhost:3000/blog and click on a blog post.

    • Open your browser’s developer tools (Elements tab) and inspect the <head> section. You should see the title, description, og:title, og:description, etc., dynamically generated based on the specific blog post’s data.

Key Metadata Properties

  • title: The page title shown in browser tabs and search results. Can be an object with template and absolute for consistent formatting.
  • description: A concise summary of the page content for search engine snippets.
  • keywords: A comma-separated list of relevant keywords.
  • authors: An array of author objects.
  • openGraph: For rich social media previews (Facebook, LinkedIn). Includes title, description, url, images, type, locale, etc.
  • twitter: For Twitter Card previews. Includes card, title, description, images, creator.
  • alternates: For canonical URLs, language alternates, and media-specific URLs.
  • viewport: Configures the viewport for responsive behavior.
  • robots: Controls search engine crawling and indexing behavior.

Exercises/Mini-Challenges (Metadata):

  1. Add robots.txt and sitemap.xml:

    • Create a robots.ts file directly under src/app to guide search engine crawlers. Example content:
      // src/app/robots.ts
      import { MetadataRoute } from 'next';
      
      export default function robots(): MetadataRoute.Robots {
        return {
          rules: [
            {
              userAgent: '*',
              allow: '/',
              disallow: ['/admin/', '/private/'], // Example disallowed paths
            },
          ],
          sitemap: 'https://yourwebsite.com/sitemap.xml',
        };
      }
      
    • Create a sitemap.ts file under src/app to list all your public pages. You can use the getAllPostIds and getAllUserIds (if you implement it similarly) to dynamically generate parts of the sitemap.
      // src/app/sitemap.ts
      import { MetadataRoute } from 'next';
      import { getAllPostIds } from '@/lib/blogPosts'; // Re-use our helper
      // import { getAllUserIds } from '@/lib/db'; // If you made one for users
      
      export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
        const baseUrl = 'http://localhost:3000'; // Replace with your production domain
      
        const staticRoutes: MetadataRoute.Sitemap = [
          { url: baseUrl, lastModified: new Date(), changeFrequency: 'weekly', priority: 1 },
          { url: `${baseUrl}/blog`, lastModified: new Date(), changeFrequency: 'daily', priority: 0.8 },
          { url: `${baseUrl}/products`, lastModified: new Date(), changeFrequency: 'monthly', priority: 0.7 },
          { url: `${baseUrl}/users`, lastModified: new Date(), changeFrequency: 'monthly', priority: 0.6 },
          { url: `${baseUrl}/jokes`, lastModified: new Date(), changeFrequency: 'yearly', priority: 0.5 },
        ];
      
        const postIds = await getAllPostIds();
        const blogPostRoutes: MetadataRoute.Sitemap = postIds.map((id) => ({
          url: `${baseUrl}/blog/${id}`,
          lastModified: new Date(), // Could be post.updatedAt
          changeFrequency: 'weekly',
          priority: 0.9,
        }));
      
        // Merge all routes
        return [...staticRoutes, ...blogPostRoutes];
      }
      
    • Test by visiting http://localhost:3000/robots.txt and http://localhost:3000/sitemap.xml.
  2. Add a notFound.tsx file to a dynamic route:

    • In src/app/blog/[postId], create a not-found.tsx file.
    • This component will be rendered if notFound() is called from page.tsx.
    • Add some custom message and a link back to the blog list.
    • Test by navigating to a non-existent blog post ID (e.g., http://localhost:3000/blog/non-existent-post).

By meticulously optimizing images, fonts, and metadata, you’re not only creating a visually appealing experience but also ensuring your Next.js applications perform exceptionally well and rank highly in search results, reaching a wider audience.