Table of contents with next-slicezone

I have a pretty simple question: is there a way to create a slice which automatically gets all h2, h3 etc. from a page and creates a table of contents?

Hey Amos,

What I've done before is that I iterate over all of my content (RichText slice in this case), grab all elements with h2/h3 tags and extract the headings. I put this logic into a function and then put it ito a TOC.

Hey Team,

I have an example here in Nuxt.js with the HTML serializer, but it should work just the same in Next.js

For your use case of the sub navigation. Something I do is use the HTMLSerializer to automatically add id's to my H2's from my rich texts. Like I do here:

  if (type === Elements.heading2) {
    var id = element.text.replace(/\W+/g, '-').toLowerCase();
    return '<h2 id="' + id + '">' + children.join('') + '</h2>';
  }

Then I build a component that looks at the Slices on the current page, takes any H2 titles and creates a link to these. This example is in Vue, but you can easily recreate it in whatever technology:

<template>
    <section v-if="sections.length" class="table-of-content">
        <h5>On this page</h5>
        <ul>
            <li v-for="([anchor, text], index) in sections" :key="`section-${index}`">
                <a :href="`#${anchor}`">
                    {{ text }}
                </a>
            </li>
        </ul>
    </section>
</template>

<script>
export default {
    name: 'toc-slice',
    props: {
		slices: {
			type: Array,
			required: true
		}
    },
    computed: {
        sections: function () {
            const buildAnchor = (text) => text.replace(/\W+/g, '-').toLowerCase()
            return this.slices
                .map((slice) => {
                    if(!(slice.slice_type === 'title' && slice.primary.title)) return;
                    const heading = slice.primary.title.find(elem => elem.type === 'heading2');
                    if(heading) return [buildAnchor(heading.text), heading.text];
                })
                .filter(e => !!e)
        }
    }
}
</script>

Here's the full file.

Let me know if you have any questions about this.

Thanks.

Hey Kristiyan,

Where are you doing this exactly? I am just curious how to pass another slice's content (in this case the slice with the h2/h3s etc.) to another slice (the table of content slice).

Then I build a component that looks at the Slices on the current page

I am curious how you can do that with next-slicezone when a page is basically this:

const Page = (props: any) => (
  <SliceZone {...props} resolver={resolver} />
);

// Fetch content from prismic
// eslint-disable-next-line react-hooks/rules-of-hooks
export const getStaticProps = useGetStaticProps({
  client: Client(),
  apiParams({ params }: any) {
    return {
      uid: params.uid,
      fetchLinks: PAGE_LINKS,
    };
  },
});

// eslint-disable-next-line react-hooks/rules-of-hooks
export const getStaticPaths = useGetStaticPaths({
  client: Client(),
  formatPath: (prismicDocument: any) => {
    return {
      params: {
        uid: prismicDocument.uid,
      },
    };
  },
});

You do the magic to generate the table of contents in the page component, then you can use Context API around your SliceZone then extract it in your TOC slice

1 Like

Well you already have access to all the slices data so you just import the Table of contents component and pass that data to it.

Something like...

import TableOfContents from 'components/TableOfContents'

const Page = (props: any) => (
  <>
    <SliceZone {...props} resolver={resolver} />
    <TableOfContents
      slices={props.slices}
    />
  </>
);

// Fetch content from prismic
// eslint-disable-next-line react-hooks/rules-of-hooks
export const getStaticProps = useGetStaticProps({
  client: Client(),
  apiParams({ params }: any) {
    return {
      uid: params.uid,
      fetchLinks: PAGE_LINKS,
    };
  },
});

// eslint-disable-next-line react-hooks/rules-of-hooks
export const getStaticPaths = useGetStaticPaths({
  client: Client(),
  formatPath: (prismicDocument: any) => {
    return {
      params: {
        uid: prismicDocument.uid,
      },
    };
  },
});

Here's our TOC component from the docs, it's got a lot of extra stuff you won't need though:

import React from 'react'
import PrismicTypes from 'types/PrismicTypes'
import { RichText } from 'components'
import {
  isHeader2,
  makeGetAnchorIdFunction,
  sliceTypesWithHeaders,
  tocTextBlockCheck,
} from 'utils/tableOfContentsHelpers'
import styles from './RightSidebar.module.scss'

export default function TableOfContents({ tocTitleField, sliceZone }) {
  const slicesWithHeaders = sliceZone.filter((slice) => (
    Object.keys(sliceTypesWithHeaders).includes(slice.slice_type)
  ))

  const getAnchorId = makeGetAnchorIdFunction()

  const tocLinks = slicesWithHeaders.map((slice, index) => {
    const headerFieldKey = sliceTypesWithHeaders[slice.slice_type].field
    const headerField = slice.primary[headerFieldKey]
    return headerField.map((block, blockIndex) => {
      if (tocTextBlockCheck(block)) {
        const linkClass = isHeader2(block) ? styles.h2Link : styles.h3Link
        const anchorId = getAnchorId(block)
        return (
          <li
            className={linkClass}
            key={`toc-link-${index}-${blockIndex}`}
          >
            <a href={`#${anchorId}`}>{RichText.asText([block])}</a>
          </li>
        )
      }
      return null
    }).filter((item) => item != null)
  }).filter((item) => item.length > 0)

  if (tocLinks.length === 0) return null

  return (
    <div className={styles.tableOfContents}>
      <h3>{RichText.asText(tocTitleField)}</h3>
      <ul>
        {tocLinks}
      </ul>
    </div>
  )
}

TableOfContents.propTypes = {
  sliceZone: PrismicTypes.sliceZone.isRequired,
  tocTitleField: PrismicTypes.richText.isRequired,
}
1 Like

Thanks a lot (once again), managed to implement something that works. Using your solution makes complete sense as they can decide whether or not to show a TOC or not.

2 Likes

This topic was automatically closed 24 hours after the last reply. New replies are no longer allowed.