I sometimes have the need to create a table of contents for a blog post, especially if it’s a long one (step by step guides?, post pillars?).

You may think ‘well that is not complex as long as you use an extra rich text or repeatable text field to generate the table, right?’

And you are right, but that often creates some friction for the marketers, as they will need to replicate each title in a different module and then create the anchor links. For future updates that can get messy (they may forget to add the new title on the table, or change the title and forget to update the table of content, etc). This would fit in the category of technical debt. While they may not be exactly time-consuming tasks for devs, they will be for the rest of your team/client. It will also create extra friction for future template updates.

Well, today we are going to solve this issue once and for all. We are going to automagically create your table of content for the blog posts.

Let's get our hands dirty.

 

Snippet

{## Table of content ##}
{% macro table_of_contents(text) %} {% if text is string_containing "</h2>" %}
{# tried with regex, but unfinished ignore the next line. #} {#% set titles2 = text.regex_replace(">([^<>]*\S)<\/h2>") %#}
{% set titles = [] %}
{# split on </h2> end tag #}
{% for t in text|split('</h2>') %}
{# ignore if the current string in the loop isn't a h2 title #}
{% if "<h2" in t %}
{#
ignore anything that comes before the h2 tag:
previous text<h2 class="custom-class">Title tag
#}
{% set title = (t|split("<h2"))[1] %}

{#
1. Remove any striptags (maybe <strong> or <span> contained within the h2.
2. Replace > for &gt; (because |striptags gave me some inconsistent results, converting sometimes the symbol).
3. Grab the second result of the title text split by &gt;
Text: <h2 class="custom-class&gt;Title text
Result: Title text
#}
{% set title = title|striptags|replace(">","&gt;")|split("&gt;") %}
{% set title = title[1:] if title|length > 1 else title[0] %}

{#
1. Save the title if found (not empty h2)
2. Remove special characters (to generate the anchor link).
Keep in mind that depending on the languages that you may expect here,
you will need to finetune to remove or match the conversion of the character
for example in spanish ü can be converted to u, while in other languages
it would make more sense to ue.
I tried to provide some common symbols and letter that may been found for me and my clients.
Feel free to tweak it for you (and share it back!).
#} {% if title %} {% do titles.append({ "name": title, "id": title|lower|striptags |replace("%20","-")|replace(" ", "-")|replace("&nbsp;","-")
|replace('"', "")|replace("'", "") |replace(".","")|replace(",","")|replace(":", "_") |replace("„", "")|replace("“","")|replace("’","") |replace("!","")|replace("¡","")|replace("?","")|replace("¿","")
|replace("&","")|replace("*","")
|replace("/","")|replace("\\","")
|replace("@","")
|replace("=","")

|replace("Á","A")|replace("É","E")|replace("Í","I")|replace("Ó","O")|replace("Ú","u")
|replace("á","a")|replace("é","e")|replace("í","i")|replace("ó","o")|replace("ú","u")
|replace("À","A")|replace("È","E")|replace("Ì","I")|replace("Ò","O")|replace("Ù","U")
|replace("à","a")|replace("è","e")|replace("ì","i")|replace("ò","o")|replace("ù","u") |replace("ü","u")

|replace("ß","ss")|replace("æ","ae")|replace("ø","oe") |replace("å","aa") |replace("ä","ae")|replace("ë","ee")|replace("ï","ie")|replace("ö","oe") }) %} {% endif %} {% endif %} {% endfor %}

{# Output your HTML table of content #}
<nav class="table-of-contents">
<ol>
{% for t in titles %}
{{ '<li><a class="table-contents-link" href="#%s">%s</a></li>'|format(t.id, t.name) if t.name }}
{% endfor %}
</ol>
</nav>
{% endif %}
{% endmacro %}

I recommend saving it in your macros file (the best file organization setup is a topic for another time), so you can call your macro where you want to display the table of content.

Use of the macro:

If you have your macros as recommended (loading all your macros from a file inside the macros variable):
{{ macros.table_of_contents(content.post_body) }}

If you have your macro self-contained in the root template or called to root without any extra middle variable:

{{ table_of_contents(content.post_body) }}

Remember to style your fresh table of content according to your brand!

Now it's time to edit where we print the post_body, so we update the h2 tags in accordance with the new ID attributes:

{# Create anchor title #}
{%- set final_post = [content.post_body] %}
{%- for title in post_titles %}
{%- do final_post.append(final_post[-1]|replace(">" ~ title.name, ' id="%s">%s'|format(title.id, title.name))) %}
{%- endfor %}
{{- final_post[-1] }}