JavaScript Development Space

Build a Modern Table of Contents in Gatsby Without Extra Plugins

Add to your RSS feed2 September 202415 min read
Build a Modern Table of Contents in Gatsby Without Extra Plugins

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:

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

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

  3. 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 new
js
1 What would you like to call your site?
2 √ · gatsby-toc
3 What would you like to name the folder where your site will be created?
4 Gatsby/ gatsby-toc
5 Will you be using JavaScript or TypeScript?
6 · TypeScript
7 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 CSS
11 Would you like to install additional features with other plugins?
12 · Add responsive images
13 · Generate a manifest file
cd gatsby-toc

Edit tsconfig.json file

json
1 {
2 "compilerOptions": {
3 // ...
4 "baseUrl": ".",
5 "paths": {
6 "@/*": ["./src/*"]
7 }
8 // ...
9 }
10 }

Create gatsby-node.ts file

ts
1 import * as path from 'path';
2
3 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 init
bash
1 Would you like to use TypeScript (recommended)? no / yes
2 Which style would you like to use? › Default
3 Which color would you like to use as base color? › Slate
4 Where is your global CSS file? › › ./src/styles/globals.css
5 Do you want to use CSS variables for colors? › no / yes
6 Where is your tailwind.config.js located? › tailwind.config.js
7 Configure the import alias for components: › @/components
8 Configure the import alias for utils: › @/lib/utils
9 Are you using React Server Components? › no

Add this code to your tailwind.config.js file:

js
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:

js
1 import type { GatsbyConfig } from 'gatsby';
2
3 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 };
62
63 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:

mdx
1 ---
2 title: Advantages and Disadvantages of Gatsby
3 slug: gatsby-pros-cons
4 permalink: gatsby-pros-cons
5 date: 2024-09-02
6 author: gatsby john
7 category: gatsby
8 type: post
9 tags: ['gatsby', 'react']
10 desc:
11 Discover the best practices for implementing the Singleton Design Pattern in JavaScript and
12 TypeScript. This guide covers step-by-step instructions to ensure efficient and scalable code.
13 ---
14
15 **Gatsby** is a popular static site generator built on React and GraphQL, offering developers a
16 powerful framework for building fast, modern websites. While Gatsby has a lot to offer, it also
17 comes with its own set of challenges. In this article, we’ll explore the key advantages and
18 disadvantages of using Gatsby, helping you decide whether it’s the right tool for your next web
19 project.
20
21 ## Advantages of Gatsby
22
23 ### 1. Blazing Fast Performance
24
25 One of Gatsby's biggest selling points is its incredible speed. Gatsby pre-renders pages into static
26 HTML at build time, resulting in fast load times. This is particularly beneficial for user
27 experience and SEO, as fast-loading sites are favored by search engines and users alike.
28
29 - **Optimized Asset Handling**: Gatsby automatically optimizes images, JavaScript, and CSS, ensuring
30 that your site loads as quickly as possible. It lazy-loads images and uses techniques like code
31 splitting and prefetching to further enhance performance.
32 - **Progressive Web App (PWA) Features**: Gatsby comes with built-in PWA features, including service
33 workers and offline support, which help deliver a seamless user experience even in poor network
34 conditions.
35
36 ### 2. SEO-Friendly
37
38 Gatsby is designed with SEO in mind. Since Gatsby generates static HTML pages at build time, these
39 pages are fully crawlable by search engines, improving your site’s visibility. Additionally, Gatsby
40 makes it easy to manage metadata, URLs, and other SEO-related elements.
41
42 - **Structured Data**: With Gatsby, you can easily implement structured data to enhance how your
43 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 SEO
46
47 ### 3. Rich Plugin Ecosystem
48
49 Gatsby has a vast ecosystem of plugins that allow you to extend its functionality with minimal
50 effort. Whether you need to integrate with a CMS, optimize images, or add analytics, there’s likely
51 a Gatsby plugin available to help.
52
53 - **Content Integration**: Gatsby integrates seamlessly with a variety of CMS platforms, including
54 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, including
56 REST APIs, GraphQL endpoints, and markdown files, and combine them into a single GraphQL schema.
57
58 ### 4. Strong Community Support
59
60 Gatsby has a large and active community, which means plenty of resources are available to help you
61 get started or troubleshoot issues. The Gatsby documentation is comprehensive, and there are
62 numerous tutorials, blog posts, and courses available online.
63
64 - **Open Source**: Gatsby is an open-source project, so you benefit from the contributions of
65 developers around the world who continuously improve the platform.
66
67 ### 5. Modern Development Experience
68
69 Gatsby leverages modern web development tools and practices, making it a great choice for developers
70 who enjoy working with cutting-edge technologies.
71
72 - **React**: Since Gatsby is built on React, you can use the latest features of React, such as hooks
73 and context, to build dynamic, interactive user interfaces.
74 - **GraphQL**: Gatsby uses GraphQL to manage data, providing a flexible and powerful way to query
75 content from various sources.
76
77 ## Disadvantages of Gatsby
78
79 ### 1. Build Times Can Be Long
80
81 One of the downsides of Gatsby is that as your site grows in size and complexity, build times can
82 become significantly longer. This can be especially problematic for large e-commerce sites or
83 content-heavy blogs with thousands of pages.
84
85 - **Incremental Builds**: While Gatsby Cloud offers incremental builds to address this issue, this
86 feature is not available in the open-source version of Gatsby.
87
88 ### 2. Steep Learning Curve
89
90 Gatsby can be challenging to learn for developers who are not familiar with React or GraphQL. The
91 platform requires a good understanding of modern JavaScript, and getting up to speed with its
92 concepts and best practices can take some time.
93
94 - **Complexity**: While Gatsby’s plugin system is powerful, it can also introduce complexity,
95 especially when integrating multiple plugins or troubleshooting plugin-related issues.
96
97 ### 3. Dependency on Third-Party Services
98
99 Gatsby’s performance and features often rely on third-party services, such as content management
100 systems (CMS) and hosting platforms. This dependency can lead to challenges, such as dealing with
101 API rate limits, managing external service outages, or facing unexpected costs.
102
103 - **Hosting and Deployment**: While you can deploy a Gatsby site on any static hosting service, some
104 advanced features (like incremental builds) require specific platforms like Gatsby Cloud, which
105 may introduce additional costs.
106
107 ### 4. Content Management Limitations
108
109 While Gatsby can integrate with various CMS platforms, the experience is not as seamless as using a
110 traditional CMS. Content editors may find it difficult to preview changes or may require a more
111 technical setup to manage content effectively.
112
113 **Previewing Content**: Gatsby does not natively support live previews of content edits, making it
114 challenging for non-technical users to see changes in real-time without additional configuration.
115
116 ### 5. Not Ideal for Every Project
117
118 Gatsby is best suited for sites that don’t require frequent updates or dynamic content. If your site
119 involves a lot of user-generated content, real-time updates, or server-side processing, a
120 traditional CMS or a server-rendered framework like Next.js might be more appropriate.
121
122 - **Dynamic Content**: Handling dynamic content or features like user authentication and real-time
123 updates can be complex in Gatsby, requiring workarounds or third-party services.
124
125 ## Conclusion
126
127 Gatsby offers numerous advantages, particularly for developers building fast, SEO-friendly static
128 websites. Its modern development experience, extensive plugin ecosystem, and strong community
129 support make it a compelling choice for many projects. However, it’s essential to be aware of its
130 limitations, including potential build time issues, a steep learning curve, and challenges with
131 dynamic content. By weighing these advantages and disadvantages, you can determine whether Gatsby is
132 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:

ts
1 import { CreateNodeArgs, CreatePagesArgs, CreateWebpackConfigArgs, GatsbyNode } from 'gatsby';
2 import * as path from 'path';
3
4 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 };
16
17 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`);
24
25 const result = await graphql<Queries.GatsbyNodeCreatePagesQuery>(`
26 query GatsbyNodeCreatePages {
27 allMdx {
28 nodes {
29 frontmatter {
30 slug
31 }
32 internal {
33 contentFilePath
34 }
35 }
36 }
37 }
38 `);
39
40 if (result.errors) {
41 reporter.panicOnBuild('Error loading MDX result', result.errors);
42 }
43
44 const posts = result.data.allMdx.nodes;
45 console.log('posts', posts);
46
47 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.

tsx
1 import { graphql, useStaticQuery } from 'gatsby';
2
3 type SiteMetadata = {
4 title: string;
5 description: string;
6 twitterUsername: string;
7 image: string;
8 siteUrl: string;
9 };
10
11 type graphqlResult = {
12 site: {
13 siteMetadata: SiteMetadata;
14 };
15 };
16
17 export const useSiteMetadata = (): SiteMetadata => {
18 const data: graphqlResult = useStaticQuery(graphql`
19 {
20 site {
21 siteMetadata {
22 title
23 description
24 twitterUsername
25 image
26 siteUrl
27 }
28 }
29 }
30 `);
31
32 return data.site.siteMetadata;
33 };

2. Create SEO component

Create file seo.tsx

ts
1 import * as React from 'react';
2 import { useLocation } from '@reach/router';
3
4 import { useSiteMetadata } from '../hooks/use-site-metadata';
5
6 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 }
17
18 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();
37
38 // By default, we will construct the canonical path ourselves, but this can
39 // be overwritten via the component properties
40 const { pathname } = useLocation();
41 const defaultCanonicalPath = `${siteUrl}/${pathname}`;
42 const canonicalUrl = propCanonicalPath || defaultCanonicalPath;
43
44 const siteName = siteTitle || 'JavaScript Development Blog';
45 const title = propTitle;
46 const description = propDescription || siteDescription || '';
47 const lang = propLang || 'en_US';
48
49 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 };
81
82 export default SEO;

3. Add slugify function

Add a function to /lib/utils.ts file:

ts
1 export const slugify = (text: string): string => {
2 return text
3 .toString()
4 .toLowerCase()
5 .replace(/\s+/g, '-') // Replace spaces with -
6 .replace(/[^\w-]+/g, '') // Remove all non-word chars
7 .replace(/--+/g, '-') // Replace multiple - with single -
8 .replace(/^-+/, '') // Trim - from start of text
9 .replace(/-+$/, ''); // Trim - from end of text
10 };

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 card

2. Create a TOC Component

Create a new file at src/components/tos/table-of-contents.tsx:

ts
1 import * as React from 'react';
2 import { Card, CardContent, CardTitle } from '../ui/card';
3
4 type TableOfContentsItem = {
5 url: string;
6 title: string;
7 items?: TableOfContentsItem[];
8 };
9
10 type TableOfContentsProps = {
11 items: TableOfContentsItem[];
12 };
13
14 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 Contents
19 </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 <a
27 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 <a
40 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 <svg
44 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 <path
50 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 <a
66 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 <svg
70 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 <path
76 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:

ts
1 import * as React from 'react';
2 import { graphql, HeadFC, PageProps } from 'gatsby';
3 import { MDXProvider } from '@mdx-js/react';
4
5 import TableOfContents from '../components/tos/table-of-contents';
6 import SEO from '../components/seo';
7 import { slugify } from '../lib/utils';
8
9 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 };
35
36 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 excerpt
59 frontmatter {
60 title
61 }
62 tableOfContents
63 }
64 }
65 `;
66
67 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 };
81
82 export default PostTemplate;

Now run:

npm run develop

Postman Table of Contents in Gatsby

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

JavaScript Development Space

© 2024 JavaScript Development Space - Master JS and NodeJS. All rights reserved.