Leveraging APIs like Notion with a nice caching system
Table of Contents
I recently added a Gaming Library page, which involved a bunch of API calls via Notion, as well as the PlayStation Network for that extra nerd factor. I initially only used Notion’s API for my Now page, but as I was reusing a lot of code, I ended up consolidating everything into helper functions on multiple levels, leveraging eleventy-fetch's AssetCache feature along the way (which you can use without Eleventy). Maybe you’ll find some of this stuff useful, though, note that this article assumes you are at least a little familiar working with APIs and JavaScript.
The Setup
My Notion setup is extremely manual for both Gaming Library and Now pages. I won’t go into details here but I had to create a “connection” in Notion, so I could call the API for my workspace, for which I received a secret token. And my PSN data is linked to my account, and is all automatic! (npm packages used: @11ty/eleventy-fetch @notionhq/client psn-api)
Ramble
I really appreciate Notion’s tooling and UI but golly gee, do I hate its sluggish speed. It spits out HTML with inline styles every step of the way and is just so dang slow (web or app on a very fast computer), and it’s near impossible to add custom styles. I almost gave up editing more than once due to the hellish non-responsive interface that would sometimes make me edit a completely different row… But sure let’s prioritise adding ✨A.I.✨ garbage. Anyway, rant over. (and their API is nice and fast, to their credit)
The Flow
- Send a request to the API endpoint for one slice of data (I’ll refer to this as
info) that is fast to query.- Notion allows querying the database itself separately from the actual data, which includes a useful
last_edited_timeproperty. - The PSN API is less granular, but returns results in descending chronological order, so we can query the latest updated game by setting
limitto1, and look at thelastUpdatedDateTimeproperty.
- Notion allows querying the database itself separately from the actual data, which includes a useful
- If there is a cache for this info, check if it’s the same value as the freshly queried info.
- If the date is the same, look for the data cache and return it, bypassing further steps.
- However, if the date is different, proceed below to get all the data.
- Query the API for the entire
data, and if there are paginated results, grab every page (in my case, both Notion and PSN APIs provide this information, so luckily there’s no guesswork involved). - Process the
datato remove any unnecessary properties, and normalise certain values (Notion returns rich text by chunk, so I convert it to Markdown with a customrichTextBlockToMdfunction). - Cache both the initial
infoand processeddatafor future use. - Return the requested
data. Done!
As you can see, this logic can be applied to more than just one API. Aside from the properties to check in the info response, and the data processing itself, it’s generic. As such, I have created a helper function in my api-cache file that does all this — greatly reducing code duplication.
By getting that info first, I can avoid querying 300 items from the PSN API if the data didn’t change, for example. I don’t think I’ll be hitting a rate limit any time soon, but this "sampling" method makes it quick to check if the entire data is stale or not.
The Abstractions
Here’s all my code split by file. This is fully commented but if something is unclear, let me know!
And I keep all the tokens and database IDs secret in my .env file. 🤫
NOTION_BEARER_TOKEN=secret_SOMESECRET
NOTION_DATABASE_ID_NOW=somedatabaseid123
NOTION_DATABASE_ID_GAMES=anotherdatabaseid456
PLAYSTATION_NPSSO_TOKEN=MyVerySecretSsoToken1
Now page
I’m not usually too busy so this page doesn’t change very often. It’s also pretty quick to make changes via Notion (I keep a tab pinned in my browser), by adding a new row, or archiving something that’s no longer current (like finishing a show). Using my function defined in notion-db, I can grab items filtered by their archived status of false (a checkbox type), then group them by category (music, book, game…). Since I have (formatted) blurbs with each entry, my "rich text to Markdown" conversion happens here. Once everything has been processed, I can cache the final result for next time!
Gaming Library
My Gaming Library is an old spreadsheet that I moved into Notion last year (though you could do all of this with Google Sheets or Airtable as well), adding a bunch of metadata that nobody cares about (but me!). I manually entered the PSN API’s game IDs into my Notion entries, one by one, to make sure they were accurate (matching by title could fail due to things like apostrophes, trademark symbols, etc.). Setting this up was a real pain, but it’s now quite easy to maintain!
Free idea
If you’re starting from scratch, you could invert this process and use the results from the PSN API to add new data to a database via the Notion API instead.
This page collects PSN and Notion data in two steps: grab PSN titles first, then match them to Notion items. It skips some platforms like GameBoy and PC (but you best believe I played the shit out of Pokémon Red and RollerCoaster Tycoon), filters out hidden rows and irrelevant properties, and includes data for each game within compilations, such as the Mass Effect Trilogy, to get a fuller picture. Then, cache and serve! (side note: I’m only including PSN stuff because I have always been in House PlayStation since the PS1 — if I had some Xbox consoles or played regularly on PC, I’d have loved to include those too)
All in all, it’s a very nerdy thing. Not many people care about this level of information, but it was a great opportunity to use some APIs. (also, I might as well have personal stuff on my personal website!) You’ll note I didn’t provide a full breakdown of how these abstractions are used in my now.js and gameslibrary.js data files as it’s very specific to my setup and might not be useful to everybody, but my website has a public repository, so you can go digging there (this post is long enough without two additional walls of code!).
Notion Markdown Converter
Right, I almost forgot… another wall of code. Here’s the function I wrote to convert a rich text value from Notion into a standard Markdown string. It is extremely naïve and can break very easily if your text includes any kind of Markdown character ([(~_*)]). If you want to build something extra robust, check out Ryan Boone’s article (Rich Text Formatting section).
/** Converts a Notion rich text block to a Markdown string. */
function richTextBlockToMd(block) {
const string = block.text.content;
let wrap = [];
if (block.annotations.bold) {
wrap.push('**');
}
if (block.annotations.italic) {
wrap.push('_');
}
if (block.annotations.underline) {
wrap.push('_');
}
if (block.annotations.strikethrough) {
wrap.push('~~');
}
if (block.annotations.code) {
wrap.push('`');
}
let mdString = `${wrap.join('')}${string}${wrap.reverse().join('')}`;
if (block.href) {
return `[${mdString}](${block.href})`;
}
return mdString;
}
And you can use it like this once you have retrieved the property (rich text is provided as an array, so .map() is used to handle conversion, followed by join('') to make it into a string):
const myText = row.properties.myText.rich_text.map((text) => richTextBlockToMd(text)).join('');
I almost considered writing some kind of Eleventy plugin, but this all feels pretty custom and opinionated, so I held off. In any case, I hope you found this interesting! I certainly learned a lot playing with these two APIs, and consolidating my code into reusable chunks was a great exercise.
Update, 2024-09-18
Would you look at that, somebody made a plugin! It’s notion2eleventy by Stefan Brechbühl. I still need my post-process-and-data-blend-before-caching setup so I’ll stick with my custom implementation, but if you thought my blog post was too long, this might just be perfect for your Notion-to-Eleventy needs.
