“Don’t solve the problem” - Implementing dynamic table of contents from the Notion API

Do more, in less time P1: Applying problem solving mental models to the problem of automatically generating a table of contents using the Notion api in record time.

Dec 27, 2021

This begins a series of articles showing how I use various problem-solving techniques (e.g. Pareto - 80/20 principle and Parkinson’s Law) to do more work in less time.

Recently, I wanted to create a sitemap dynamically from the blocks I get back from the Notion API for each blog post I write. The final product looks something like:

To begin, I thought about creating a data structure that looked like:

interface TableOfContentsEntity {
  type: 'heading_1' | 'heading_2' | 'heading_3';
  title: string;
	children: TableOfContentsEntity[]
}

And I had an accompanying function to generate the data structure:

function createTableOfContents(notionBlocks: NotionBlock[]): TableOfContentsEntity[] {
	// ....
}

Where notionBlocks is a flat list that look something like:

interface NotionBlock {
	id: string;
  type: "paragraph" | "heading_1" | "heading_2" | "heading_3" | "bulleted_list_item" | "numbered_list_item" // ... 
  // ... data
}

The full description of the Notion block object can be found here.

The solution to createTableOfContents didn’t pop to mind instantly, so I looked at the options that my mind surfaced in the moment:

  • “Spike” using Google: Google around to see if anyone had done something similar
  • Sit down with a pen and paper and figure it out
  • Reframe the problem
  • Scrap the table of contents sidebar for now, as I hadn’t even released the website yet anyway

The first step of this process I always take, is a mental note that I have to achieve this in an unreasonable amount of time. By doing this, you'll find yourself thinking outside the box. Otherwise, tasks with deadlines tend to take as long as the deadline allows. This is Parkinson’s Law. This is obviously unideal as it gets in the way of creative problem solving and efficiency. This is one of the ways Tim Ferris describes how we can live the “4 hour work week”. Or in my case, determine how you can get a 40 hour work week done in a 4 hour work week, then do a 84 hour work week. Imagine whats possible!

“Spike” using Google

I always believe it’s a good idea to “Spike” via a quick Google. I spend 5 mins Googling efficiently (topic for another article) to work out if I can re-work (or copy) someone elses solution, thereby eliminating the problem for me all together.

Unfortunately, my Googling sesh turned out to be fruitless. Back to the options:

  • “Spike” using Google Google around to see if anyone had done something similar
  • Sit down with a pen and paper and figure it out
  • Reframe the problem
  • Scrap the table of contents sidebar for now, as I hadn’t even released the website yet anyway

Sit down with a pen and paper and figure it out

Jokes. We should rarely solve problems like this. We’ll come back to this in the worst case scenario.

Back again!

  • “Spike” using Google Sit down with a pen and paper and figure it out
  • Google around to see if anyone had done something similar
  • Reframe the problem
  • Scrap the table of contents sidebar for now, as I hadn’t even released the website yet anyway

Reframe the problem

There are lots of ways of going about this step; it’s the hardest, but most fruitful. It becomes easy with practice, like everything else, our brains, are neuroplastic after all. Here is the journey I went on:

I began by asking myself, what the essence of what I was trying to do was. Why was I doing it? What was the motivation?

Some of the motivations include:

  • For long articles of many semi-independent it can assist the user in quickly “jump” to the section of an article they care about
  • This can tell them if you’re going to answer their question or take them down a path that piques their curiosity.

We can draw a few insights:

  • We don’t need a table of contents for all articles. This doesn’t help too much with the problem at hand. We’ll come back to this later.
  • We can achieve the above without a table of contents. For example, a quick summary at the beginning of an article of the main points covered, with links to the relevant sections. The problem with this is we’ll have to write a block at the beginning of each article, manually. Blech! Repeated actions should be automated.
  • (Pareto-ish prinicple application) The user gains most value from having a table of contents, not necessarily having a hierarchical tree of all types of headings. This is extremely useful, as the issue above with createTableOfContents stemmed from the fact that TableOfContentsEntity was a tree of headings. Why don’t we just generate a flat list of heading_1s instead?

Insight: Only render the top level heading_1s

This turns our data structure into:

interface TableOfContentsEntity {
  title: string;
}

And reduces our createTableOfContents function into a 2 minute implementation:

const isHeading = (block: Block) => block.type === 'heading_1';

const extractTitleFromBlock = (block: Block) => // ...

export const createTableOfContents = (blocks: Block[]): TableOfContentsEntry[] =>
  blocks
    .filter(isHeading)
    .map((block) => ({
      title: extractTitleFromBlock(block),
    });

Implementing the tree: “what can I remove from the problem”

At this point, we have essentially used scope reduction and problem transformation to turn the task from a potentially 1h+ task into 2min task.

How about if we wanted to apply these same ideas, and implement a hierarchical table of contents without reducing the scope and still taking < 10min?

Instead of just diving into the solution that popped into my mind at the beginning to create a tree, I thought to myself, how can I transform the problem so the most “diffiuclt” part is removed?

I couldn’t see an instant solution to generate a tree, so let’s ask ourselves, how can we not generate a tree.

This is a useful exercise to repeat,

💡
Instead of asking yourself “can can I do X”, ask yourself “how can I remove X”.

Through this exercise, I realised that I could just generate a flat list, and delegate the structural aspects to the rendering to React. If I gave React a data structure similar to:

interface TableOfContentsEntity {
  title: string;
  type: 'heading_1' | 'heading_2' | 'heading_3';
}

type TOC = TableOfContentsEntity[];

I could then capture the hierarchical structure in a flat list using:

const isHeading = (block: Block) => ['heading_1', 'heading_2', 'heading_3'].includes(block.type)

const extractTitleFromBlock = (block: Block) => // ...

export const createTableOfContents = (blocks: Block[]): TableOfContentsEntry[] =>
  blocks
    .filter(isHeading)
    .map((block) => ({
			type: block.type,
      title: extractTitleFromBlock(block),
    });

Now in the client side, we need to render something like:

This is definitely a lot simpler than trying to generate the tree on the server side. All we have to do is render an indentation using each items heading type:

interface Props {
  toc: TableOfContentsEntry[];
}

const getMarginLeftForHeadingType = (type: TableOfContentsEntry['type']) => ({
  'heading_1': 0,
  'heading_2': 14,
  'heading_3': 20,
})[type];

function TOC({ toc }: Props) {
  return (
    <TOCWrapper>
      {toc.map((tocItem) => (
        <TOCItem ml={getMarginLeftForHeadingType(tocItem.type)}>
          {tocItem.title}
        </TOCItem>
      ))}
    </TOCWrapper>
  );
}

Finito!

The above may have seemed like a long process, but with practice it takes mintues and happens automatically.

In the next few articles on this topic I’ll show how these principles are applied in business building, design, UX and lifestyle design.

To see how this is implemented, take a browse of the code in my personal website repo.

Get the latest from me

If you want to hear about updates about this place (new posts, new awesome products I find etc) add your email below:

If you'd like to get in touch, consider writing an email or reaching out on X.

Check this site out on Github.