Post Icon

Creating a blog with Next.js and Notion!

#programming

TLDR

here is the github repo that I put all the code mentioned here.

when I was creating my personal website (the one you are reading this), I wanted to also have a blog, some place where I could post some of my stuff, just like this one you are reading. But I got stuck thinking that I didn’t want to:

  1. set up an external blog
  2. set up some framework just for this (such as Ghost)
  3. pay for some database that would store my posts
  4. pay for anything really…
  5. … you got the idea
I wanted something quick to set up and easy to add and remove posts from.
well… there it was, right in my face. Notion!

then, it all started, the mix of feelings that every developer has gone through:

  • GREAT IDEIA! I can’t wait to begin
  • start researching how to do it
  • sounds hard… and it will take a lot of work…
  • should I try an easier way?
  • NO! I can do it myself
  • and there I was, on the Notion API documentation, trying to understand how it works, what it gives from requests, and how to make it work with my framework of choice: Next.js

    instead of telling you all the ups and downs or trying to tell you to figure it out yourself because that way you will learn more, I’ll just show you how I did it. sometimes, we don’t have the time or energy to learn something from scratch, and that is okay! but before we start, here are something that I’ll need you to keep in mind (just so I don’t feel too judged on my lack of attention to details on the code…):

    1. I just wanted it done. not perfectly done, just done. working… and free
    2. I love typescript, but for these things I usually add the types later 🤷🏽‍♂️
    so, the first thing that I needed was to get the data from a database in my personal notion to my website using the notion api. that was simple enough, notion provides a SDK! to use that, you will need you notion api secret and the id of the database you will get your posts from

    here is the code of the class I created to simplify the usage

    copy button
    typescript
    import { Client } from "@notionhq/client"; export class NotionApi { private notion: Client; constructor() { this.notion = new Client({ auth: <YOUR API KEY>, }); } async getDatabase(databaseId: string, filter: any = undefined) { const response = await this.notion.databases.query({ database_id: databaseId, filter: filter, }); return response.results; } async getActivePosts() { const filterByActivePosts = { property: "active", checkbox: { equals: true, }, }; return this.getDatabase( <YOUR DATABASE ID>, filterByActivePosts ); } async getPageContent(pageId: string) { const response = await this.notion.blocks.children.list({ block_id: pageId, page_size: 100, }); } }
    ⚠️
    put your api secret and database id on the environment variables!

    the main getaway from the code above is:

    you will need to instantiate your client

    copy button
    typescript
    this.notion = new Client({ auth: <your notion api secret>, });
    you can query all the pages from a database, or filter them passing a filter prop
    copy button
    typescript
    const response = await this.notion.databases.query({ database_id: <your database id>, filter: { property: "active", checkbox: { equals: true, }, }, }); return response.results;

    and with the page id, you can query all the info on that page

    copy button
    typescript
    async getPageContent(pageId: string) { const response = await this.notion.blocks.children.list({ block_id: pageId, page_size: 100, // returns up to 100 blocks in a page }); }
    PS: for this example, I’m using a simple database on notion that has a boolean property called “active”. you can find the template for it here!

    ok! we can already get the pages (posts) from our database. So now, we can just render them directly on our website, right?! … right?…

    nope, sorry.

    notion works with blocks! that means that the const results returned is an array of “blocks” object, with A LOT of information on each one of them.

    For the block below

    Lacinato kale

    the block object returned from the api is

    copy button
    typescript
    { "object": "block", "id": "...", "parent": { "type": "page_id", "page_id": "..." }, "created_time": "...", "last_edited_time": "...", "created_by": { "object": "user", "id": "..." }, "last_edited_by": { "object": "user", "id": "..." }, "has_children": false, "archived": false, "type": "heading_2", "heading_2": { "rich_text": [ { "type": "text", "text": { "content": "Lacinato kale", "link": null }, "annotations": { "bold": false, "italic": false, "strikethrough": false, "underline": false, "code": false, }, "plain_text": "Lacinato kale", "href": null } ], "color": "default", "is_toggleable": false } }

    some information was removed, but the prop key was kept.

    so, a lot of information for a simple block, that means that we have to sanitize the information.

    for that, I selected only the props that were useful to me for rendering them on a webpage later. that resulted in a ENOURMOUS switch case (again, my main focus was not clean code…). I won’t put it all here, because there is no need to it (there is a github repo that has all of it), but for exemplification, it looks like this
    copy button
    typescript
    export const parsePageContent = (content) => { const parsedContent = content.map((block) => { const { type, has_children, heading_1, heading_2, heading_3, paragraph, to_do, toggle, callout, numbered_list_item, bulleted_list_item, id, table, quote, code, } = block; switch (type) { case "heading_1": return { type: "heading_1", text: heading_1.rich_text[0].text.content, color: heading_1.color, props: heading_1.rich_text[0].annotations, }; case "heading_2": return { type: "heading_2", text: heading_2.rich_text[0].text.content, color: heading_2.color, props: heading_2.rich_text[0].annotations, }; <there are more cases here...> case "paragraph": if (paragraph.rich_text.length == 0) { return { type: "empty_line", }; } else if (paragraph.rich_text.length === 1) { switch (paragraph.rich_text[0].type) { case "text": return { type: "paragraph", text: paragraph.rich_text[0].text.content, color: paragraph.color, props: paragraph.rich_text[0].annotations, }; case "code": return { type: "code", text: paragraph.rich_text[0].plain_text, color: paragraph.color, props: paragraph.rich_text[0].annotations, }; default: return { message: "This block type is not supported", type: paragraph.rich_text[0].type, }; } } else { let textList = []; for (let i = 0; i < paragraph.rich_text.length; i++) { let { text, href } = paragraph.rich_text[i]; text = text.content; let textObj = {}; if (href) { textObj = { type: "link", text: text, href: href, }; } else { textObj = { type: "text", text: text, href: href, }; } textList.push(textObj); } return { type: "text_list", text: textList, }; } <there are more cases here too...> } }); return parsedContent; };

    as you can see, a lot of code. there were some edge cases too, like the “paragraph” case above.

    but, it did the job. it returns a list of objects that has everything that I needed to render it. So now, what was left to do was receive this list and render what was necessary. and again… another really big switch case

    copy button
    typescript
    export const getComponent = (block: any) => { switch (block.type) { case "heading_1": return <h1>{block.text}</h1>; case "heading_2": return <h2>{block.text}</h2>; ... case "empty_line": return <br />; case "paragraph": const { bold, code, color, italic, strikethrough, underline } = block.props; const colorProps = color.split("_"); let textBlock; if (bold) textBlock = <strong>{block.text}</strong>; else if (code) textBlock = <code>{block.text}</code>; else if (italic) textBlock = <em>{block.text}</em>; else if (strikethrough) textBlock = <del>{block.text}</del>; else if (underline) textBlock = <u>{block.text}</u>; else textBlock = <>{block.text}</>; return <p colors={colorProps}>{textBlock}</p>; case "numbered_list_item": if (block.list.length > 1) return ( <ol> {block.list.map((li: string, i: number) => ( <li key={i}>{li}</li> ))} </ol> ); return <></>; case "divider": return <hr />; ... default: console.warn( `This block (${block.type}) type is not supported.` ); return <></>; } };

    and with that, after hours reading returned JSON from notion’s api, I managed to write this post on my notion and present it here to you!


    Thank you for reading all of it!

    If the information on this post was useful to you, maybe it will be useful to someone else too! Don’t forget to share!

    Posted on July 17, 2023
    Last edited on September 21, 2023