Next.js & Advanced Routing: A Comprehensive Guide

Routing is the backbone of any web application. It dictates how users navigate between different pages and interact with your content. In the world of Next.js, routing is a particularly powerful and flexible tool, offering developers a wide range of options to create dynamic and engaging user experiences. This tutorial will delve deep into advanced routing techniques in Next.js, equipping you with the knowledge to build complex and performant web applications.

Why Advanced Routing Matters

While basic routing is straightforward, real-world applications often demand more sophisticated routing strategies. Consider these scenarios:

  • Dynamic Content: Displaying content based on user input or data fetched from a database.
  • Nested Routes: Creating complex layouts with multiple levels of navigation.
  • Prefetching and Optimization: Improving user experience by preloading routes.
  • Custom Route Handling: Implementing specific logic for different routes.

Mastering advanced routing allows you to build applications that are not only functional but also highly optimized for performance and user experience. This is crucial for creating applications that rank well in search engines and keep users engaged.

Understanding the Basics: File-Based Routing

Before diving into advanced techniques, let’s recap the fundamentals. Next.js employs a file-based routing system. This means that the structure of your project’s `pages` directory directly translates into your application’s routes. For instance:

  • `pages/index.js` becomes the route `/` (the homepage).
  • `pages/about.js` becomes the route `/about`.
  • `pages/blog/post.js` becomes the route `/blog/post`.

This simple convention makes it incredibly easy to get started. Each JavaScript or TypeScript file within the `pages` directory represents a route. The content of these files are React components that render the content for each page.

Example: A Basic Route (pages/about.js)

function About() {
  return (
    <div>
      <h1>About Us</h1>
      <p>Learn more about our company.</p>
    </div>
  );
}

export default About;

In this example, the `About` component renders a simple “About Us” page accessible at `/about`.

Dynamic Routes

Dynamic routes are a powerful feature that allows you to create routes that handle variable data. They are defined using square brackets `[]` in the filename. This is particularly useful for things like displaying blog posts, product pages, or user profiles.

Example: Creating a Blog Post Route (pages/posts/[slug].js)

In this scenario, `[slug]` is a dynamic route segment. The `slug` part of the URL (e.g., `/posts/my-first-post`) will be available as a parameter within your component. The file structure dictates that anything after `/posts/` will be treated as the `slug` parameter.

import { useRouter } from 'next/router';

function Post() {
  const router = useRouter();
  const { slug } = router.query;

  return (
    <div>
      <h1>Post: {slug}</h1>
      <p>Content for post with slug: {slug} goes here.</p>
    </div>
  );
}

export default Post;

Explanation:

  • `useRouter`: This hook from `next/router` provides access to routing information.
  • `router.query`: This object contains the dynamic route parameters. In our example, `router.query.slug` will hold the value of the `slug` from the URL.
  • The component then uses the `slug` to fetch and display the appropriate content. In a real-world scenario, you’d likely fetch data from a database or API using the `slug` to retrieve the post details.

Data Fetching in Dynamic Routes

To fetch data based on the dynamic route parameters, you can use `getStaticProps` or `getServerSideProps`. `getStaticProps` is preferred for static content, while `getServerSideProps` is used for content that needs to be generated on each request. Let’s look at an example using `getStaticProps`.

import { useRouter } from 'next/router';

// Assuming you have a function to fetch post data by slug
async function getPostData(slug) {
  // Replace with your data fetching logic (e.g., from an API or database)
  const res = await fetch(`https://your-api.com/posts/${slug}`);
  const post = await res.json();
  return post;
}

// Function to get all possible slugs (for static site generation)
async function getAllPostSlugs() {
  // Replace with your logic to get all slugs
  const res = await fetch('https://your-api.com/posts');
  const posts = await res.json();
  return posts.map(post => ({ params: { slug: post.slug } }));
}

export async function getStaticPaths() {
  const paths = await getAllPostSlugs();
  return {
    paths,
    fallback: false, // or 'blocking' if you want to show a fallback page
  };
}

export async function getStaticProps({ params }) {
  const post = await getPostData(params.slug);

  if (!post) {
    return {
      notFound: true,
    };
  }

  return {
    props: {
      post,
    },
  };
}

function Post({ post }) {
  const router = useRouter();
  const { slug } = router.query;

  if (router.isFallback) {
    return <p>Loading...</p>; // Display a loading state during fallback
  }

  return (
    <div>
      <h1>{post.title}</h1>
      <p>{post.content}</p>
    </div>
  );
}

export default Post;

Explanation:

  • `getStaticPaths`: This function is required when using `getStaticProps` with dynamic routes. It tells Next.js which paths to pre-render at build time. It returns an array of objects, where each object has a `params` property containing the dynamic route parameters. `fallback: false` means that any paths not returned by `getStaticPaths` will result in a 404. `fallback: ‘blocking’` would show a loading state while Next.js generates the page on the server.
  • `getStaticProps`: This function fetches the data for a specific post based on the `slug` parameter. The fetched data is passed as props to the `Post` component.
  • The `Post` component receives the `post` prop and displays its content.
  • `router.isFallback`: This allows you to handle cases where a path is not pre-rendered (when using `fallback: true` or `fallback: ‘blocking’`).

Nested Routes

Nested routes allow you to create complex layouts with multiple levels of navigation. They are created by simply nesting folders within your `pages` directory. For example, to create a route like `/blog/article/my-article`, you would structure your files like this:

  • `pages/blog/article/my-article.js`

Example: Creating a Nested Route (pages/blog/article/[slug].js)


import { useRouter } from 'next/router';

function Article() {
  const router = useRouter();
  const { slug } = router.query;

  return (
    <div>
      <h1>Article: {slug}</h1>
      <p>Content for article with slug: {slug} goes here.</p>
    </div>
  );
}

export default Article;

This creates a route at `/blog/article/[slug]`. The `slug` parameter will be available in the component via `router.query.slug`.

Nested Layouts

Nested routes are also useful for creating nested layouts. You can use a layout component to wrap the content of nested routes. For example, you could have a `pages/blog/index.js` file that displays a list of blog posts and a `pages/blog/[slug].js` file that displays a single blog post. You could create a `components/BlogLayout.js` component to wrap the content of both of these routes, providing a consistent layout for all blog-related pages.


// components/BlogLayout.js
function BlogLayout({ children }) {
  return (
    <div>
      <header>
        <h1>My Blog</h1>
      </header>
      <main>
        {children}
      </main>
      <footer>
        <p>© 2024 My Blog</p>
      </footer>
    </div>
  );
}

export default BlogLayout;

Then, in your blog post pages:


// pages/blog/[slug].js
import BlogLayout from '../../components/BlogLayout';

function BlogPost({ post }) {
  return (
    <BlogLayout>
      <h1>{post.title}</h1>
      <p>{post.content}</p>
    </BlogLayout>
  );
}

export default BlogPost;

export async function getStaticProps({ params }) {
  // Fetch post data...
}

export async function getStaticPaths() {
  // Get all paths...
}

This approach keeps the layout consistent across all blog posts while allowing you to manage the content of each post individually. The `BlogLayout` component wraps the content of each blog post, providing the header, footer, and any other common elements.

Prefetching and Link Component

Next.js provides a “ component for client-side navigation. This component automatically prefetches the code for the linked page in the background when the link appears in the viewport. This significantly improves the perceived performance of your application by making page transitions feel instantaneous. This is a critical aspect of creating a fast and responsive user experience.

Example: Using the <Link> Component

import Link from 'next/link';

function HomePage() {
  return (
    <div>
      <h1>Welcome</h1>
      <Link href="/about">
        <a>About Us</a>
      </Link>
    </div>
  );
}

export default HomePage;

Explanation:

  • `import Link from ‘next/link’;`: This imports the `Link` component.
  • `<Link href=”/about”>`: This creates a link to the `/about` route.
  • `<a>About Us</a>`: You must wrap the `Link` component around an `<a>` (anchor) tag for semantic correctness and proper styling. The `href` attribute specifies the destination route.

Benefits of <Link>:

  • Client-Side Navigation: The `<Link>` component handles navigation on the client-side, avoiding full page reloads and providing a smoother user experience.
  • Automatic Prefetching: Next.js automatically prefetches the code for the linked page when it’s in the viewport, making subsequent navigations faster. This prefetching is a key optimization for performance.
  • SEO Friendly: The `<Link>` component renders standard `<a>` tags, which are crawlable by search engines.

Customizing Prefetching

You can control the prefetching behavior using the `prefetch` prop on the `Link` component. By default, `prefetch` is set to `true`. If you want to disable prefetching for a specific link, you can set `prefetch={false}`. This can be useful if you have links to pages that are very large or rarely accessed.

<Link href="/expensive-page" prefetch={false}>
  <a>Expensive Page</a>
</Link>

Custom Routes with `next.config.js`

While file-based routing is convenient, sometimes you need more control over your routes. You can use the `rewrites` option in `next.config.js` to create custom routes that map a specific URL path to a different page. This allows you to create more user-friendly URLs or to handle legacy routes.

Example: Rewriting a Route

Let’s say you want to rewrite `/old-about` to `/about`.


// next.config.js
module.exports = {
  async rewrites() {
    return [
      {
        source: '/old-about',
        destination: '/about',
      },
    ];
  },
};

Explanation:

  • `source`: This is the URL path that you want to rewrite.
  • `destination`: This is the internal path to which the URL should be rewritten. It points to the file in your `pages` directory that should handle the request.

Benefits of Rewrites:

  • URL Customization: Create cleaner, more user-friendly URLs.
  • Legacy Support: Redirect old URLs to new pages without breaking existing links.
  • Internal Routing: Map multiple URLs to the same internal page.

Redirects vs. Rewrites

In `next.config.js`, you can also define `redirects`. The key difference between redirects and rewrites is how the user sees the URL in their browser’s address bar.

  • Rewrites: The URL in the address bar *remains unchanged*. Next.js internally maps the requested URL to a different page.
  • Redirects: The URL in the address bar *changes* to the destination URL. This is a client-side or server-side redirect, depending on the configuration.

Example: Redirecting a Route


// next.config.js
module.exports = {
  async redirects() {
    return [
      {
        source: '/old-contact',
        destination: '/contact-us',  // Matched both paths
        permanent: true, // Use 301 redirect
      },
    ];
  },
};

In this example, when a user visits `/old-contact`, they will be redirected to `/contact-us`, and the URL in the address bar will change.

Catch-All Routes

Catch-all routes are a powerful feature for handling dynamic content that doesn’t have a fixed structure, such as user-generated content or content from a CMS. They allow you to capture all segments of a URL path after a specific prefix.

Example: Creating a Catch-All Route (pages/blog/[…slug].js)

The three dots `…` before `slug` indicate a catch-all route. This will capture all segments of the URL path after `/blog/`. For example:

  • `/blog/my-first-post` will be captured, and `router.query.slug` will be `[‘my-first-post’]`.
  • `/blog/category/news/article-1` will be captured, and `router.query.slug` will be `[‘category’, ‘news’, ‘article-1’]`.

import { useRouter } from 'next/router';

function CatchAllBlog() {
  const router = useRouter();
  const { slug } = router.query;

  return (
    <div>
      <h1>Catch-All Blog</h1>
      <p>Slug: {JSON.stringify(slug)}</p>
    </div>
  );
}

export default CatchAllBlog;

Explanation:

  • The `[…slug].js` file in the `pages/blog` directory handles all routes that start with `/blog/`.
  • `router.query.slug` will be an array containing the captured segments of the URL path.
  • You can then use the `slug` array to fetch data from an API or database.

Catch-All Route with `getStaticPaths`

When using `getStaticProps` with catch-all routes, you must also define `getStaticPaths`. This is because Next.js needs to know all the possible paths to pre-render at build time. You need to fetch all possible values for the catch-all segment, which can be challenging if the number of possible paths is very large or dynamic.


import { useRouter } from 'next/router';

// Assuming you have a function to fetch all blog posts
async function getAllPosts() {
  // Replace with your data fetching logic
  const res = await fetch('https://your-api.com/posts');
  const posts = await res.json();
  return posts;
}

export async function getStaticPaths() {
  const posts = await getAllPosts();
  const paths = posts.map(post => ({
    params: {
      slug: [post.category, post.slug], // Example: ['category', 'article-slug']
    },
  }));
  return {
    paths,
    fallback: false, // or 'blocking'
  };
}

export async function getStaticProps({ params }) {
  const { slug } = params;
  const post = await getPostData(slug); // Fetch data based on the slug array

  if (!post) {
    return {
      notFound: true,
    };
  }

  return {
    props: {
      post,
    },
  };
}

function CatchAllBlog({ post }) {
  const router = useRouter();
  const { slug } = router.query;

  if (router.isFallback) {
    return <p>Loading...</p>; // Display a loading state during fallback
  }

  return (
    <div>
      <h1>{post.title}</h1>
      <p>{post.content}</p>
    </div>
  );
}

export default CatchAllBlog;

Common Mistakes and How to Fix Them

Even experienced developers can run into problems when working with advanced routing. Here are some common mistakes and how to avoid them:

  • Incorrect File Paths: Double-check your file paths in the `pages` directory. Typos or incorrect nesting can lead to unexpected routing behavior.
  • Missing `getStaticPaths`: If you’re using `getStaticProps` with dynamic or catch-all routes, you must define `getStaticPaths`. Otherwise, your site won’t build correctly.
  • Incorrect Parameter Access: Ensure you’re accessing the correct parameters from `router.query`. For example, in a catch-all route, the parameters are in an array (e.g., `router.query.slug[0]` for the first segment).
  • Forgetting to Import `Link`: When using the `<Link>` component, make sure you import it from `next/link`.
  • Not Using `fallback`: When using `getStaticProps` with dynamic routes and `fallback: true` or `fallback: ‘blocking’`, remember to handle the `router.isFallback` state to display a loading indicator or a fallback page while the page is being generated.
  • Caching Issues: Be mindful of caching when using `getServerSideProps`. If you’re fetching data that changes frequently, consider appropriate caching strategies to avoid performance bottlenecks.

SEO Considerations

Proper routing is crucial for SEO. Here are some SEO best practices to keep in mind:

  • Use Descriptive URLs: Create URLs that are clear, concise, and include relevant keywords. For example, `/blog/my-article-title` is better than `/blog/123`.
  • Implement Canonical URLs: Use the `<link rel=”canonical”>` tag in the `<head>` of your pages to specify the preferred version of a URL. This helps prevent duplicate content issues.
  • Create a Sitemap: Generate a sitemap (e.g., `sitemap.xml`) and submit it to search engines. This helps them discover and index your pages. Next.js can be used to generate sitemaps dynamically.
  • Use Meta Tags: Include descriptive meta titles and meta descriptions for each page. Next.js provides tools to manage these effectively.
  • Ensure Crawlability: Use the `<Link>` component or standard `<a>` tags for internal links to ensure search engine crawlers can navigate your site.
  • Optimize for Mobile: Ensure your website is responsive and mobile-friendly, as mobile-first indexing is now a standard practice.

Summary / Key Takeaways

Advanced routing in Next.js offers a powerful and flexible way to build dynamic and performant web applications. By understanding file-based routing, dynamic routes, nested routes, the “ component, and custom routes with `next.config.js`, you can create complex applications with ease. Remember to pay attention to SEO best practices, handle potential errors, and optimize your application for performance. Mastering these techniques will enable you to build modern, user-friendly, and search-engine-optimized web applications with Next.js.

The journey of mastering advanced routing is a continuous learning process. As you build more complex applications, you will encounter new challenges and discover innovative solutions. Next.js provides a robust foundation for building modern web applications, and its routing capabilities are a key component of its power and flexibility. Embrace the learning curve, experiment with different techniques, and continually refine your approach to create exceptional web experiences. The ability to craft intuitive and efficient navigation systems is a valuable skill that will serve you well as a developer.