thmsmlr

Notion powered Next.js blog on Vercel

June 25, 2020☕️4 min read

I recently rebuilt my blog. My entire website is powered by next.js, but I decided to use the latest SSG features in next.js 9.3 to use Notion as the headless CMS to power my blog.

There are definitely pros and cons, but the main reason why I tackled it so I can more comfortable write blog posts in a WYSIWYG editor on my iPad. Like this post! Anyways, this is how I did it. It was pretty easy.

Next.js 9.3+ getStaticProps

The main idea behind Next.js is that it's a hybrid framework where you can determine whether you are using Server Side Rendering (SSR), or Static Site Generation (SSG) on a page per page basis. This gives you the power of statically compile your blog and homepage of your website, while retaining the flexibility to build out more dynamic pages as you see fit. I don't know what i'm going to use this for, but maybe it'll be useful in the future.

Next.js 9.3+ specifically makes it easy to statically generate a dynamic number of pages using their recent additions getStaticProps and getStaticPaths.

The basic idea is that you can create a page with a dynamic route, say page/blog/[slug].js and export getStaticPaths which dictates the possible values for slug at build time. Then you can implement the getStaticProps method to compute the specific props for the specified slug.

In the case of this blog, the code for the page/blog/[slug].js page looks something like this

import Notion from 'lib/notion'
import { NotionRenderer } from 'react-notion'

const DB_PAGE_ID = 'xyz'

export default function Page({ post }) {
	return (
		<div>
			<NotionRenderer blockMap={post.recordMap.block} />
		</div>
	)
}

export default getStaticPaths() {
	const posts = await Notion.getTable(DB_PAGE_ID)
	return {
		paths: posts.map(x => ({
			params: { slug: x.Slug }
		})),
		fallback: false,
	}
}

export default getStaticProps(ctx) {
	const posts = await Notion.getTable(DB_PAGE_ID)
	const post = posts.find(x => x.Slug === ctx.params.slug)
	return {
		post: await Notion.getPage(post.id)
	}
}

The idea is here is pretty simple, I have a library function to lib/notion that allows me to fetch a specific Notion page or database using the private Notion API. NOTE: This is a private API and if you are going to copy me know that Notion will probably break this API at some point. We use the getStaticPaths function to enumerate the number of pages to render, then we use the getStaticProps function to load the specific blog post.

Notion private API

Next up, let's dive into how we fetch the blog posts to Notion. There are two main things you'll need to get:

  1. An authentication token
  1. The PageID of your database of blog posts

Start off by loading Notion into your web browser, create a database to store your blog posts. Create whatever columns on the table you want to use in your next.js code. Then write a demo post.

Notion blog posts database page
Notion blog posts database page

To get the PageID of your posts database, load up that database page in Notion in Chrome, then open the Chrome Dev Tools, go to the network tab, search for the loadPageChunks API request. Scroll down the request parameters and you'll see the page_id of the page, copy that value into the DB_PAGE_ID variable referenced in the code sample above.

notion image

Next, head on over to the application tab in the dev tools and look for the cookies stored for Notion.so. You'll see a cookie called token_v2 that will be your JWT authentication token. Save that and store it in your Vercel environment variables.

notion image

You'll then be able to setup your environment by running vercel env pull in your project so the NOTION_TOKEN is available whenever you are running your code locally using vercel dev. Remember to set this variable in the Production, Preview and Development sections so that it is available in all environments. If you are not using Vercel, you can use whatever strategy you like to manage your secrets.

notion image

Great, so you have your blog posts setup in Notion and your code setup, you can head on over to my GitHub to steal the lib/notion.js library that I wrote to power this site. This library is pretty basic, I would suggest just copying the code, no need to make it a npm package. The library will use your the NOTION_TOKEN to communicate the v3 private API. WARNING: This is a private api and Notion and they will probably make breaking changes at any time.

Publishing new blog posts

Since my site is deployed on Vercel I can take advantage of one of their new features, deploy hooks. The rough idea is that you can create a unique URL that when hit, it will trigger a new deploy that will statically compile all the blog posts from Notion and deploy them across their global CDN.

To create one of these deploy hooks is pretty easy, got into the Git Integration section of your projects settings and create a deploy hook,

notion image

I then copy the URL, paste it in a Notion page so I don't lose it. Any time I click it, my site is redeployed with the latest changes.

Future Work

When you upload an image into notion it will store the results into an S3 bucket in us-west-2, for example,

https://notion.so/image/https%3A%2F%2Fs3-us-west-2.amazonaws.com%2Fsecure.notion-static.com%2Fdb5226d2-9f7a-4470-a05e-ce91577388fa%2FUntitled.png

It would be great to figure out an automated process to get those images served through the Vercel CDN. My first thought is to use the routes directive in the vercel.json configuration file. This probably won't ship them to the edge, but will in theory cache them at the edge after the first request? I'll have to do some testing and read some more documentation.

It would also be nice to explore better ways to do incremental rebuilding of pages. Next.js 9.4 has the preview mode and the incremental rendering of static pages using the revalidate parameter to do a stale-while-revalidate style recomputing of a page.

I don't think i'll use the fallback and revalidate parameter because I don't really want live editing of the site. I think i'd rather do a full rebuild of the site, but I may explore the preview mode work so I can at least see what the page may look like before I trigger the full redeploy. I'll poke around and see what feels good for a workflow.