Publishing blog posts from Notion with 1 click

July 29, 2021

I finally wrote a blog post after telling myself I would for 10 years.. and it wasn't until I made a workflow that reduced the friction of publishing to a single click. My past attempts used git to track content but it was just hasslesome enough that I never used it.

Now, I use Notion to write my content and publish it with a Gatsby app deployed on Cloudflare Pages. My favorite part about this setup is that my editor of choice, Notion, also doubles as my Content Management System so writing or editing automatically saves my changes with the source of truth. Publishing changes is just a matter of running the deploy script to grab the latest content from Notion and generate the static web pages.

The key steps in this process are...

  • Writing posts in Notion

  • Downloading Notion posts as Markdown

  • Transforming Markdown into HTML with Gatsby

  • Deploying Gatsby on Cloudflare Pages

Steps 2~4 are run automatically after clicking deploy in the Cloudflare Pages web console.

Writing posts in Notion


My posts are organized as rows in a Notion table with a few properties.

- **Name** - Title of the post as well as a link to the content page

- **Published** - Marked as true to generate an HTML page on next deployment. Post is invisible otherwise. 

- **Slug** - URL path

- **Publish Date** - Publish date

All of these fields were defined by me and don't have any special meaning from Notion's perspective so I can add modify the schema however I wish provided that I update my build script to handle the new behavior.

Downloading Notion posts as Markdown

The next step is to grab the data from Notion using the unofficial notion-py API. I based my script off of this one which downloads Notion pages as Markdown. The markdown formatting code is mostly the same but I added some code to handle the table format which I described above.

Another interesting bit I had to add was some image processing to reorient the image if its JPEG EXIF metadata indicates that it's not in the standard orientation. The reorientation fixes a tricky bug where portrait images uploaded from my phone would be rendered by Gatsby with the wrong aspect ratio. Turns out that one of the downstream libraries doesn't check for the orientation field so it mixes up the width and height of an image when calculating its display size.

Transforming Markdown into HTML with Gatsby

Gatsby is a React-based front end framework for building websites. When I chose it, I was looking for a static site generator so I was comparing it to options like Hugo and Jekyll. It looks like Gatsby has grown into a much more powerful tool but static site generation is still my primary use case for it.

One of it's features, through the gatsby-transformer-remark plugin, is parsing markdown files into HTML. Their official docs actually has a guide on how to set that up because it's a common use case.

It's commonly paired with gatsby-remark-images to process images in markdown but I actually use a less popular fork of it called gatsby-remark-images-custom-widths. As far as I can tell, gatsby-remark-images only supports a single maxWidth property for all images so I was unable to specify different widths for each of my images. Another user ran into the same issue and made the fork that I'm now using.. thanks!

Another approach would be to reprocess the image into the target width when I run the notion downloading script. This has the additional benefit of reducing the image file size, use less bandwidth, and reduce load time.

Deploying Gatsby on Cloudflare Pages

Incredibly, I'm able to build and host my website for free using Cloudflare Pages. I was originally using Netlify (also great) but I switched over because Cloudflare Pages supports free server side analytics whereas Netlify charges $9/month. The free server side web analytics are pretty bare bones but that's fine for me since I just want a rough measure of traffic and be able to tell which posts are the most popular. The alternative was to use Google Analytics but I wanted to avoid adding a client side tracker*.

Cloudflare Pages (and Netlify) supports build scripts so I'm able to run the notion downloading script as part of the deployment and fetch the latest content with every build. The python requirements for the script are specified with a Pipenv file in the root folder.

Another benefit of using Cloudflare is that it handles HTTPS encryption for me. All I had to do was point my domain's name servers to it.

*I noticed that Cloudflare added a beacon tracker to my site but I think it can be removed given that their analytics works without it. Looks like there's some recent discussion on it.


I like this setup because...

  • Notion is my favorite editor and is now also the source of truth for content.

  • Notion is available on all of my devices.

  • Updating content is as simple as making edits and clicking deploy.

  • Everything is free aside from the domain name which I purchased on Namecheap.