Build a Modern Table of Contents in Gatsby Without Extra Plugins
Add to your RSS feed2 September 202415 min readTable of Contents
Creating a table of contents (TOC) is a common feature in content-heavy websites like blogs, documentation, and eBooks. In a Gatsby project, implementing a modern TOC component that is efficient, user-friendly, and doesn’t rely on DOM fetching or extra plugins can greatly enhance your site’s usability. This article will guide you through building a TOC in Gatsby that dynamically generates links to headings within a page, all while maintaining optimal performance.
Why Avoid DOM Fetching and Extra Plugins?
Before diving into the implementation, let's briefly discuss why it's beneficial to avoid DOM fetching and additional plugins when creating a TOC:
-
Performance: Direct DOM manipulation can be costly in terms of performance, especially in large pages with many elements. It’s better to work with the data you already have at build time.
-
SEO: Relying on JavaScript to fetch and manipulate the DOM can delay content rendering, which might negatively impact SEO. A server-rendered TOC is available immediately to search engines.
-
Simplicity: By avoiding extra plugins, you keep your project dependencies minimal, reducing the potential for conflicts and making the code easier to maintain.
Overview of the Approach
Our approach will leverage Gatsby’s powerful GraphQL data layer to fetch headings from markdown or MDX files at build time. We will then use this data to generate the TOC without the need to parse the DOM dynamically. The end result is a static, SEO-friendly TOC that can be styled and customized to match your site’s design.
Setting Up the Gatsby Project
Prerequisites
- Gatsby: Make sure you have a Gatsby project set up. If not, you can quickly create one using the Gatsby CLI.
- Markdown or MDX Content: This guide assumes that your content is written in Markdown or MDX, which is typical for blogs or documentation sites built with Gatsby.
If you don't already have a Gatsby project, you can create one by running the following command in your terminal:
gatsby new1 What would you like to call your site?2 √ · gatsby-toc3 What would you like to name the folder where your site will be created?4 √ Gatsby/ gatsby-toc5 √ Will you be using JavaScript or TypeScript?6 · TypeScript7 √ Will you be using a CMS?8 · No (or I\'ll add it later)9 √ Would you like to install a styling system?10 · Tailwind CSS11 √ Would you like to install additional features with other plugins?12 · Add responsive images13 · Generate a manifest file
Edit tsconfig.json file
1 {2 "compilerOptions": {3 // ...4 "baseUrl": ".",5 "paths": {6 "@/*": ["./src/*"]7 }8 // ...9 }10 }
Create gatsby-node.ts file
1 import * as path from 'path';23 export const onCreateWebpackConfig = ({ actions }) => {4 actions.setWebpackConfig({5 resolve: {6 alias: {7 '@/components': path.resolve(__dirname, 'src/components'),8 '@/lib/utils': path.resolve(__dirname, 'src/lib/utils'),9 },10 },11 });12 };
Run the CLI
npx shadcn@latest init1 Would you like to use TypeScript (recommended)? no / yes2 Which style would you like to use? › Default3 Which color would you like to use as base color? › Slate4 Where is your global CSS file? › › ./src/styles/globals.css5 Do you want to use CSS variables for colors? › no / yes6 Where is your tailwind.config.js located? › tailwind.config.js7 Configure the import alias for components: › @/components8 Configure the import alias for utils: › @/lib/utils9 Are you using React Server Components? › no
Add this code to your tailwind.config.js file:
1 content: [2 `./src/pages/**/*.{js,jsx,ts,tsx}`,3 `./src/components/**/*.{js,jsx,ts,tsx}`,4 `./src/templates/**/*.{js,jsx,ts,tsx}`,5 ],
Next, ensure you have the necessary plugins for working with Markdown or MDX:
npm install gatsby-source-filesystem gatsby-plugin-mdx rehype-autolink-headings
In your gatsby-config.ts, configure the plugins to source content from your markdown or MDX files:
1 import type { GatsbyConfig } from 'gatsby';23 const config: GatsbyConfig = {4 siteMetadata: {5 title: `Table of Contents Blog`,6 siteUrl: `https://www.yourdomain.tld`,7 twitterUsername: '@some',8 image: './src/images/icon.png',9 description: `Table of Contents Blog. `,10 },11 graphqlTypegen: true,12 plugins: [13 'gatsby-plugin-postcss',14 'gatsby-plugin-image',15 {16 resolve: 'gatsby-plugin-manifest',17 options: {18 icon: 'src/images/icon.png',19 },20 },21 'gatsby-plugin-sharp',22 'gatsby-transformer-sharp',23 {24 resolve: 'gatsby-source-filesystem',25 options: {26 name: 'images',27 path: './src/images/',28 },29 __key: 'images',30 },31 {32 resolve: 'gatsby-source-filesystem',33 options: {34 name: 'pages',35 path: './src/pages/',36 },37 __key: 'pages',38 },39 {40 resolve: `gatsby-source-filesystem`,41 options: {42 name: 'content',43 path: `./content/`,44 },45 },46 {47 resolve: 'gatsby-plugin-mdx',48 options: {49 extensions: ['.md', '.mdx'],50 mdxOptions: {51 rehypePlugins: [52 {53 resolve: `rehype-autolink-headings`,54 options: { behavior: `wrap` },55 },56 ],57 },58 },59 },60 ],61 };6263 export default config;
Prepare some MDX content
Create a content directory at the root of your project and add some markdown or MDX files to test with.
Create a file gatsby-pros-cons/post.mdx inside the content directory. Fill the file:
1 ---2 title: Advantages and Disadvantages of Gatsby3 slug: gatsby-pros-cons4 permalink: gatsby-pros-cons5 date: 2024-09-026 author: gatsby john7 category: gatsby8 type: post9 tags: ['gatsby', 'react']10 desc:11 Discover the best practices for implementing the Singleton Design Pattern in JavaScript and12 TypeScript. This guide covers step-by-step instructions to ensure efficient and scalable code.13 ---1415 **Gatsby** is a popular static site generator built on React and GraphQL, offering developers a16 powerful framework for building fast, modern websites. While Gatsby has a lot to offer, it also17 comes with its own set of challenges. In this article, we’ll explore the key advantages and18 disadvantages of using Gatsby, helping you decide whether it’s the right tool for your next web19 project.2021 ## Advantages of Gatsby2223 ### 1. Blazing Fast Performance2425 One of Gatsby's biggest selling points is its incredible speed. Gatsby pre-renders pages into static26 HTML at build time, resulting in fast load times. This is particularly beneficial for user27 experience and SEO, as fast-loading sites are favored by search engines and users alike.2829 - **Optimized Asset Handling**: Gatsby automatically optimizes images, JavaScript, and CSS, ensuring30 that your site loads as quickly as possible. It lazy-loads images and uses techniques like code31 splitting and prefetching to further enhance performance.32 - **Progressive Web App (PWA) Features**: Gatsby comes with built-in PWA features, including service33 workers and offline support, which help deliver a seamless user experience even in poor network34 conditions.3536 ### 2. SEO-Friendly3738 Gatsby is designed with SEO in mind. Since Gatsby generates static HTML pages at build time, these39 pages are fully crawlable by search engines, improving your site’s visibility. Additionally, Gatsby40 makes it easy to manage metadata, URLs, and other SEO-related elements.4142 - **Structured Data**: With Gatsby, you can easily implement structured data to enhance how your43 site appears in search engine results.44 - **Sitemaps and RSS Feeds**: Gatsby plugins make it simple to generate sitemaps and RSS feeds,45 further boosting your site's SEO4647 ### 3. Rich Plugin Ecosystem4849 Gatsby has a vast ecosystem of plugins that allow you to extend its functionality with minimal50 effort. Whether you need to integrate with a CMS, optimize images, or add analytics, there’s likely51 a Gatsby plugin available to help.5253 - **Content Integration**: Gatsby integrates seamlessly with a variety of CMS platforms, including54 WordPress, Contentful, and Sanity, allowing you to manage your content easily.55 - **Data Sources**: With Gatsby’s source plugins, you can pull data from multiple sources, including56 REST APIs, GraphQL endpoints, and markdown files, and combine them into a single GraphQL schema.5758 ### 4. Strong Community Support5960 Gatsby has a large and active community, which means plenty of resources are available to help you61 get started or troubleshoot issues. The Gatsby documentation is comprehensive, and there are62 numerous tutorials, blog posts, and courses available online.6364 - **Open Source**: Gatsby is an open-source project, so you benefit from the contributions of65 developers around the world who continuously improve the platform.6667 ### 5. Modern Development Experience6869 Gatsby leverages modern web development tools and practices, making it a great choice for developers70 who enjoy working with cutting-edge technologies.7172 - **React**: Since Gatsby is built on React, you can use the latest features of React, such as hooks73 and context, to build dynamic, interactive user interfaces.74 - **GraphQL**: Gatsby uses GraphQL to manage data, providing a flexible and powerful way to query75 content from various sources.7677 ## Disadvantages of Gatsby7879 ### 1. Build Times Can Be Long8081 One of the downsides of Gatsby is that as your site grows in size and complexity, build times can82 become significantly longer. This can be especially problematic for large e-commerce sites or83 content-heavy blogs with thousands of pages.8485 - **Incremental Builds**: While Gatsby Cloud offers incremental builds to address this issue, this86 feature is not available in the open-source version of Gatsby.8788 ### 2. Steep Learning Curve8990 Gatsby can be challenging to learn for developers who are not familiar with React or GraphQL. The91 platform requires a good understanding of modern JavaScript, and getting up to speed with its92 concepts and best practices can take some time.9394 - **Complexity**: While Gatsby’s plugin system is powerful, it can also introduce complexity,95 especially when integrating multiple plugins or troubleshooting plugin-related issues.9697 ### 3. Dependency on Third-Party Services9899 Gatsby’s performance and features often rely on third-party services, such as content management100 systems (CMS) and hosting platforms. This dependency can lead to challenges, such as dealing with101 API rate limits, managing external service outages, or facing unexpected costs.102103 - **Hosting and Deployment**: While you can deploy a Gatsby site on any static hosting service, some104 advanced features (like incremental builds) require specific platforms like Gatsby Cloud, which105 may introduce additional costs.106107 ### 4. Content Management Limitations108109 While Gatsby can integrate with various CMS platforms, the experience is not as seamless as using a110 traditional CMS. Content editors may find it difficult to preview changes or may require a more111 technical setup to manage content effectively.112113 **Previewing Content**: Gatsby does not natively support live previews of content edits, making it114 challenging for non-technical users to see changes in real-time without additional configuration.115116 ### 5. Not Ideal for Every Project117118 Gatsby is best suited for sites that don’t require frequent updates or dynamic content. If your site119 involves a lot of user-generated content, real-time updates, or server-side processing, a120 traditional CMS or a server-rendered framework like Next.js might be more appropriate.121122 - **Dynamic Content**: Handling dynamic content or features like user authentication and real-time123 updates can be complex in Gatsby, requiring workarounds or third-party services.124125 ## Conclusion126127 Gatsby offers numerous advantages, particularly for developers building fast, SEO-friendly static128 websites. Its modern development experience, extensive plugin ecosystem, and strong community129 support make it a compelling choice for many projects. However, it’s essential to be aware of its130 limitations, including potential build time issues, a steep learning curve, and challenges with131 dynamic content. By weighing these advantages and disadvantages, you can determine whether Gatsby is132 the right tool for your next web development project.
Querying for Content and Headings with GraphQL
Gatsby’s GraphQL layer allows you to query for specific data from your Markdown or MDX files. First add createPages function to your gatsby-node.ts file:
1 import { CreateNodeArgs, CreatePagesArgs, CreateWebpackConfigArgs, GatsbyNode } from 'gatsby';2 import * as path from 'path';34 export const onCreateWebpackConfig: GatsbyNode[`onCreateWebpackConfig`] = ({5 actions,6 }: CreateWebpackConfigArgs) => {7 actions.setWebpackConfig({8 resolve: {9 alias: {10 '@/components': path.resolve(__dirname, 'src/components'),11 '@/lib/utils': path.resolve(__dirname, 'src/lib/utils'),12 },13 },14 });15 };1617 export const createPages: GatsbyNode[`createPages`] = async ({18 graphql,19 actions,20 reporter,21 }: CreatePagesArgs) => {22 const { createPage } = actions;23 const postTemplate = path.resolve(`src/templates/post-template.tsx`);2425 const result = await graphql<Queries.GatsbyNodeCreatePagesQuery>(`26 query GatsbyNodeCreatePages {27 allMdx {28 nodes {29 frontmatter {30 slug31 }32 internal {33 contentFilePath34 }35 }36 }37 }38 `);3940 if (result.errors) {41 reporter.panicOnBuild('Error loading MDX result', result.errors);42 }4344 const posts = result.data.allMdx.nodes;45 console.log('posts', posts);4647 posts.forEach((node) => {48 console.log(node);49 createPage({50 path: `/${node.frontmatter.slug}`,51 component: `${postTemplate}?__contentFilePath=${node.internal.contentFilePath}`,52 context: {53 slug: node.frontmatter.slug,54 },55 });56 });57 };
1. Create use-site-metadata hook
Create file hooks/use-site-metadata.tsx inside the src directory.
1 import { graphql, useStaticQuery } from 'gatsby';23 type SiteMetadata = {4 title: string;5 description: string;6 twitterUsername: string;7 image: string;8 siteUrl: string;9 };1011 type graphqlResult = {12 site: {13 siteMetadata: SiteMetadata;14 };15 };1617 export const useSiteMetadata = (): SiteMetadata => {18 const data: graphqlResult = useStaticQuery(graphql`19 {20 site {21 siteMetadata {22 title23 description24 twitterUsername25 image26 siteUrl27 }28 }29 }30 `);3132 return data.site.siteMetadata;33 };
2. Create SEO component
Create file seo.tsx
1 import * as React from 'react';2 import { useLocation } from '@reach/router';34 import { useSiteMetadata } from '../hooks/use-site-metadata';56 interface SeoProps {7 title?: string;8 description?: string;9 lang?: string;10 image?: string;11 article?: boolean;12 canonicalUrl?: string;13 nonCanonical?: boolean;14 author?: string;15 noindex?: boolean;16 }1718 const SEO: React.FC<React.PropsWithChildren<SeoProps>> = ({19 title: propTitle,20 description: propDescription,21 lang: propLang,22 image,23 article,24 canonicalUrl: propCanonicalPath,25 nonCanonical = false,26 author: propAuthor,27 noindex = false,28 children,29 }) => {30 const {31 title: siteTitle,32 description: siteDescription,33 image: siteImage,34 siteUrl,35 twitterUsername,36 } = useSiteMetadata();3738 // By default, we will construct the canonical path ourselves, but this can39 // be overwritten via the component properties40 const { pathname } = useLocation();41 const defaultCanonicalPath = `${siteUrl}/${pathname}`;42 const canonicalUrl = propCanonicalPath || defaultCanonicalPath;4344 const siteName = siteTitle || 'JavaScript Development Blog';45 const title = propTitle;46 const description = propDescription || siteDescription || '';47 const lang = propLang || 'en_US';4849 return (50 <>51 <title>{title}</title>52 {!nonCanonical && <link rel='canonical' href={canonicalUrl} />}53 <meta name='description' content={description} />54 <meta property='og:title' content={title} />55 <meta property='og:description' content={description} />56 <meta property='og:type' content={article ? 'article' : 'website'} />57 <meta property='og:url' content={canonicalUrl} />58 <meta property='og:site_name' content={siteName} />59 <meta property='og:locale' content={lang} />60 <meta name='twitter:creator' content={twitterUsername} />61 <meta name='twitter:site' content={twitterUsername} />62 <meta name='tiwtter:url' content={canonicalUrl} />63 <meta name='twitter:title' content={title} />64 <meta name='twitter:description' content={description} />65 {image ? (66 <>67 <meta property='og:image' content={`${siteUrl}/${image}`} />68 <meta name='twitter:card' content='summary_large_image' />69 </>70 ) : (71 <>72 <meta property='og:image' content={`${siteUrl}/${siteImage}`} />73 <meta name='twitter:card' content='summary' />74 </>75 )}76 {noindex && <meta name='googlebot' content='noindex, nofollow' />}77 {children}78 </>79 );80 };8182 export default SEO;
3. Add slugify function
Add a function to /lib/utils.ts file:
1 export const slugify = (text: string): string => {2 return text3 .toString()4 .toLowerCase()5 .replace(/\s+/g, '-') // Replace spaces with -6 .replace(/[^\w-]+/g, '') // Remove all non-word chars7 .replace(/--+/g, '-') // Replace multiple - with single -8 .replace(/^-+/, '') // Trim - from start of text9 .replace(/-+$/, ''); // Trim - from end of text10 };
Creating the Table of Contents Component
Now, let’s create a Table Of Contents component that will render the TOC data as a nested list of links.
1. Install the shadcn card component
npx shadcn@latest add card2. Create a TOC Component
Create a new file at src/components/tos/table-of-contents.tsx:
1 import * as React from 'react';2 import { Card, CardContent, CardTitle } from '../ui/card';34 type TableOfContentsItem = {5 url: string;6 title: string;7 items?: TableOfContentsItem[];8 };910 type TableOfContentsProps = {11 items: TableOfContentsItem[];12 };1314 const TableOfContents = ({ items }: TableOfContentsProps) => {15 return (16 <Card className='my-10 rounded p-2 pb-0 dark:bg-gray-800'>17 <CardTitle className='text-slate-900 font-semibold mb-4 dark:text-slate-100 pt-2'>18 Table of Contents19 </CardTitle>20 <CardContent>21 <ul className='text-slate-700 pb-0'>22 {items.map((item) => {23 return (24 <div key={`${item.title}`}>25 <li className='my-4'>26 <a27 href={`${item.url}`}28 className='py-1 font-medium hover:text-slate-900 dark:text-slate-300 dark:hover:text-slate-200'29 >30 {item.title}31 </a>32 </li>33 {item.items &&34 item.items.length &&35 item.items.map((item) => {36 return (37 <span key={`${item.title}`}>38 <li className='ml-4 my-2' key={`${item.title}`}>39 <a40 href={`${item.url}`}41 className='group flex gap-2 items-start py-1 font-medium hover:text-slate-900 dark:text-slate-300 dark:hover:text-slate-200'42 >43 <svg44 width='3'45 height='24'46 viewBox='0 -9 3 24'47 className='mr-2 text-slate-400 overflow-visible group-hover:text-slate-600 dark:text-slate-500 dark:group-hover:text-slate-400'48 >49 <path50 d='M0 0L3 3L0 6'51 fill='none'52 stroke='currentColor'53 strokeWidth='1.5'54 strokeLinecap='round'55 ></path>56 </svg>57 {item.title}58 </a>59 </li>60 {item.items &&61 item.items.length &&62 item.items.map((item) => {63 return (64 <li className='ml-8 my-1' key={`${item.title}`}>65 <a66 href={`${item.url}`}67 className='group flex gap-2 items-start py-1 font-medium hover:text-slate-900 dark:text-slate-300 dark:hover:text-slate-200'68 >69 <svg70 width='3'71 height='24'72 viewBox='0 -9 3 24'73 className='mr-2 text-slate-400 overflow-visible group-hover:text-slate-600 dark:text-slate-500 dark:group-hover:text-slate-400'74 >75 <path76 d='M0 0L3 3L0 6'77 fill='none'78 stroke='currentColor'79 strokeWidth='1.5'80 strokeLinecap='round'81 ></path>82 </svg>83 {item.title}84 </a>85 </li>86 );87 })}88 </span>89 );90 })}91 </div>92 );93 })}94 </ul>95 </CardContent>96 </Card>97 );98 };99 export default TableOfContents;
Integrating the TOC into Your Pages
Create file post-template.tsx and templates directory inside the src folder:
1 import * as React from 'react';2 import { graphql, HeadFC, PageProps } from 'gatsby';3 import { MDXProvider } from '@mdx-js/react';45 import TableOfContents from '../components/tos/table-of-contents';6 import SEO from '../components/seo';7 import { slugify } from '../lib/utils';89 const MdxComponents = {10 h2: ({ children }: HeadingProps) => {11 const id = slugify(children);12 return (13 <h2 className={`my-5 font-ptSerif font-medium leading-9 tracking-wide text-3xl`} id={`${id}`}>14 {children}15 </h2>16 );17 },18 h3: ({ children }: HeadingProps) => {19 const id = slugify(children);20 return (21 <h3 className={`my-5 font-ptSerif font-medium tracking-wide text-2xl`} id={`${id}`}>22 {children}23 </h3>24 );25 },26 h4: ({ children }: HeadingProps) => {27 const id = slugify(children);28 return (29 <h4 className={`my-5 font-ptSerif tracking-wide text-xl`} id={`${id}`}>30 {children}31 </h4>32 );33 },34 };3536 const PostTemplate: React.FC<PageProps<Queries.GetSinglePostQuery>> = ({37 data: {38 mdx: {39 frontmatter: { title },40 tableOfContents,41 },42 },43 children,44 }) => {45 return (46 <div className='my-16 px-64'>47 <article className='text-lg'>48 <h1 className='tracking-wide text-4xl font-medium space-y-5 my-5'>{title}</h1>49 <TableOfContents items={tableOfContents.items} />50 <MDXProvider components={MdxComponents}> {children}</MDXProvider>51 </article>52 </div>53 );54 };55 export const query = graphql`56 query GetSinglePost($slug: String) {57 mdx(frontmatter: { slug: { eq: $slug } }) {58 excerpt59 frontmatter {60 title61 }62 tableOfContents63 }64 }65 `;6667 export const Head: HeadFC<Queries.GetSinglePost, unknown> = ({68 data: {69 mdx: {70 excerpt: description,71 frontmatter: { title },72 },73 },74 }) => {75 return (76 <>77 <SEO title={title} description={description} />78 </>79 );80 };8182 export default PostTemplate;
Now run:
npm run developGet the full code below on github
Conclusion
By following this guide, you've learned how to build a modern Table of Contents component in Gatsby without relying on DOM fetching or additional plugins. This approach leverages Gatsby's GraphQL data layer to generate a TOC at build time, ensuring your site remains fast, SEO-friendly, and easy to maintain. You can now easily customize and expand upon this TOC component to fit the unique needs of your Gatsby site.