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!
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.
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:
src/api/<collection-name>/content-types/<collection-name>
module.exports = {
beforeUpdate(event) {
},
};
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.
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) {}
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");
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.
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>"
};
},
};
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.