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:
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!
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:
Jokes. We should rarely solve problems like this. We’ll come back to this in the worst case scenario.
Back again!
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:
We can draw a few insights:
createTableOfContents
stemmed from the fact that TableOfContentsEntity
was a tree of headings. Why don’t we just generate a flat list of heading_1
s instead? heading_1
sThis 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),
});
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,
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>
);
}
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.