šŸ§‘šŸ½ā€šŸ’» Notion as a CMS

Tags
APINotionTutorial
Date Created
9/16/2024

If you take notes with Notion, this is for you.

Overview

Youā€™ve heard of SaaS and PaaS and BaaS. Get ready for the latest and greatest aaS yet - NaaBaaS (Notion as a Backend as a Service).

Idea

The idea was simple. Use Notion to power my portfolioā€™s blog functionality, and make pushing updates as easy as possible.

Some of my intuitive ideas were as follows:

  • Leverage Notionā€™s built-in deployment capabilities and somehow embed the pages into my site
  • Use Notionā€™s API to extract my blog data, and convert JSON to Svelte components
  • Use a 3rd party library to npm install my way to victory.
  • There are great options for #3. A couple ā€œehā€ ones for #1. Of course I opted for #2.

    Steps

    Notion Setup

  • Set up your Notion integration (Menu ā†’ Connect To ā†’ Manage Connections)
  • Select ā€œDevelop or manage integrationsā€
  • Name integration, keep internal, and uncheck the box for ā€œmake user info availableā€
  • Return to the desired database and connect to new integration (Menu ā†’ Connect To ā†’
  • Node.js + Typescript

    The next few steps are used to extract and parse the data from your selected database

  • Initialize a node project npm init -y
  • Install Notion API and dotenv using npm install @notionhq/client dotenv
  • Create an entry point file called index.js and create a structure like this šŸ‘‡šŸ½
  • // index.js (pseudo code) import { Client } from "@notionhq/client"; import { DataFetcher, DataTransformer } from "./lib"; const DATABASE_NAME = "Example"; const notion = new Client({ auth: process.env.API_KEY }); async function main() { const fetcher = new DataFetcher(notion, DATABASE_NAME); await fetcher.getDatabaseId(); const pages = fetcher.getDatabasePages(); await fetcher.saveToFile(pages, outPath); const transformer = new DataTransformer(inputfile); await transformer.loadPages(); await transformer.parseData(); await transformer.saveTransformedPages(outPath); )

    Note: This code is meraly outline and not intended as a working sample.

    // DataFetcher.js export class DataFetcher { private notionClient; private databaseId?; constructor(notion, databaseId?) { ... } async getDatabaseIdFromQuery(dbName) { try { const response = await this.notion.search({ query: query ... // Get from API docs // https://developers.notion.com/reference/post-search this.databaseId = response.results[0].id; } catch(err) { } } async getDatabasePages() { try { const { results } = await this.notion.databases.query({ database_id: this.databaseId, }); const { pagesWithBlocks } = await Promise.all( results.map(async (page) => { const blocks = await this.getBlocks(page.id); } return pagesWithBlocks; } catch (err) { } } // getBlocks is a helper that uses this API endpoint // https://developers.notion.com/reference/get-block-children async saveToFile(pages, outPath) { await fs.writeFile(outPath, JSON.stringify(pages, null, 2)); }

    The output of the Fetcher class is huge. Just a few pages can quickly generate thousands of lines of JSON code. This is what our Transformer is for

    // DataTransformer.js class DataTransformer { private inputFile; private pages = []; constructor(inputFile, pages) { ... } async loadPages() { try { const data = fs.readFile(...) this.pages = JSON.parse(data); } } async transformPages() { pages.map((page) => { const { // destructure what you can }} = page; return { ..., blocks: resolveBlocks(children); } } } async saveTransformedPages(outPath) { fs.writeFile(...) } resolveBlocks = (blocks) => { return blocks .map((block) => { console.log(block.type); const blockHandler = this.blockMap[block.type as keyof typeof this.blockMap]; if (blockHandler) { return blockHandler(block); } console.log("NOT SUPPORTED:", block.type); return null; }) } private blockMap = { paragraph: (block) => { ... }, image: (block) => { ... }, heading_1: (block) => { ... } // extend as needed } }

    Now what?

    If you either cloned my repo or tried to fill in the gaping holes in my pseudo-code (congrats btw), then you are now the proud owner of a clean and intelligible JSON document. The next step is to write a React (or Svelte, Vue, etc) component that can consume the data in the exported format. As an example, Iā€™ll attach my component (Svelte) below:

    <script lang="ts"> const renderBlock = (block: { component: string; text: string; code: string; }) => { switch (block.component) { case "Paragraph": return `<p class="my-1">${block.text}</p>`; case "Heading1": return `<h1 class="text-3xl my-4 font-bold">${block.text}</h1>`; // etc } }; </script> <div class="flex flex-col justify-center mx-auto sm:px-16 sm:mt-2"> <div class="mb-6"> <a href="/notes" class="text-primary hover:text-accent">Go Back</a> </div> <p class="text-4xl lg:text-5xl font-bold mb-8 lg:leading-tight"> {data.note.icon} {data.note.title} </p> <div class="text-neutral-400 flex flex-col gap-2"> <div class="grid grid-cols-3 gap-4"> <div class="col-span-1 flex gap-2 items-center"> <IonList class="inline" /> <span>Tags</span> </div> <div class="col-span-2"> {#each data.note.tags as tag} <span class="text-xs bg-primary text-white rounded-full px-2 mr-2" >{tag}</span > {/each} </div> </div> <div class="grid grid-cols-3 gap-4 mb-8"> <div class="col-span-1 flex gap-2 items-center"> <BiCalendar class="inline" /> <span>Date Created</span> </div> <div class="col-span-2"> {new Date(data.note.dateCreated).toLocaleDateString()} </div> </div> </div> {#each data.note.blocks as block} {@html renderBlock(block)} {/each} </div>

    Further Help

    If you get stuck, check out the code on Github or reach out via the ā€œContactā€ section of this website!

    https://github.com/jakeevans00/notion-scripts

    Jake Evans @2024