How to Create a Table of Contents with Next.js 13 + 14

· 17 min read

Table of Contents

    Introduction (with Next 14 update)

    With the recent releases of Next.js 13 and 14, web developers, Next.js beginners, and content creators are eager to explore its new capabilities and features. Next.js, an open-source React framework, has quickly become a favored tool for many in the developer community. Among the many functionalities it offers, one particular feature that can significantly improve user experience, especially on content-heavy websites or blogs, is the Table of Contents (TOC).

    Update 10/26 2023 - Next.js 14 Released

    Next.js 14 has been released, including Server Actions (Stable) and Partial Prerendering (Preview). The guidance in this post still applies.

    A properly implemented TOC allows your users to navigate through your content seamlessly, jumping directly to the sections that interest them the most. Not only does this provide added convenience to the users, but it also enhances the overall accessibility of your website. While creating a TOC might sound like a complicated process, the state-of-the-art features of Next.js 13 / 14 make it an achievable task, even for beginners.

    In the following sections, we will cover in detail how you can create a dynamic TOC with Next.js 13 / 14. We will guide you through the process of fetching text from headings in your markdown articles using MDX syntax, creating a linked list of headings, and ensuring that clicking a heading leads to the appropriate section.

    We will further elaborate on topics such as enabling smooth scrolling when users click on a TOC link, and highlighting active links when their corresponding headers are visible on the webpage. To make the learning experience more practical, we will also provide real-world examples and code snippets for implementing these functionalities.

    Whether you're an experienced web developer looking to extend your Next.js knowledge, a beginner trying to get a hang of this powerful framework, or a blogger aiming to enhance your website's navigability, this guide aims to provide you with clear and thorough instructions. By the end of this tutorial, you should be able to implement a TOC using Next.js 13 / 14, improving your website's user experience and accessibility.

    Understanding the basics of Next.js App Router

    Next.js 13 and 14, the latest iterations of the widely-adopted open-source React framework, extends a number of powerful features to developers for crafting fast, dynamic websites or applications. These versions are celebrated by some for their robust performance and the enhanced flexibility it affords for server-side rendering, static site generation, and API routes.

    A central feature of both Next.js 13 and 14 are their evolved routing mechanism, now encompassing both file system-based routing via the 'pages' directory and the new App Router introduced in the 'app' directory.

    The file system-based routing remains intuitive; for example, a file named 'about.js' in your 'pages' directory automatically configures the route 'www.your-website.com/about'. This mechanism simplifies the process of configuring and managing routes, reducing the overhead traditionally associated with routing setup.

    On the other hand, the App Router brings a fresh perspective to routing and data fetching in Next.js. It facilitates co-location of components with their required data, suspense handling, and provides a structured way to manage layouts across your application.

    Another core functionality offered by Next.js 13 / 14 is the dynamic route segments feature. This enables you to build routes with dynamic parts, so you can create pages that can render varying content based on the route parameters. This is particularly useful when you are working on projects like blogs or e-commerce sites where you might want to generate pages for each blog post or product dynamically.

    Next.js 13 / 14 also allows for fetching and rendering markdown content using MDX and next-mdx-remote/rsc. MDX is a syntax extension for markdown that lets you write JSX directly in your markdown files, meaning you can import and use React components within your content.

    Additionally, Next.js 13 / 14 puts a significant emphasis on SEO optimization. It includes features like the Metadata API to manage your website metadata effectively. You can also integrate sitemap support and RSS feed generation into your project for better discoverability and accessibility.

    Finally, when it comes to deployment, you can use any static site provider, but Next.js 13 / 14 is optimized for Vercel deployments. Vercel is a cloud platform for serverless deployment developed by the creators of Next.js, which allows for an incredibly smooth deployment experience.

    By understanding these basic features and principles of the newer versions of Next.js, you've already taken the first step towards creating dynamic, robust, and SEO-optimized web applications, including adding features like a table of contents to your webpages. Now, let's move onto the specifics of how a TOC can be implemented using these features and principles.

    Creating a Table of Contents with JSX in Next.js 13 / 14

    When working on a Table of Contents (TOC) in Next.js, the primary task is to organize your content with headings, which will serve as the foundation for your TOC. Here we will utilize JSX to define our content and headings directly within our Next.js components.

    React, the core of Next.js, facilitates rendering the TOC through a dedicated component.

    To begin, create a new React component, which we'll call `TableOfContents`. The main purpose of this component is to take an array of headings as input and display it as an interactive list of links.

    const TableOfContents = ({ headings }) => {
    // Rendering the TOC goes here
    }

    In the above code snippet, we've declared a functional React component that accepts `headings` as a prop. This array holds the headings we've extracted from our markdown content. Each heading in the array can be an object consisting of the title of the heading and its unique ID.

    Next, we'll iterate over the `headings` array and create a list item for each heading. Each list item will be a link that points to the corresponding section in the content. We'll use the unique ID previously generated for each heading as the `href` for each link. Here's how to do this using the `map` function in JavaScript:

    const TableOfContents = ({ headings }) => {
      return (
        <nav>
          <ul>
            {headings.map((heading) => (
              <li key={heading.id}>
                <a href={`#${heading.id}`}>
                  {heading.title}
                </a>
              </li>
            ))}
          </ul>
        </nav>
      );
    };

    In this `TableOfContents` component, we're rendering a `nav` element that contains an unordered list (`ul`). Then, we're mapping each heading to a list item (`li`) that contains an anchor (`a`) tag. The `href` attribute of the anchor tag is assigned the value of `#${heading.id}`, which links to the corresponding section in the content. The text inside the anchor tag is set to the title of the heading.

    Initially, in your JSX file, you'll have your content structured under various headings as illustrated below:

    export default function Article() {
      return (
        <div>
          <h1 id="title">Title of the Article</h1>
          <section id="section1">
            <h2>Section 1</h2>
            <p>Your content for Section 1.</p>
          </section>
          <section id="section2">
            <h2>Section 2</h2>
            <p>Your content for Section 2.</p>
          </section>
        </div>
      );
    }
    

    Now, you can prepare an array of headings based on your content structure, which will be passed to the TableOfContents component:

    const headings = [
      { id: 'title', title: 'Title of the Article' },
      { id: 'section1', title: 'Section 1' },
      { id: 'section2', title: 'Section 2' },
    ];
    

    In this simple example, we've created a start for the Table of Contents. However, a truly effective TOC would also include features like smooth scrolling and dynamic highlighting of the active link based on which section is currently visible on the page. While these require additional steps and more advanced knowledge of React and Next.js, they significantly enhance the user experience. We will learn more about these functionalities in the next chapters.

    At this point, you should have a functional TOC that lists the different sections of your content and allows the user to navigate directly to any section by clicking on its title in the TOC. While it may seem straightforward, this feature greatly enhances the navigability and accessibility of your Next.js 13 / 14 site, benefiting both your users and the SEO of your site.

    After successfully creating a basic Table of Contents (TOC) with clickable links, the next step is to enhance the user navigation experience with smooth scrolling and active link highlighting. These features not only make your TOC more interactive but also provide visual feedback to your users, enhancing the overall user experience.

    Smooth Scrolling

    When a user clicks on a link in the TOC, it's more visually appealing if the page doesn't jump abruptly to that section, but scrolls smoothly to it. You can achieve this through some CSS magic.

    Add a CSS rule for smooth scrolling in your global stylesheet (`styles.css` or `styles.scss` if you're using Sass). This instructs the browser to animate a smooth scroll when the user clicks on a link that navigates to a different part of the page.

    html {
    scroll-behavior: smooth;
    }

    With this simple CSS rule in place, clicking on any link in the TOC now results in a smooth scroll to the related content section.

    Active Link Highlighting

    The next part of enhancing your TOC involves dynamically highlighting the active link. As the user scrolls through different sections on the page, the corresponding link in the TOC should highlight, indicating the current section that is in view.

    This feature requires the use of JavaScript, more specifically, the React Hooks `useState` and `useEffect` in our TOC component.

    The `useState` Hook lets us add React state to our component, and the `useEffect` Hook allows us to perform side effects, such as DOM manipulation, in our component. We'll use these hooks to track which TOC link should be active based on the user's scroll position.

    Firstly, let's add a state to store the active heading ID using `useState`.

    const TableOfContents = ({ headings }) => {
    const [activeId, setActiveId] = useState();
    
    // ...
    }

    In the above code, `activeId` is the state variable that stores the current active heading ID, and `setActiveId` is the function to update this state.

    Next, let's use the `useEffect` Hook to observe the visibility of our headings. When a heading becomes visible in the viewport, the function will update `activeId`.

    useEffect(() => {
    const observer = new IntersectionObserver(
    entries => {
    entries.forEach(entry => {
    if (entry.isIntersecting) {
    setActiveId(entry.target.id);
    }
    });
    },
    { rootMargin: '0px 0px -90% 0px' }
    );
    
    headings.forEach(heading => {
    observer.observe(document.getElementById(heading.id));
    });
    
    return () => {
    headings.forEach(heading => {
    observer.unobserve(document.getElementById(heading.id));
    });
    };
    }, [headings]);

    In the above code, we're creating a new `IntersectionObserver` to observe changes in the intersection of each heading with the viewport. When a heading becomes highly visible in the viewport, `IntersectionObserver` fires a callback that sets the `activeId` state to the ID of the current heading.

    Now, in our TOC component's JSX, we simply add a conditional classname to highlight the active link.

    const TableOfContents = ({ headings }) => {
    const [activeId, setActiveId] = useState();
    
    useEffect(() => {
    // Observer code here
    }, [headings]);
    
    return (
    <nav>
    <ul>
    {headings.map(heading => (
    <li key={heading.id}>
    <a
    href={`#${heading.id}`}
    className={activeId === heading.id ? 'active' : ''}
    >
    {heading.title}
    </a>
    </li>
    ))}
    </ul>
    </nav>
    );
    };

    With the addition of these functionalities, your TOC is now more dynamic and engaging. The smooth scrolling and active link highlighting features significantly enhance the user's experience, enabling them to comprehend your content's structure more efficiently and navigate it effortlessly.

    Remember, while these additions aid in enhancing the user experience, it's equally crucial to consider factors such as accessibility and semantic HTML when designing a TOC for optimal results.

    Dealing with URL Encoding Issues

    During the implementation of the Table of Contents, one common issue that often arises is the discrepancy caused by the URL encoding of spaces in the headings. This can result in a mismatch between the encoded URL in the Table of Contents links and the actual IDs assigned to the sections, leading to the unsuccessful navigation to the respective sections. Thankfully, this issue can be addressed using a technique involving the proper extraction of markdown headings and the consistent usage of slugs.

    When creating a Table of Contents, we generate URLs for each heading by converting the heading text into a URL-encoded string, often referred to as a 'slug'. This slug then becomes the `href` for the respective Table of Contents link.

    However, spaces in the heading text are encoded as `%20` in URLs. For example, a heading with the text `Section 1` would be converted into a slug `Section%201`. The issue arises when assigning IDs to the sections in the content. If we don't apply the same URL encoding to these IDs, a heading with the text `Section 1` might be assigned an ID `Section-1`, creating a mismatch with the slug in the Table of Contents link.

    To resolve this issue, we need to ensure that the same slug generation logic is applied while assigning IDs to the sections and creating the Table of Contents links.

    One way to achieve this is by creating a utility function that takes a string of text as input and returns a URL-encoded slug. While there are many ways to implement this function, here's a simple example:

    function generateSlug(text) {
      return text.toLowerCase().replace(/\s+/g, '-');
    }
    

    Now, you can use this function to generate both the section IDs and the Table of Contents links. Here's how you might assign IDs to the headings in your JSX content:

    export default function Article() {
      return (
        <div>
          <h1 id={generateSlug("Title of the Article")}>Title of the Article</h1>
          <section id={generateSlug("Section 1")}>
            <h2>Section 1</h2>
            <p>Your content for Section 1.</p>
          </section>
          <section id={generateSlug("Section 2")}>
            <h2>Section 2</h2>
            <p>Your content for Section 2.</p>
          </section>
        </div>
      );
    }
    

    And here's how you'd adapt the TableOfContents component to use the generateSlug function:

    const TableOfContents = ({ headings }) => {
      return (
        <nav>
          <ul>
            {headings.map((heading) => (
              <li key={generateSlug(heading.title)}>
                <a href={`#${generateSlug(heading.title)}`}>
                  {heading.title}
                </a>
              </li>
            ))}
          </ul>
        </nav>
      );
    };
    

    By applying the same slug generation logic to both your heading IDs and Table of Contents links, you can ensure that the navigation functionality of your Table of Contents works flawlessly, regardless of the text of your headings or the presence of spaces or special characters.

    Implementing SEO Features and Deploying Your Project

    An important aspect of developing your Next.js 13 site, after creating your Table of Contents (TOC), is ensuring your site is discoverable by search engines. This is where Search Engine Optimization (SEO) features come in. SEO is integral to enhancing the visibility of your website on search engine result pages and improving the user experience. With Next.js 13 and 14, you have an array of SEO-optimizing capabilities that you can implement.

    Read our full Next.js SEO Guide

    Read our deatiled guide into optimizing your Next.js 13 and 14 website for organic search.

    One of these features is the Metadata API. Metadata, in the context of web pages, refers to information about your web page that doesn't appear on the page itself, but in the page's code. This information helps search engines understand the content on the page and how to index it.

    Using the Metadata API in Next.js 13 / 14, you can manage metadata for your web pages efficiently. This involves setting meta tags in the `head` of your document, such as the title, description, and keywords for your page. Here's an example of how you can implement this:

    import Head from 'next/head';
    
    export default function HomePage() {
    return (
    <div>
    <Head>
    <title>Your Page Title</title>
    <meta name="description" content="Description of your page" />
    <meta name="keywords" content="relevant, keywords, for, your, page" />
    {/* Other metadata */}
    </Head>
    {/* Page content */}
    </div>
    );
    }

    Another SEO-related feature you can implement in your Next.js project is sitemap support. A sitemap is, essentially, a list of all the pages on your website that a search engine should be aware of. It's a tool that helps search engine crawlers find all the content on your site. There are several npm packages available that can generate sitemaps for your Next.js application (see more in our full Next.js SEO guide).

    After implementing these SEO features, the final step to getting your project out there is deployment. While Next.js applications can be deployed to any hosting provider that supports Node.js, it is optimized for deployment on Vercel, a serverless platform for static and hybrid applications built by the creators of Next.js. With Vercel, each deploy automatically becomes a unique URL, and every push to the main branch of your selected repository is deployed and pushed to production.

    # Install Vercel
    npm install -g vercel
    
    # Deploy your application
    vercel

    Following the installation of the Vercel command-line interface (CLI) and deploying your application, your application should be live and accessible on a unique Vercel URL.

    By leveraging the features of Next.js 13 / 14, from its dynamic routing, to rendering using JSX, to enhancing SEO with metadata, and final deployment, you can craft an interactive, user-friendly, and easily discoverable website. Coupled with an interactive TOC, your Next.js 13 / 14website can stand apart by offering excellent navigation and discoverability to users.

    Summary

    Embracing the state-of-the-art features of Next.js 13 / 14, we can significantly enhance the user experience on our websites. By implementing dynamic routing, utilizing JSX for rendering, building an interactive Table of Contents, and leveraging SEO optimization capabilities, we can create websites that are not only user-friendly and accessible but also easily discoverable by search engines.

    Richard Lawrence

    About Richard Lawrence

    Constantly looking to evolve and learn, I have have studied in areas as diverse as Philosophy, International Marketing and Data Science. I've been within the tech space, including SEO and development, since 2008.
    Copyright © 2024 evolvingDev. All rights reserved.