Welcome to the first workshop in this series! The final goal of the project is to create a table of contents for the headings in an MDX blog post.

Note: as mentioned in the prerequisites, this challenge is meant for folks who are already familiar with React, Next.js and MDX. If you’ve never worked with these technologies, this series will be quite daunting.

Goals for the entire series

1. Table of contents

workshops 1 and 2

There will be a table of contents that contains all of the h2 and h3 level headings in the blog post.

blog post with a table of contents on the right

2. Responsive display

workshop 3

The table of contents will only display when the viewport is wide enough.

3. Sticky positioning

workshop 3

The table of contents will be “sticky” – that is, it won’t scroll off the page when you scroll to see later parts of the blog post.

4. Scroll to headings

workshops 4, 5 and 6

  • Each item in the table of contents will be link that will scroll the corresponding blog post heading to the top of the viewport.
  • For users who do not have “prefers-reduced-motion” enabled, the page will scroll smoothly to the appropriate heading.
  • The scroll will account for the height of the sticky page header at the top.

Scaffoding code

All right! Time to get started with workshop #1: Extract the headings from the MDX.

This workshop starts out with the MDX blog site already written . The blog posts were generated by ChatGPT and shouldn’t be taken seriously. 😅

Before you can start the workshop, you’ll need to follow the “Getting Started” instructions in the project README .

If you’re interested in a series for how to write the MDX blog site that comes with the scaffolding code, please let me know by tagging me in a post on LinkedIn or Twitter !

Workshop goal

  • By the end of this workshop, you should see a list of level 2 and level 3 headings in the console when you visit a blog post page.

You can work by using the resources at the bottom of this page only (and your own research and skills), or by looking at the hints – or even by visiting the solution page and then going back and writing the code from memory.

Workshop context

  • This workshop is an intermediate step before displaying the table of contents. There is no UI associated with this particular workshop.
  • You can assume the headings in the blog post MDX files are written using Markdown syntax (an h1 is indicated by a single #, h2 by ##, h3 by ### etc.)
  • ”Heading” vs. “Header”: I get these mixed up a lot. In HTML-land, a heading is an element of the form h#, such as h1, h2, etc. A header is an element that shows across the top of an HTML page.
  • Why level 2 and level 3 headings only? There’s only one level 1 heading , and you can get to it by scrolling to the top of the page. It’s an arbitrary decision to stop at level 3 headings; I’m going on the assumption that level 4 headings will clutter up the table of contents, and folks won’t need to navigate anywhere that specific. Also, none of the blog post (so far) have level 4 headings.


Hint 1 You can get the MDX content from the blog post in loadBlogPost, in the src/helpers/file-helpers.ts file. This would be a good place to look for the headings. You can then return the headings from loadBlogPost (in addition to the frontmatter and content items that are already being returned).

Hint 2 Parsing the MDX is best done with regular expressions . If you’re not up on your regular expressions, here’s an example of extracting MDX headings this way . Note: for now we’re not intersted in the id or the slug — just the title and the level.

Hint 3 You can adapt the example heading extraction code to add the headings to the array only if the level value is 2 or 3.

Hint 4 loadBlogPost is called from src/components/BlogPost/BlogPost.tsx . This is where you can destructure the headings from the return value and then print them to the console (using console.log).

Hint 5 Because BlogPost is a React Server Component , the console.log output will be shown in the terminal where you’re running npm run dev – not in the browser console.