How I Use Notion As My CMS For My Gatsby Site

July 14th, 2019

For the past year I have been using Notion as a general life manager and so far it has been incredibly useful in creating one space where I can manage to-do's, content, goal tracking, and even my personal finances.

One aspect where Notion falls short, is that it doesn't have an official API, therefore it's limited to it's own platform when it comes to development or expansion. Contrary to this, I started seeing that many people on Twitter were using the unofficial Python API or even reverse-engineering Notion's API in order to use it as a CMS.

I fell in love with the idea of being able to use Notion as a CMS specifically because of my use of Notion on multiple devices and the idea of being able to write and publish from anywhere would reduce friction from me being able to put out content.

Therefore I began my journey of trying to integrate Notion as a CMS for my Gatsby-based blog, tony.so.

Step 1: Research

I found a couple of different articles and resources that helped me to understand if this was possible and the tools that I'd have to use:

I found the unofficial Notion API's in Go and in Python. After some exploration I definitely preferred the Python client because I'm more familiar with Python and it seemed like something that would play nicer with popular deployment tools.

After some more research, I figured that from an idea point-of-view this project could be done by:

  • Having a Python script that loop through pages in Notion and extract their content and put them into .md files. If I point that to the right directory, Gatsby will create blog posts from those markdown files.
  • I'd have to have a custom build script that will let me run a Python script that does the above prior to running gatsby build .

Step 2: The Script

The way that this worked was tricky, debugging exactly how to receive the text from a Notion page took most of the time.

Ultimately I just wanted to be able to write blog posts from my iPad without leaving the Notion app, and since I wanted these blog posts to be independent of my day-to-day Notion setup I made a new page in my Notion setup called "Blog Posts" like so:


Setting up this sandboxed environment lets me start playing around with a script that didn't take too long to finish up:

// get_blog_posts.py

import os
import datetime
from notion.client import NotionClient

client = NotionClient(token_v2="NOTION_TOKEN")

blog_home = client.get_block("NOTION_BLOG_POSTS_PAGE")

# Main Loop
for post in blog_home.children:

    # Handle Frontmatter
    text = """---
title: %s
date: "%s"
description: ""
---""" % (post.title, datetime.datetime.now())

    # Handle Title
    text = text + '\n\n' + '# ' + post.title + '\n\n'

    for content in post.children:

        # Handles H1
        if (content.type == 'header'):
            text = text + '# ' + content.title + '\n\n'

        # Handles H2
        if (content.type == 'sub_header'):
            text = text + '## ' + content.title + '\n\n'

        # Handles H3
        if (content.type == 'sub_sub_header'):
            text = text + '### ' + content.title + '\n\n'

        # Handles Code Blocks
        if (content.type == 'code'):
            text = text + '```\n' + content.title + '\n```\n'

        # Handles Images
        if (content.type == 'image'):
            text = text + '![' + content.id + '](' + content.source + ')\n\n'

        # Handles Bullets
        if (content.type == 'bulleted_list'):
            text = text + '* ' + content.title + '\n'

        # Handles Dividers
        if (content.type == 'divider'):
            text = text + '---' + '\n'

        # Handles Basic Text, Links, Single Line Code
        if (content.type == 'text'):
            text = text + content.title + '\n'

    title = post.title.replace(' ', '-')
    title = title.replace(',', '')
    title = title.replace(':', '')
    title = title.replace(';', '')
    title = title.lower()

        os.mkdir('../content/blog/' + title)

    file = open('../content/blog/' + title + '/index.mdx', 'w')
    print('Wrote A New Page')

For reference this is the page that blog_home is referring to, this is important if you want to copy the script to use for your personal setup since it's just expecting individual pages inside an empty "Blog Posts" page: 4a64467e-8f84-4ae4-bd90-da1b204b0841

The script handles most of the markdown that is normal to everyday use and will write them to my content/blog/ route where it'll create a folder with the title of the blog post and put the text inside an index.mdx (because I have MDX setup in case I'll ever need it but it'll work fine for just .md files if you change it).

For reference my current tree structure works like so:

- tony.so/
-- /content
--- /blog
---- Posts Get Dynamically Created Here!
-- /notion
--- get_blog_posts.py
--- Pipfile
--- Pipfile.lock
-- /src
-- /static
-- build.sh
-- package.json
-- package-lock.json
-- gatsby-browser.js
-- gatsby-config.js
-- gatsby-node.js

Essentially you'd like to point the folders and index.mdx files wherever you have gatsby dynamically creating pages based off a .md files.

Step 3: Deployment

Since the original goal for this project was to be able to have a setup where I never had to leave the Notion app from my iPhone or iPad and I honestly did not have a great idea about how I could deploy this.

After some research into using Netlify I figured that since they have a trigger build hooks that it might be a good idea to use them (and their integration with a Python setup seemed easy to use).

So I configured a build.sh to as my build script and configured a netlify.toml to set up Netlify as my CI. Also since get_blog_posts.py is using Python 3.7, a simple runtime.txt file needed to be created to point Netlify to the right Python version.

# build.sh
pip install pipenv

echo "Starting build"
cd notion
pipenv install

echo "Get posts"
pipenv run python get_blog_posts.py
cd ..

echo "Build frontend site"
npm install
npm run build
# netlify.toml
  publish = "public/"

  # Default build command.
  command = "bash build.sh"
# runtime.txt

I first had this setup running through githooks to see that all was smooth sailing but then further dived into the webhooks that Netlify has available.

It was pretty easy to setup a webhook and I figured that an easy setup of this would be just to add a page to my website that would run a POST to the Netlify webhook that I created on a page load.

I gave my react component name a random hash in order to hide it a little bit from the public, sadly I still can't think of a solution to completely make this process exclusive to just me, but if you do feel free to reach out to me on Twitter!

// hash.jsx
import React, { useEffect } from "react";

const hash = () => {

    useEffect(() => {
        fetch("NETLIFY_WEBHOOK", { method: "POST" })
    }, [])

    return <div>Build Triggered!</div>

export default hash

Gatsby is also set up to create pages for me depending on which directory I put it on (for me it's my pages directory).

So the final result was to create a web bookmark on my home notion page in order to build my site whenever I am done with a blog post.

It looks like the following: 4a13cb7e-bde6-4e6b-8bf8-c876cbc0e212

There's still a lot of problems with this set up but it definitely works and has been helping me to quickly publish posts from anywhere which was the goal in the first place. I wanted to reduce any amount of friction that I could possibly have to get stuff out there.

Hope this helps!