Step-by-Step Tutorial: How to Easily Build a Table of Contents in Strapi

Easily build table of contents with strapi
Mar 8 2023 · 5 min read

Introduction

Strapi is an open-source headless CMS (Content Management System) that allows developers to build, deploy, and manage content-rich websites and applications.

A Table of Contents (TOC) is a list of headings that provides an overview of the content of a document. TOCs are typically found at the beginning of longer documents, such as reports, ebooks, and articles, and can help readers quickly navigate to the specific section they are interested in.

Including a TOC in your content can improve user experience and make your content more accessible and readable.

In this context, building a Table of Contents in Strapi means creating a list of headings for your content, which can then be displayed user-friendly on your website or application.


Build good habits to discover your hidden talents and use them when needed with Justly. Try it today!

What will we implement?

Table of contents for your content, so called index

Recently, we had a requirement of creating a custom table of contents field for the website content. We have gone through a bulk of references for the solution but haven’t found a satisfactory solution. Lastly, we have decided to implement it by ourselves.

I would like to share the same with you guys with the aim that it might save a bit of your time.

I used CKEditor as the rich text editor. Feel free to use your favorite one.
On the strapi console, by clicking on save button, you will have an automatically generated TOC field.

You can find a working example of this at canopas-blog.

Lifecycle hooks in strapi

Strapi provides a lifecycle hook system that allows developers to execute custom code during different stages of the request-response cycle of the collection.

Refer to strapi docs for more information on lifecycle hooks.

I will use beforeUpdate hook for this blog. You can use any of them according to the requirements.

Note: The collection is a model which contains content, toc, etc fields. For example user or article can be considered as collection.

To add a lifecycle hook for the collection, perform the following steps:

  • create a lifecycle.js file in src/api/<collection-name>/content-types/<collection-name>
  • Add the following code to it.
module.exports = {
  beforeUpdate(event) {
    
  },
};

Add id attribute in headers of content

On clicking on lists, we need to redirect to that content. For that, we have to add id field in the header tags .

We can do this in the following steps.

1. Get model data

We have all fields of the model in each hook. We can get it using the event.params as below,

const result = event.params.data;
if (result) {}

2. Get header fields

Our content is in HTML format. We can access dom using JSDOM in nodeJs.

Install jsdom and init it like the below,

const jsdom = require("jsdom");
const { JSDOM } = jsdom;

Then using querySelectorAll , we can get all h1, ……, h6 fields.

// field name is *content*
const dom = new JSDOM(result.content);
const doc = dom.window.document;

// find all header tags in the document
const headers = doc.querySelectorAll("h1, h2");

3. Prepare and set ids to the header

Now, it's time to set ids in the headers. I have converted the header text to dashed text. You can do as per requirements.

headers.forEach((header, index) => {
  let text = header.textContent.toLowerCase().replace(/\s/g, '-')
  header.setAttribute("id", `${text}`);
 });

 event.params.data.content = dom.serialize()

Now, the content will have all the headers with the id attribute like below,

<html>
  <head></head>
  <body>
      <h1 id="strapi">Strapi</h1>
      <h2 id="introduction">Introduction</h2>
      <h2 id="table-of-contents">Table of contents</h2>
  </body>
</html>

You can access these ids in your front end by calling strapi APIs.

Create a table of contents

Let’s create a table of contents list.

Add the following code block to the file,

 // table of contents list
 let toc = `<ul style="list-style-type: disc">`

 // max header node h6 
 let lastNode = 6

 // if list is indented
 let indented = false

 headers.forEach((header, index) => {
   // get current header node number i.e 1,2,...,6
   let currentNode = parseInt(header.nodeName.at(-1))
   let text = header.textContent.toLowerCase().replace(/\s/g, '-')

   /**
   if lastNode was h2 and currentNode is h3 then add indentation to the list
      or continue identation if both nodes are same and list was indented
   else continue adding elements in list without identation
   **/
   if (currentNode > lastNode || (currentNode == lastNode && childNode)) {
     toc = childNode ? toc : toc + "<ul style='text-indent:20px'>"
     toc += `<li><a href="#${text}">${header.textContent}</a></li>`
     indented = true
   } else {
     toc = childNode ? toc + "</ul>" : toc
     toc += `<li><a href="#${text}">${header.textContent}</a></li>`
     indented = false
   }

   lastNode = currentNode
 });

 // assign generated toc to the field
 event.params.data.toc = toc += "</ul>"

Pretty easy? I have added comments in the code to make it easy to understand. Now you will have a complete list of headers in the TOC field.

The full code of the lifecycle.js file should look like the below,

const jsdom = require("jsdom");
const { JSDOM } = jsdom;

module.exports = {
  beforeUpdate(event) {
    const result = event.params.data;
    if (result) {
      const dom = new JSDOM(result.content);
      const doc = dom.window.document;

      // find all header tags in the document
      const headers = doc.querySelectorAll("h1, h2");

      // table of contents list
      let toc = `<ul style="list-style-type: disc">`

      // max header node h6 
      let lastNode = 6
    
      // if list is indented
      let indented = false

      headers.forEach((header, index) => {
        // get current header node number i.e 1,2,...,6
        let currentNode = parseInt(header.nodeName.at(-1))
        let text = header.textContent.toLowerCase().replace(/\s/g, '-')

        /**
        if lastNode was h2 and currentNode is h3 then add indentation to the list
          or continue identation if both nodes are same and list was indented
        else continue adding elements in list without identation
        **/
        if (currentNode > lastNode || (currentNode == lastNode && childNode)) {
          toc = childNode ? toc : toc + "<ul style='text-indent:20px'>"
          toc += `<li><a href="#${text}">${header.textContent}</a></li>`
          childNode = true
        } else {
          toc = childNode ? toc + "</ul>" : toc
          toc += `<li><a href="#${text}">${header.textContent}</a></li>`
          childNode = false
        }

        lastNode = currentNode
        header.setAttribute("id", `${text}`);
      });

      // update content with updated headers
      event.params.data.content = dom.serialize()
   
      // assign generated toc to the field
      event.params.data.toc = toc += "</ul>"
    };
  },
};

Conclusion

Table of contents is a must-have field when considering a content-based website. It can help you to give a better user experience.

Using lifecycles hooks in strapi, we can customize fields at the time of creating or updating data.

We’re Grateful to have you with us on this journey!

Suggestions and feedback are more than welcome! 

Please reach us at Canopas Twitter handle @canopas_eng with your content or feedback. Your input enriches our content and fuels our motivation to create more valuable and informative articles for you.

That’s it for today. Keep exploring for the best.


sumita-k image
Sumita Kevat
Sumita is an experienced software developer with 5+ years in web development. Proficient in front-end and back-end technologies for creating scalable and efficient web applications. Passionate about staying current with emerging technologies to deliver.


sumita-k image
Sumita Kevat
Sumita is an experienced software developer with 5+ years in web development. Proficient in front-end and back-end technologies for creating scalable and efficient web applications. Passionate about staying current with emerging technologies to deliver.


Talk to an expert
get intouch
Our team is happy to answer your questions. Fill out the form and we’ll get back to you as soon as possible
footer
Subscribe Here!
Follow us on
2024 Canopas Software LLP. All rights reserved.